.. SPDX-License-Identifier: GPL-2.0 :Author: Deepak Gupta :Date: 12 January 2024 ========================================================= Shadow stack to protect function returns on RISC-V Linux ========================================================= This document briefly describes the interface provided to userspace by Linux to enable shadow stacks for user mode applications on RISC-V. 1. Feature Overview -------------------- Memory corruption issues usually result in crashes. However, in the hands of a creative adversary, these issues can result in a variety of security problems. Some of those security issues can be code re-use attacks on programs where an adversary can use corrupt return addresses present on the stack. chaining them together to perform return oriented programming (ROP) and thus compromising the control flow integrity (CFI) of the program. Return addresses live on the stack in read-write memory. Therefore they are susceptible to corruption, which allows an adversary to control the program counter. On RISC-V, the ``zicfiss`` extension provides an alternate stack (the "shadow stack") on which return addresses can be safely placed in the prologue of the function and retrieved in the epilogue. The ``zicfiss`` extension makes the 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 the PTE encoding for shadow stack pages. - The ``sspush x1/x5`` instruction pushes (stores) ``x1/x5`` to shadow stack. - The ``sspopchk x1/x5`` instruction pops (loads) from shadow stack and compares with ``x1/x5`` and if not equal, the CPU raises a ``software check exception`` with ``*tval = 3`` The compiler toolchain ensures that function prologues have ``sspush x1/x5`` to save the return address on shadow stack in addition to the regular stack. Similarly, function epilogues have ``ld x5, offset(x2)`` followed by ``sspopchk x5`` to ensure that a popped value from the regular stack matches with the popped value from the shadow stack. 2. Shadow stack protections and linux memory manager ----------------------------------------------------- As mentioned earlier, shadow stacks get new page table encodings that have some special properties assigned to them, along with instructions that operate on the shadow stacks: - Regular stores to shadow stack memory raise store access faults. This protects shadow stack memory from stray writes. - Regular loads from shadow stack memory are allowed. This allows stack trace utilities or backtrace functions to read the true call stack and ensure that it has not been tampered with. - Only shadow stack instructions can generate shadow stack loads or shadow stack stores. - Shadow stack loads and stores on read-only memory raise AMO/store page faults. Thus both ``sspush x1/x5`` and ``sspopchk x1/x5`` will raise AMO/store page fault. This simplies COW handling in kernel during fork(). The kernel can convert shadow stack pages into read-only memory (as it does for regular read-write memory). As soon as subsequent ``sspush`` or ``sspopchk`` instructions in userspace are encountered, the kernel can perform COW. - Shadow stack loads and stores on read-write or read-write-execute memory raise an access fault. This is a fatal condition because shadow stack loads and stores should never be operating on read-write or read-write-execute memory. 3. ELF and psABI ----------------- The toolchain sets up :c:macro:`GNU_PROPERTY_RISCV_FEATURE_1_BCFI` for property :c:macro:`GNU_PROPERTY_RISCV_FEATURE_1_AND` in the notes section of the object file. 4. Linux enabling ------------------ User space programs can have multiple shared objects loaded in their address space. It's a difficult task to make sure all the dependencies have been compiled with shadow stack support. Thus it's left to the dynamic loader to enable shadow stacks for the program. 5. prctl() enabling -------------------- :c:macro:`PR_SET_SHADOW_STACK_STATUS` / :c:macro:`PR_GET_SHADOW_STACK_STATUS` / :c:macro:`PR_LOCK_SHADOW_STACK_STATUS` are three prctls added to manage shadow stack enabling for tasks. These prctls are architecture-agnostic and return -EINVAL if not implemented. * prctl(PR_SET_SHADOW_STACK_STATUS, unsigned long arg) If arg = :c:macro:`PR_SHADOW_STACK_ENABLE` and if CPU supports ``zicfiss`` then the kernel will enable shadow stacks for the task. The dynamic loader can issue this :c:macro:`prctl` once it has determined that all the objects loaded in address space have support for shadow stacks. Additionally, if there is a :c:macro:`dlopen` to an object which wasn't compiled with ``zicfiss``, the dynamic loader can issue this prctl with arg set to 0 (i.e. :c:macro:`PR_SHADOW_STACK_ENABLE` being clear) * prctl(PR_GET_SHADOW_STACK_STATUS, unsigned long * arg) Returns the current status of indirect branch tracking. If enabled it'll return :c:macro:`PR_SHADOW_STACK_ENABLE`. * prctl(PR_LOCK_SHADOW_STACK_STATUS, unsigned long arg) Locks the current status of shadow stack enabling on the task. Userspace may want to run with a strict security posture and wouldn't want loading of objects without ``zicfiss`` support. In this case userspace can use this prctl to disallow disabling of shadow stacks on the current task. 5. violations related to returns with shadow stack enabled ----------------------------------------------------------- Pertaining to shadow stacks, the CPU raises a ``software check exception`` upon executing ``sspopchk x1/x5`` if ``x1/x5`` doesn't match the top of shadow stack. If a mismatch happens, then the CPU sets ``*tval = 3`` and raises the exception. The Linux kernel will treat this as a :c:macro:`SIGSEGV` with code = :c:macro:`SEGV_CPERR` and follow the normal course of signal delivery. 6. Shadow stack tokens ----------------------- Regular stores on shadow stacks are not allowed and thus can't be tampered with via arbitrary stray writes. However, one method of pivoting / switching to a shadow stack is simply writing to the CSR ``CSR_SSP``. This will change the active shadow stack for the program. Writes to ``CSR_SSP`` in the program should be mostly limited to context switches, stack unwinds, or longjmp or similar mechanisms (like context switching of Green Threads) in languages like Go and Rust. CSR_SSP writes can be problematic because an attacker can use memory corruption bugs and leverage 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, the shadow stack pointer should be saved on the shadow stack itself (this is called the ``shadow stack token``). - When software is switching to a shadow stack, it should read the ``shadow stack token`` from the shadow stack pointer and verify that the ``shadow stack token`` itself is a pointer to the shadow stack itself. - Once the token verification is done, software can perform the write to ``CSR_SSP`` to switch shadow stacks. Here "software" could refer to the user mode task runtime itself, managing various contexts as part of a single thread. Or "software" could refer to the kernel, when the kernel has to deliver a signal to a user task and must save the shadow stack pointer. The kernel can perform similar procedure itself by saving a token on the user mode task's shadow stack. This way, whenever :c:macro:`sigreturn` happens, the kernel can read and verify the token and then switch to the shadow stack. Using this mechanism, the kernel helps the user task so that any corruption issue in the user task is not exploited by adversaries arbitrarily using :c:macro:`sigreturn`. Adversaries will have to make sure that there is a valid ``shadow stack token`` in addition to invoking :c:macro:`sigreturn`. 7. Signal shadow stack ----------------------- The following structure has been added to sigcontext for RISC-V:: struct __sc_riscv_cfi_state { unsigned long ss_ptr; }; As part of signal delivery, the shadow stack token is saved on the current shadow stack itself. The updated pointer is saved away in the :c:macro:`ss_ptr` field in :c:macro:`__sc_riscv_cfi_state` under :c:macro:`sigcontext`. The existing shadow stack allocation is used for signal delivery. During :c:macro:`sigreturn`, kernel will obtain :c:macro:`ss_ptr` from :c:macro:`sigcontext`, verify the saved token on the shadow stack, and switch the shadow stack.