kasumi research

Project

ia2

hardware-enforced compartments inside one C process

A stray read or write in one library cannot reach another's memory.

IA2 splits a C or C++ program into compartments where the build already drew boundaries, at its shared libraries, and has the CPU enforce them: memory protection keys on x86-64, memory tagging on 64-bit Arm. One process, one address space, no messages.

One processtests/two_keys_minimal · 64-bit Arm
compartment 1main.c x180x0100000000000000
call gate__wrap_start_plugin save · switch stacks · scrub · movz x18 · call
compartment 2plugin.c x180x0200000000000000
1 = 12 ≠ 1
  • globalssecret
  • thread-local
  • stacks
  • heap
compartment 1's memorytag 1
no tagno tag
  • IA2_SHARED_DATAclean_exit
  • code, read-only data
  • the C library
shareduntagged
2 = 21 ≠ 2
  • globalsplugin_secret
  • thread-local
  • stacks
  • heap
compartment 2's memorytag 2
16 bytes and the tag they carry

Every load and store IA2's compiler emits carries x18's tag in the top byte of its pointer, and the processor compares that tag with the tag on the 16 bytes it reaches. Equal, the access goes ahead; different, it faults at that instruction. Shared memory carries no tag and is never checked.

What the CPU checks

x86-64memory protection keys
memory is marked by
a 4-bit key in each page table entry
the running compartment is held in
PKRU, a per-thread register with two bits per key
a gate switches compartments with
WRPKRU, one unprivileged instruction
a denied access raises
SIGSEGV, with si_code SEGV_PKUERR
compartments
15: keys 1 to 15; key 0 is shared
compiler
GCC or Clang, unmodified
64-bit Armthe Memory Tagging Extension
memory is marked by
a 4-bit tag on each 16-byte granule
the running compartment is held in
register x18, reserved by the compiler
a gate switches compartments with
movz x18
a denied access raises
SIGSEGV, with si_code SEGV_MTESERR
compartments
15: tags 1 to 15; untagged memory is shared
compiler
a modified Clang, for every library in the process

On x86-64 the permission belongs to the thread, so even code IA2 never compiled is held to the running compartment’s key. On Arm it travels in the pointer, and code IA2 did not compile uses whatever tag it was handed.

Using it

IA2 adds a step to a C build. Its rewriter, a tool built on Clang, finds every call that crosses a boundary and writes a gate for it, into a directory of its own; the original sources are left alone. IA2’s CMake functions do the whole build: its video player example is two calls to one of them.

Annotations
Only where no tool can infer them: data that is shared, function pointers handed to code IA2 does not rewrite, and functions that run in their caller’s compartment.
A gate at every crossing
About 200 instructions for a round trip on Arm, against two for a plain call and return. The cost in real applications is not yet measured.
On Arm, a toolchain
A modified Clang, and every library in the process rebuilt with it, the C library included. Each load and store carries two or three extra instructions.
examples/video_player/CMakeLists.txt
add_ia2_compartment(video_decoder LIBRARY
  SOURCES video_decoder.c
  PKEY 2
  INCLUDE_DIRECTORIES include
)
add_ia2_compartment(video_player EXECUTABLE
  SOURCES video_player.c
  LIBRARIES video_decoder ${SDL2_LIBRARIES} ${FFMPEG_LIBRARIES}
  PKEY 1
)
abridged.

What it does not protect against

IA2 gives coarse spatial memory safety: a stray read or write in one compartment cannot reach another’s private stack, heap, globals or thread-local storage, in either direction. That is narrower than memory safety.

  • Bugs inside a compartment

    IA2 bounds what a bug reaches. It does not prevent the bug.

  • Shared and read-only data

    Both are reachable from every compartment. In the nginx demo, about two thousand function pointers that nginx core calls sit in shared memory.

  • Control flow

    Code that takes over control flow can jump straight to a gate’s WRPKRU or movz x18. The design relies on control-flow integrity to close that gap.

  • Variadic calls

    A gate is generated per function, and a variadic function’s arguments differ at every call, so a variadic call across a boundary runs ungated, in the caller’s compartment. The rewriter reports each such function, and the private data it could reach.

  • Forged tags, on Arm

    Code that does not clamp pointers, the kernel and hand-written assembly among it, uses whatever tag a pointer carries.

  • Memory system calls, on Arm

    On x86-64 an optional tracer checks every call that maps, unmaps or protects memory against which compartment owns it. Arm has none yet.

Where it stands

nginx demo
Runs with enforcement on: HTTP served from compartment 1, a stream published over RTMP, HLS and DASH written from compartment 2, and the playlist served back and decoded.
ahead
Closing the gaps above, and measuring what a boundary costs.

Updates

IA2: Hardware-Enforced Compartments Inside One C Process

why one buggy library can compromise a whole C program, how IA2 splits a process into compartments along its shared libraries, and how two CPU features already in shipping hardware, x86 protection keys and Arm memory tagging, enforce the split.

← Home

Questions about IA2?

The writeup has the design in full. For what it does not answer, write to us.

hello@kasumi.org