Shadow stack to protect function returns on RISC-V Linux

This document briefly describes the interface provided to userspace by Linux to enable shadow stack for user mode applications on RISC-V

1. Feature Overview

Memory corruption issues usually result into crashes, however when in hands of an adversary and if used creatively can result into a variety security issues.

One of those security issues can be code re-use attacks on program where adversary can use corrupt return addresses present on stack and chain them together to perform return oriented programming (ROP) and thus compromising control flow integrity (CFI) of the program.

Return addresses live on stack and thus in read-write memory and thus are susceptible to corruption and which allows an adversary to reach any program counter (PC) in address space. On RISC-V zicfiss extension provides an alternate stack termed as shadow stack on which return addresses can be safely placed in prolog of the function and retrieved in epilog. zicfiss extension makes following changes:

  • PTE encodings for shadow stack virtual memory An earlier reserved encoding in first stage translation i.e. PTE.R=0, PTE.W=1, PTE.X=0 becomes PTE encoding for shadow stack pages.

  • sspush x1/x5 instruction pushes (stores) x1/x5 to shadow stack.

  • sspopchk x1/x5 instruction pops (loads) from shadow stack and compares with x1/x5 and if un-equal, CPU raises software check exception with *tval = 3

Compiler toolchain makes sure that function prologue have sspush x1/x5 to save return address on shadow stack in addition to regular stack. Similarly function epilogs have ld x5, offset(x2) followed by sspopchk x5 to ensure that popped value from regular stack matches with popped value from shadow stack.

2. Shadow stack protections and linux memory manager

As mentioned earlier, shadow stacks get new page table encodings and thus have some special properties assigned to them and instructions that operate on them as below:

  • Regular stores to shadow stack memory raises access store faults. This way shadow stack memory is protected from stray inadvertent writes.

  • Regular loads to shadow stack memory are allowed. This allows stack trace utilities or backtrace functions to read true callstack (not tampered).

  • Only shadow stack instructions can generate shadow stack load or shadow stack store.

  • Shadow stack load / shadow stack store on read-only memory raises AMO/store page fault. Thus both sspush x1/x5 and sspopchk x1/x5 will raise AMO/ store page fault. This simplies COW handling in kernel during fork, kernel can convert shadow stack pages into read-only memory (as it does for regular read-write memory) and as soon as subsequent sspush or sspopchk in userspace is encountered, then kernel can perform COW.

  • Shadow stack load / shadow stack store on read-write, read-write-execute memory raises an access fault. This is a fatal condition because shadow stack should never be operating on read-write, read-write-execute memory.

3. ELF and psABI

Toolchain sets up GNU_PROPERTY_RISCV_FEATURE_1_BCFI for property GNU_PROPERTY_RISCV_FEATURE_1_AND in notes section of the object file.

4. Linux enabling

User space programs can have multiple shared objects loaded in its address space and it’s a difficult task to make sure all the dependencies have been compiled with support of shadow stack. Thus it’s left to dynamic loader to enable shadow stack for the program.

5. prctl() enabling

PR_SET_SHADOW_STACK_STATUS / PR_GET_SHADOW_STACK_STATUS / PR_LOCK_SHADOW_STACK_STATUS are three prctls added to manage shadow stack enabling for tasks. prctls are arch agnostic and returns -EINVAL on other arches.

  • prctl(PR_SET_SHADOW_STACK_STATUS, unsigned long arg)

If arg1 PR_SHADOW_STACK_ENABLE and if CPU supports zicfiss then kernel will enable shadow stack for the task. Dynamic loader can issue this prctl once it has determined that all the objects loaded in address space have support for shadow stack. Additionally if there is a dlopen to an object which wasn’t compiled with zicfiss, dynamic loader can issue this prctl with arg1 set to 0 (i.e. PR_SHADOW_STACK_ENABLE being clear)

  • prctl(PR_GET_SHADOW_STACK_STATUS, unsigned long *arg)

Returns current status of indirect branch tracking. If enabled it’ll return PR_SHADOW_STACK_ENABLE.

  • prctl(PR_LOCK_SHADOW_STACK_STATUS, unsigned long arg)

Locks current status of shadow stack enabling on the task. User space may want to run with strict security posture and wouldn’t want loading of objects without zicfiss support in it and thus would want to disallow disabling of shadow stack on current task. In that case user space can use this prctl to lock current settings.

6. Shadow stack tokens

Regular stores on shadow stacks are not allowed and thus can’t be tampered with via arbitrary stray writes due to bugs. However method of pivoting / switching to shadow stack is simply writing to csr CSR_SSP and that will change active shadow stack for the program. Instances of writes to CSR_SSP in the address space of the program should be mostly limited to context switching, stack unwind, longjmp or similar mechanisms (like context switching of green threads) in languages like go, rust. This can be problematic because an attacker can use memory corruption bugs and eventually use such context switching routines to pivot to any shadow stack. Shadow stack tokens can help mitigate this problem by making sure that:

  • When software is switching away from a shadow stack, shadow stack pointer should be saved on shadow stack itself and call it shadow stack token

  • When software is switching to a shadow stack, it should read the shadow stack token from shadow stack pointer and verify that shadow stack token itself is pointer to shadow stack itself.

  • Once the token verification is done, software can perform the write to CSR_SSP to switch shadow stack.

Here software can be user mode task runtime itself which is managing various contexts as part of single thread. Software can be kernel as well when kernel has to deliver a signal to user task and must save shadow stack pointer. Kernel can perform similar procedure by saving a token on user shadow stack itself. This way whenever sigreturn happens, kernel can read the token and verify the token and then switch to shadow stack. Using this mechanism, kernel helps user task so that any corruption issue in user task is not exploited by adversary by arbitrarily using sigreturn. Adversary will have to make sure that there is a shadow stack token in addition to invoking sigreturn

7. Signal shadow stack

Following structure has been added to sigcontext for RISC-V:

struct __sc_riscv_cfi_state {
    unsigned long ss_ptr;
};

As part of signal delivery, shadow stack token is saved on current shadow stack itself and updated pointer is saved away in ss_ptr field in __sc_riscv_cfi_state under sigcontext. Existing shadow stack allocation is used for signal delivery. During sigreturn, kernel will obtain ss_ptr from sigcontext and verify the saved token on shadow stack itself and switch shadow stack.