GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing llvm.org — Lightweight Fault Isolation (LFI) in LLVM - LLVM Lightweight Fault Isolation (LFI) in LLVM#Introduction#Lightweight Fault Isolation (LFI) is a compiler-based sandboxing technology for native code. Like WebAssembly and Native Client, LFI isolates sandboxed code in-process (i.e., in the same address space as a host application). LFI is designed from the ground up to sandbox existing code, such as C/C++ libraries (including assembly code) and device drivers. LFI aims for the following goals:
When building a program for the LFI target the compiler is designed to ensure that the program will only be able to access memory within a limited region of the virtual address space, starting from where the program is loaded (the current design sets this region to a size of 4GiB of virtual memory). Programs built for the LFI target are restricted to using a subset of the instruction set, designed so that the programs can be soundly confined to their sandbox region. LFI programs must run inside of an “emulator” (usually called the LFI runtime), responsible for initializing the sandbox region, loading the program, and servicing system call requests, or other forms of runtime calls. LFI uses an architecture-specific sandboxing scheme based on the general technique of Software-Based Fault Isolation (SFI). LLVM currently supports LFI for the AArch64 and X86-64 platforms. The AArch64 version is designed to support the Armv8.1 AArch64 architecture. See https://github.com/lfi-project for details about the LFI project and additional software needed to run LFI programs. Compiler Requirements#When building for an LFI target ( These rewrites (also called “expansions”) are applied at the very end of the LLVM compilation pipeline (during the assembler step). This allows the rewrites to be applied to hand-written assembly, including inline assembly. Context Register#Both architectures designate a context register that points to a block of
thread-local memory managed by the LFI runtime. The context register is
Linker Support#In the initial version, LFI only supports static linking, and only supports
creating Assembler Directives#The following directives are supported for controlling the rewriter.
|
Original |
Rewritten |
|---|---|
{br,blr,ret} xN |
add x28, x27, wN, uxtw {br,blr,ret} x28 |
ret
|
ret
|
Memory accesses are rewritten to use the [x27, wM, uxtw] addressing mode if
it is available, which is automatically safe. Otherwise, rewrites fall back to
using x28 along with an instruction to safely load it with the target
address.
Original |
Rewritten |
|---|---|
LDSTr xN, [xM] |
LDSTr xN, [x27, wM, uxtw] |
LDSTr xN, [xM, #I] |
add x28, x27, wM, uxtw LDSTr xN, [x28, #I] |
LDSTr xN, [xM, #I]! |
add xM, xM, #I LDSTr xN, [x27, wM, uxtw] |
LDSTr xN, [xM], #I |
LDSTr xN, [x27, wM, uxtw] add xM, xM, #I |
LDSTr xN, [xM1, xM2] |
add x26, xM1, xM2 LDSTr xN, [x27, w26, uxtw] |
LDSTr xN, [xM1, xM2, MOD #I] |
add x26, xM1, xM2, MOD #I LDSTr xN, [x27, w26, uxtw] |
LDSTx ..., [xM] |
add x28, x27, wM, uxtw LDSTx ..., [x28] |
LDSTx ..., [xM, #I] |
add x28, x27, wM, uxtw LDSTx ..., [x28, #I] |
LDSTx ..., [xM, #I]! |
add x28, x27, wM, uxtw LDSTx ..., [x28, #I] add xM, xM, #I |
LDSTx ..., [xM], #I |
add x28, x27, wM, uxtw LDSTx ..., [x28] add xM, xM, #I |
LDSTx ..., [xM1], xM2 |
add x28, x27, wM1, uxtw LDSTx ..., [x28] add xM1, xM1, xM2 |
When the stack pointer is modified, we write the modified value to a temporary,
before moving it back into sp with a safe add.
Original |
Rewritten |
|---|---|
mov sp, xN |
add sp, x27, wN, uxtw |
{add,sub} sp, sp, {#I,xN} |
{add,sub} x26, sp, {#I,xN} add sp, x27, w26, uxtw |
When the link register is modified, it is guarded back into the sandbox with a
safe add x30, x27, w30, uxtw. This guard is deferred until the next
control-flow instruction rather than emitted immediately after the
modification. Deferral keeps a signed return address intact so that a following
authentication instruction (such as autiasp) can run before the guard,
which would otherwise destroy the pointer authentication signature. See
Pointer Authentication Code (PAC) support.
Original |
Rewritten |
|---|---|
ldr x30, [...] ret |
ldr x30, [...] add x30, x27, w30, uxtw ret |
ldp xN, x30, [...] ret |
ldp xN, x30, [...] add x30, x27, w30, uxtw ret |
LFI is compatible with Arm Pointer Authentication Code (PAC) instructions,
which are used to sign and authenticate x30 to protect against control-flow
hijacking.
The typical use is -mbranch-protection=pac-ret, which signs only the return
address in x30 using the hint-space paciasp and autiasp
instructions. The combined authenticate-and-branch and authenticate-and-return
instructions covered below require Armv8.3-a and are not produced by
-mbranch-protection. They can appear in hand-written assembly or from
environments that sign all code pointers, so the rewriter still sandboxes them
rather than passing them through unmodified.
To gain the security benefit of PAC under LFI, the hardware must implement
FEAT_FPAC, so that authentication failures fault immediately. Without
FEAT_FPAC, a failed authentication produces a poisoned pointer, which LFI
still keeps confined to the sandbox by masking it, but the mask overwrites the
poison caused by the authentication failure.
Original |
Rewritten |
|---|---|
paciasp
|
paciasp
|
autiasp ret |
autiasp add x30, x27, w30, uxtw ret |
Authenticated returns (retaa/retab) combine authentication with return,
and must be expanded during rewriting.
Original |
Rewritten |
|---|---|
retaa
|
autiasp add x30, x27, w30, uxtw ret |
retab
|
autibsp add x30, x27, w30, uxtw ret |
Authenticated branches (braa/brab/braaz/brabz) and calls
(blraa/blrab/blraaz/blrabz) combine authentication with an
indirect branch or call. They are expanded by first authenticating the target
register in place, then performing a normal sandboxed branch or call.
Original |
Rewritten |
|---|---|
braa xN, xM |
autia xN, xM add x28, x27, wN, uxtw br x28 |
braaz xN |
autiza xN add x28, x27, wN, uxtw br x28 |
blraa xN, xM |
autia xN, xM add x28, x27, wN, uxtw blr x28 |
Authenticated exception returns (eret/eretaa/eretab) are privileged
and are not supported: the rewriter reports an error for them.
System calls are rewritten into a sequence that loads the address of the first
runtime call entrypoint and jumps to it. The runtime call entrypoint table is
stored at a negative offset from the sandbox base, so it can be referenced by
x27. The rewrite also saves and restores the link register, since it is
used for branching into the runtime.
Original |
Rewritten |
|---|---|
svc #0 |
mov x26, x30 ldur x30, [x27, #-8] blr x30 add x30, x27, w26, uxtw |
TP accesses are rewritten into loads/stores from the context register
(x25), which holds the virtual thread pointer at offset 16 (see
Context Register).
Original |
Rewritten |
|---|---|
|
|
|
|
If a register is guarded multiple times in the same basic block without any modifications to it during the intervening instructions, then subsequent guards can be removed.
Original |
Rewritten |
|---|---|
add x28, x27, wN, uxtw ldur xN, [x28] add x28, x27, wN, uxtw ldur xN, [x28, #8] add x28, x27, wN, uxtw ldur xN, [x28, #16] |
add x28, x27, wN, uxtw ldur xN, [x28] ldur xN, [x28, #8] ldur xN, [x28, #16] |
Note: not yet implemented.
Addresses to global symbols in position-independent executables are frequently
generated via adrp followed by ldr. Since the address generated by
adrp can be statically guaranteed to be within the sandbox, it is safe to
directly target x28 for these sequences. This allows the omission of a
guard instruction before the ldr.
Original |
Rewritten |
|---|---|
adrp xN, target ldr xN, [xN, imm] |
adrp x28, target ldr xN, [x28, imm] |
Note: not yet implemented.
If the stack pointer is modified by adding/subtracting a small immediate, and then later used to perform a memory access without any intervening jumps, then the guard on the stack pointer modification can be removed. This is because the load/store is guaranteed to trap if the stack pointer has been moved outside of the sandbox region.
Original |
Rewritten |
|---|---|
add x26, sp, #8 add sp, x27, w26, uxtw ... (same basic block) ldr xN, [sp] |
add sp, sp, #8 ... (same basic block) ldr xN, [sp] |
Note: not yet implemented.
In certain cases, guards may be hoisted outside of loops.
Original |
Rewritten |
|---|---|
mov w8, #10 mov w9, #0 .loop: add w9, w9, #1 ldr xN, [xM] cmp w9, w8 b.lt .loop .end: |
mov w8, #10 mov w9, #0 add x28, x27, wM, uxtw .loop: add w9, w9, #1 ldr xN, [x28] cmp w9, w8 b.lt .loop .end: |
The X86-64 LFI target is x86_64_lfi.
The X86-64 LFI target reserves the following registers:
r14: always holds the sandbox base address. Also used as the runtime call
table pointer (the runtime call table is stored at the sandbox base).
gs: always holds the sandbox base address (used as a segment register for
memory access sandboxing).
rsp: always holds an address within the sandbox.
r15: context register (see Context Register).
r11: scratch register.
The X86-64 LFI target confines control flow using 32-byte bundles. Indirect branch targets are masked to a 32-byte boundary, so control can only enter code at a bundle-aligned address. For this to constrain the instruction stream, no instruction may span a bundle boundary.
LFI object files are therefore assembled with aligned instruction bundling enabled, which inserts nop padding wherever an
instruction would otherwise cross a boundary. Bundling is enabled implicitly
for the x86_64_lfi target.
A rewrite that expands one instruction into a sequence which must not be
entered in the middle wraps that sequence in .bundle_lock / .bundle_unlock.
This keeps the whole group within a single bundle, so no masked indirect branch
can land between its instructions.
Calls are emitted in a .bundle_lock align_to_end group, which places the call
at the end of its bundle. This makes the return address pushed by the call
bundle-aligned, so that a masked ret returns to the instruction following the
call.
To make sure that indirect branch targets remain reachable after masking, the compiler aligns them to a bundle boundary. The targets of direct branches do not need to be aligned, since they are resolved statically.
Bundling is specific to X86-64. AArch64 instructions are fixed-width and naturally aligned, and the AArch64 LFI target confines indirect branches by guarding the target register instead.
In the following assembly rewrites, some shorthand is used.
%rN or %eN: refers to any general-purpose non-reserved register.
%vN: refers to any vector register.
{a,b,c}: matches any of a, b, or c.
N(...): refers to any memory addressing mode.
N: refers to any displacement, including none.
S: refers to any scale.
Op: refers to any instruction with a memory operand.
MOD: refers to any instruction that writes the stack pointer.
x?: matches an optional x.
Indirect jumps are rewritten to first apply a mask that zeroes the top 32 bits
and bottom 5 bits of the target. An addq instruction is then used to fill
in the top 32 bits with the sandbox base, producing an address that is both
inside the sandbox and bundle-aligned.
Indirect branches through memory first load the branch target into the scratch
register (%r11), and then dispatch through it. That load is itself sandboxed
by the memory access rewrite below.
Returns are rewritten to pop the return address into the scratch register, followed by a sandboxed indirect jump.
Direct jumps and direct calls do not need to be rewritten, since their targets are resolved at link time. Direct calls are placed at the end of a bundle.
Original |
Rewritten |
|---|---|
jmpq *%rX |
andl $-32, %eX addq %r14, %rX jmpq *%rX |
jmpq *N(%rX) |
movq %gs:N(%eX), %r11 andl $-32, %r11d addq %r14, %r11 jmpq *%r11 |
callq *%rX |
andl $-32, %eX addq %r14, %rX callq *%rX |
callq *N(%rX) |
movq %gs:N(%eX), %r11 andl $-32, %r11d addq %r14, %r11 callq *%r11 |
ret
|
popq %r11 andl $-32, %r11d addq %r14, %r11 jmpq *%r11 |
Memory accesses are confined to the sandbox by rewriting the addressing mode.
The address is computed from 32-bit registers, so that it wraps within the
4GiB sandbox rather than escaping it, and a %gs segment override supplies the
sandbox base.
An access based on a register that always holds an address inside the sandbox
is already safe, and is left alone. Those registers are %rsp, %rip, and
%r14.
Every addressing mode is rewritten, except those that encode an address without
dereferencing it, such as lea and multi-byte nops.
Only the addressing mode changes, so the rewrites are listed as addressing modes rather than whole instructions.
Original |
Rewritten |
|---|---|
N(%rX) |
%gs:N(%eX) |
N(%rX, %rY, S) |
%gs:N(%eX, %eY, S) |
N(, %rY, S) |
%gs:N(, %eY, S) |
N
|
N(%r14) |
N({%rsp,%rip,%r14}) |
N({%rsp,%rip,%r14}) |
leaq N(...), %rX |
leaq N(...), %rX |
String instructions also address memory implicitly, but they can be rewritten and are covered below.
Gather and scatter index memory with a vector register, which holds a separate
offset for every element rather than part of one address. The index therefore
keeps its width and only the base is truncated, since the 32-bit address size
makes each element’s address wrap within the sandbox just as a scalar one does.
A vector index cannot give the access a 32-bit address size on its own, so an
access with no base register gets %r14d, whose value is zero because the
sandbox base is 4GiB-aligned.
Original |
Rewritten |
|---|---|
N(%rX, %vY, S) |
%gs:N(%eX, %vY, S) |
N(, %vY, S) |
%gs:N(%r14d, %vY, S) |
String instructions address memory through %rdi and %rsi, which cannot be
replaced by an addressing mode, so the registers themselves are guarded before
the access. The guard truncates the pointer to a sandbox offset and adds the
sandbox base back with a lea, which does not disturb the flags.
The guards and the access must be in the same bundle, so that control cannot enter between them.
movs and cmps access memory through both registers, stos and scas
through %rdi alone, and lods through %rsi alone.
Original |
Rewritten |
|---|---|
rep? {stos,scas} |
.bundle_lock movl %edi, %edi leaq (%r14, %rdi), %rdi rep? {stos,scas} .bundle_unlock |
rep? lods |
.bundle_lock movl %esi, %esi leaq (%r14, %rsi), %rsi rep? lods .bundle_unlock |
rep? {movs,cmps} |
.bundle_lock movl %edi, %edi leaq (%r14, %rdi), %rdi movl %esi, %esi leaq (%r14, %rsi), %rsi rep? {movs,cmps} .bundle_unlock |
The stack pointer must always hold an address inside the sandbox, so an
instruction that writes it is demoted to its 32-bit form, which clears the top
32 bits, and the sandbox base is then added back. As above, the lea form of
the addition is used so that the flags are left alone. Both halves must be in
the same bundle, so that control cannot enter with an unsandboxed %rsp.
The rewrite recognizes mov, lea, add, sub, and, or, and xor, and
reports an error for any other instruction that writes the stack pointer.
push and pop move the stack pointer by a fixed amount and perform an
access, which traps if it exits the sandbox, so they are not rewritten.
Original |
Rewritten |
|---|---|
MOD ..., %rsp |
.bundle_lock MOD ..., %esp leaq (%rsp, %r14), %rsp .bundle_unlock |
System calls are rewritten into a sequence that loads the return address into
the scratch register and jumps to the runtime call handler. The runtime call
handler table is stored at the address pointed to by r14. The r11
register stores the return address (marked by the label .Ltmp in the
block below).
Original |
Rewritten |
|---|---|
syscall
|
leaq .Ltmp(%rip), %r11 jmpq *-8(%r14) .Ltmp: |
Thread pointer accesses via the %fs segment (used for TLS) are rewritten to
use the virtual thread pointer from the context register (r15) at offset 16
(see Context Register). The rewrite handles any load or
store instruction with an %fs-segment memory operand.
The thread pointer is an address inside the sandbox like any other, so the
resulting access is sandboxed by the memory access rewrite above. A bare
%fs:0 is the exception: it becomes an access to the context register itself,
which the runtime maintains outside the sandbox, and so is left unsandboxed.
Original |
Rewritten |
|---|---|
Op %fs:0, %rD |
Op 16(%r15), %rD |
Op %fs:(%rX), %rD |
movq 16(%r15), %rD Op %gs:(%eD, %eX), %rD |
Op %rS, %fs:(%rX) |
movq 16(%r15), %r11 Op %rS, %gs:(%r11d, %eX) |
Op %fs:N(%rX, %rY, S), %rD |
movq 16(%r15), %r11 leaq (%r11, %rX), %r11 Op %gs:N(%r11d, %eY, S), %rD |
Op %rS, %fs:N(%rX, %rY, S) |
movq 16(%r15), %r11 leaq (%r11, %rX), %r11 Op %rS, %gs:N(%r11d, %eY, S) |
For more information, please see the following resources:
Contact info:
Zachary Yedidia - zyedidia@cs.stanford.edu
Tal Garfinkel - tgarfinkel@google.com
Sharjeel Khan - sharjeelkhan@google.com