138 lines
5.7 KiB
ReStructuredText
138 lines
5.7 KiB
ReStructuredText
.. SPDX-License-Identifier: GPL-2.0
|
|
|
|
:Author: Deepak Gupta <debug@rivosinc.com>
|
|
:Date: 12 January 2024
|
|
|
|
====================================================
|
|
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 zicfiss.rst)
|
|
|
|
- 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 :c:macro:`GNU_PROPERTY_RISCV_FEATURE_1_FCFI` for
|
|
property :c:macro:`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 :c:macro:`PR_GET_CFI` and :c:macro:`PR_SET_CFI`
|
|
``prctl()` arguments (respectively), by supplying
|
|
:c:macro:`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(:c:macro:`PR_SET_CFI`, :c:macro:`PR_CFI_BRANCH_LANDING_PADS`, unsigned long arg)
|
|
|
|
arg is a bitmask.
|
|
|
|
If :c:macro:`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 :c:macro:`PR_CFI_LOCK` in arg. Either indirect branch
|
|
tracking must already be enabled for the task, or the bit
|
|
:c:macro:`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
|
|
:c:macro:`PR_CFI_DISABLE`. Disabling indirect branch tracking for the
|
|
task is not possible if it has previously been enabled and locked.
|
|
|
|
|
|
* prctl(:c:macro:`PR_GET_CFI`, :c:macro:`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 :c:macro:`PR_CFI_ENABLE` bit set if indirect branch tracking
|
|
is currently enabled for the task, and if it is locked, will
|
|
additionally have the :c:macro:`PR_CFI_LOCK` bit set. If indirect
|
|
branch tracking is currently disabled for the task, the
|
|
:c:macro:`PR_CFI_DISABLE` bit will be set.
|
|
|
|
|
|
5. violations related to indirect branch tracking
|
|
--------------------------------------------------
|
|
|
|
Pertaining to indirect branch tracking, the CPU raises a software
|
|
check exception in the following conditions:
|
|
|
|
- missing ``lpad`` after indirect call / jmp
|
|
- ``lpad`` not on 4 byte boundary
|
|
- ``imm_20bit`` embedded in ``lpad`` instruction doesn't match with ``x7``
|
|
|
|
In all 3 cases, ``*tval = 2`` is captured and software check exception is
|
|
raised (``cause=18``).
|
|
|
|
The kernel will treat this as :c:macro:`SIGSEGV` with code =
|
|
:c:macro:`SEGV_CPERR` and follow the normal course of signal delivery.
|