Tracking indirect control transfers on RISC-V Linux

This document briefly describes the interface provided to userspace by Linux to enable indirect branch tracking 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 can result in a variety of security issues.

Some of those security issues can be code re-use attacks, where an adversary can use corrupt function pointers, chaining them together to perform jump oriented programming (JOP) or call oriented programming (COP) and thus compromise control flow integrity (CFI) of the program.

Function pointers live in read-write memory and thus are susceptible to corruption. This can allow an adversary to control the program counter (PC) value. On RISC-V, the zicfilp extension enforces a restriction on such indirect control transfers:

  • Indirect control transfers must land on a landing pad instruction lpad. There are two exceptions to this rule:

    • rs1 = x1 or rs1 = x5, i.e. a return from a function and returns are protected using shadow stack (see Shadow stack to protect function returns on RISC-V Linux)

    • rs1 = x7. On RISC-V, the compiler usually does the following to reach a function which is beyond the offset of possible J-type instruction:

      auipc x7, <imm>
      jalr (x7)
      

      This form of indirect control transfer is immutable and doesn’t rely on memory. Thus rs1=x7 is exempted from tracking and these are considered software guarded jumps.

The lpad instruction is a pseudo-op of auipc rd, <imm_20bit> with rd=x0. This is a HINT op. The lpad instruction must be aligned on a 4 byte boundary. It compares the 20 bit immediate with x7. If imm_20bit == 0, the CPU doesn’t perform any comparison with x7. If imm_20bit != 0, then imm_20bit must match x7 else CPU will raise software check exception (cause=18) with *tval = 2.

The compiler can generate a hash over function signatures and set them up (truncated to 20 bits) in x7 at callsites. Function prologues can have lpad instructions encoded with the same function hash. This further reduces the number of valid program counter addresses a call site can reach.

2. ELF and psABI

The toolchain sets up GNU_PROPERTY_RISCV_FEATURE_1_FCFI for property GNU_PROPERTY_RISCV_FEATURE_1_AND in the notes section of the object file.

3. Linux enabling

User space programs can have multiple shared objects loaded in their address spaces. It’s a difficult task to make sure all the dependencies have been compiled with indirect branch support. Thus it’s left to the dynamic loader to enable indirect branch tracking for the program.

4. prctl() enabling

Per-task indirect branch tracking state can be monitored and controlled via the PR_GET_CFI and PR_SET_CFI ``prctl()` arguments (respectively), by supplying PR_CFI_BRANCH_LANDING_PADS as the second argument. These are architecture-agnostic, and will return -EINVAL if the underlying functionality is not supported.

  • prctl(PR_SET_CFI, PR_CFI_BRANCH_LANDING_PADS, unsigned long arg)

arg is a bitmask.

If PR_CFI_ENABLE is set in arg, and the CPU supports zicfilp, then the kernel will enable indirect branch tracking for the task. The dynamic loader can issue this prctl() once it has determined that all the objects loaded in the address space support indirect branch tracking.

Indirect branch tracking state can also be locked once enabled. This prevents the task from subsequently disabling it. This is done by setting the bit PR_CFI_LOCK in arg. Either indirect branch tracking must already be enabled for the task, or the bit PR_CFI_ENABLE must also be set in arg. This is intended for environments that wish to run with a strict security posture that do not wish to load objects without zicfilp support.

Indirect branch tracking can also be disabled for the task, assuming that it has not previously been enabled and locked. If there is a dlopen() to an object which wasn’t compiled with zicfilp, the dynamic loader can issue this prctl() with arg set to PR_CFI_DISABLE. Disabling indirect branch tracking for the task is not possible if it has previously been enabled and locked.

  • prctl(PR_GET_CFI, PR_CFI_BRANCH_LANDING_PADS, unsigned long * arg)

Returns the current status of indirect branch tracking into a bitmask stored into the memory location pointed to by arg. The bitmask will have the PR_CFI_ENABLE bit set if indirect branch tracking is currently enabled for the task, and if it is locked, will additionally have the PR_CFI_LOCK bit set. If indirect branch tracking is currently disabled for the task, the PR_CFI_DISABLE bit will be set.