Home > OS >  Is there a way to flush ARM cache from EL0?
Is there a way to flush ARM cache from EL0?

Time:08-12

I have been trying to implement a PoC code for Spectre Attack on ARMV8 (I understand most ARMV8 processors aren't vulnerable to the attack but trying to implement it anyway). I am using the asm volatile ("DC CIVAC, %[ad]" : : [ad] "r" (addr)); instruction to flush the cache and then try to measure the time to read the flushed address again. But I don't find any difference in reading the address before and after clearing the cache. This has made me realize that the instruction I'm using for flush isn't working.

On reading the ARMV8 reference manual (https://static.docs.arm.com/ddi0487/b/DDI0487B_a_armv8_arm.pdf) I realized that for the DC CIVAC instruction to work the SCTLR_EL1.UCI should be set to 1. I am not sure of how I can check the value of the SCTLR_EL1 register and change it to 1 without requiring sudo privileges.

CodePudding user response:

The procedure hereafter provides a way to read/write SCTLR_EL1 from Linux user mode. Whether or not reading/writing SCTLR_EL1 on a running Linux system is possible/sufficient for being able to flush the cache is not answered.

It is obviously an unsafe practice since it does obviously break the Armv8-a security scheme - do not do try this at home but for learning purpose.

The procedure was executed on an OrangePi PC2 running Armbian Buster (kernel version was Linux orangepipc2 5.4.28-sunxi64 #20.02.7 SMP Sat Mar 28 17:25:10 CET 2020 aarch64 GNU/Linux.

Installing latest kernel/linux headers:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install linux-headers-current-sunxi64
sudo shutdown -r now

Compiling a minimal loadable kernel module with procfs support:

Makefile:

obj-m =/sctlr_el1.o
KBUILD_DIR=/usr/src/linux-headers-5.4.28-sunxi64
all:
        make -C $(KBUILD_DIR) M=$(PWD) modules
# Kernel module clean dependency
clean:
        make -C $(KBUILD_DIR) M=$(PWD) clean

install:
        insmod sctlr_el1.ko
remove:
        rmmod sctlr_el1

sctlr_el1.c:

// Credits:
//
// minimal kernel module:    https://docs.legato.io/17_07/yoctoOutofTreeKernelModule.html
// procfs example:           https://gist.github.com/BrotherJing/c9c5ffdc9954d998d1336711fa3a6480

#include<linux/module.h>
#include<linux/init.h>
#include<linux/proc_fs.h>
#include<linux/sched.h>
#include<linux/uaccess.h>
#include<linux/fs.h>
#include<linux/seq_file.h>
#include<linux/slab.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Frant");
MODULE_DESCRIPTION("Minimal kernel module suitable for reading/writing SCTLR_EL1");
MODULE_VERSION("0.0.1");

// write system register  SCTLR_EL1 (s3_0_c1_c0_0) with specified value.
static inline void system_write_SCTLR_EL1(uint64_t val)
{
    asm volatile("msr s3_0_c1_c0_0 , %0" : : "r" (val));
    asm volatile("isb" : : : "memory"); 
}

// read system register value SCTLR_EL1 (s3_0_c1_c0_0).
static inline uint64_t system_read_SCTLR_EL1(void)
{
    uint64_t val;
    asm volatile("mrs %0, s3_0_c1_c0_0" : "=r" (val));
    return val;
}

static int my_proc_show(struct seq_file *m,void *v) {
    uint64_t value = system_read_SCTLR_EL1();
    printk(KERN_INFO "Read value <0x6llx> from SCTLR_EL1.", value);
    seq_printf(m,"0x6llx\n",value);
    return 0;
}

static ssize_t my_proc_write(struct file* file,const char __user *buffer,size_t count,loff_t *f_pos){
    int rc = -1;
    uint64_t value = 0;

    char *tmp = kzalloc((count 1),GFP_KERNEL);
    if(!tmp)return -ENOMEM;
    if(copy_from_user(tmp,buffer,count)){
        kfree(tmp);
        return EFAULT;
    }

    rc = kstrtoul (tmp, 0, (unsigned long*) &value);
    if (rc != 0) {
       printk(KERN_ERR "Value <%s> is invalid.", tmp);
       kfree(tmp);
       return EFAULT;
    }

    kfree(tmp);
    system_write_SCTLR_EL1(value);
    printk(KERN_INFO "Wrote value <0x6llx> to SCTLR_EL1.", value);

    return count;
}

static int my_proc_open(struct inode *inode,struct file *file){
    return single_open(file,my_proc_show,NULL);
}

static struct file_operations my_fops={
    .owner = THIS_MODULE,
    .open = my_proc_open,
    .release = single_release,
    .read = seq_read,
    .llseek = seq_lseek,
    .write = my_proc_write
};

static int __init sctlr_el1_init(void) {
    struct proc_dir_entry *entry;
    entry = proc_create("SCTLR_EL1",0777,NULL,&my_fops);
    if(!entry){
        return -1;  
    } else {
        printk(KERN_INFO "Entering sctlr_el1\n");
    }

 return 0;
}

static void __exit sctlr_el1_exit(void) {
    remove_proc_entry("SCTLR_EL1",NULL);
    printk(KERN_INFO "Exiting sctlr_el1\n");
}

module_init(sctlr_el1_init);
module_exit(sctlr_el1_exit);

Installing:

make install
insmod sctlr_el1.ko

dmesg
[  927.217571] sctlr_el1: loading out-of-tree module taints kernel.
[  927.218556] Entering sctlr_el1

Reading SCTLR_EL1

cat /proc/SCTLR_EL1
0x0000000034d4d91d

SCTLR_EL1.UCI seems to be set on my system.

Re-writing the same value:

echo 0x0000000034d4d91d > /proc/SCTLR_EL1
dmesg
[ 1130.967798] Wrote value <0x0000000034d4d91d> to SCTLR_EL1.

There is no effect. Writing 0x0 hangs the system, which means the SCTLR_EL1 was probably updated, but the update obviously caused a(n expected) system crash:

echo 0x0 > /proc/SCTLR_EL1

CodePudding user response:

The SCTLR_EL1.UCI bit is set by Linux by default (source) for all arm64 processors except a few Cortex-A53 revisions that need workarounds for errata (source, source). However, even in the case where UCI is not set, a trap handler in the kernel executes them in kernel-mode for you, so they still work, but are just slower.

In conclusion, you can expect the DC CVAU, DC CIVAC, DC CVAC, DC CVAP, and IC IVAU cache maintenance instructions to always be functional in userspace, at least on recent versions of Linux.

  • Related