Userlevel software compartmentalization (experimental)

CheriBSD implements two different CHERI-enabled software compartmentalization models:

  • Colocated processes (co-processes) compartmentalization accelerates UNIX Inter-Process Communication (IPC) and context switching by colocating multiple processes in the same address space, separating them using CHERI capabilities. Support for co-processes is maintained in an experimental development branch and is not included in current software releases. For more information see the Colocation Tutorial wiki page.
  • Dynamic-linker-based compartmentalization isolates shared libraries within a process using CHERI capabilities limiting the access of attackers who have achieved arbitrary code execution within a library. The linker-based library compartmentalization model has been included since the 22.12 release of CheriBSD. See the c18n(3) man page on an installed system for more information.

Library compartmentalization

CheriBSD's library compartmentalization feature (c18n) executes each dynamic library within a compartmentalization-enabled process in its own protection domain. When c18n is enabled, the run-time linker grants libraries capabilities only to resources (global variables, APIs) declared in their ELF linkage. Function calls that cross domain boundaries are interposed on by domain-crossing shims implemented by the run-time linker.

The adversary model for these compartments is one of trusted code but untrustworthy execution: a library such as libpng or libjpeg is trusted until it begins dynamic execution -- and has potentially been exposed to malicious data. With library compartmentalization, an adversary who achieves arbitrary code execution within the library at run time will be able to reach only the resources (and further attack surfaces) declared statically through its linkage. The programmer must then harden that linkage, and any involved APIs, to make them suitable for adversarial engagement -- but the foundation of isolation, controlled access, and controlled domain transition is provided by the c18n implementation.

In addition to a modified run-time linker, modest changes have been made to the aarch64c calling convention to avoid assumptions such as implicit stack sharing between callers and callees across library boundaries when passing variadic argument lists. This modified ABI is now used by all CheriABI binaries in CheriBSD, and so off-the-shelf aarch64c binaries and libraries can be used with library compartmentalization without recompilation to the modified ABI. More information on library compartmentalization can be found in the c18n(3) man page:

man c18n

Enabling library compartmentalization system-wide

Compartmentalization can be enabled for all eligible new processes with sysctl:

sysctl security.cheri.lib_based_c18n_default=1

Enabling library compartmentalization temporarily for a particular binary

To run a particular binary with library compartmentalization enabled just for this run, use proccontrol:

proccontrol -m cheric18n -s enable ./helloworld

Enabling library compartmentalization permanently for a particular binary

To enable library compartmentalization permanently for a particular binary, run the following command to set a flag in the binary:

elfctl -e +cheric18n ./helloworld

This can be reversed by replacing +cheric18n with -cheric18n.

You can confirm whether c18n is permanently enabled for a binary by inspecting it using the elfctl command:

elfctl ./helloworld

The output will contain two lines like the following indicating whether c18n is enabled or disabled.

cheric18n       'Force Enable CHERI library-based compartmentalisation' is set.
nocheric18n     'Force Disable CHERI library-based compartmentalisation' is unset.

Note that nocheric18n disables c18n for the binary, overriding the system-wide default.

Tracing compartment-boundary crossings

The BSD ktrace(1) command is able to trace compartment-boundary crossings. To enable this feature, set the LD_UTRACE_COMPARTMENT environmental variable, which will cause the run-time linker to emit records using the utrace(2) system call. Run the program under ktrace with the -tu argument to capture only those records (and not a full system-call trace):

env LD_UTRACE_COMPARTMENT=1 ktrace -tu ./helloworld

The resulting ktrace.out file can be viewed using the kdump(1) command:

kdump

It is important to understand, however, that simply isolating running code is almost always insufficient to achieve robust sandboxing. The competent adversary will now consider further rights and attack surfaces to explore in search of further vulnerabilities. While this increased work factor of finding additional vulnerabilities is an important part of compartmentalization, internal software APIs are rarely well suited to be security boundaries without performing additional hardening. With this in mind, you can:

  • Inspect the source code, output from objdump, and output from chericat to assess the robustness of this compartmentalization. You can install objdump with pkg64 install llvm-base and chericat with pkg64c install chericat.
  • Consider larger software architectural changes that will allow a library to be used more robustly when running within a computerment.

Library compartmentalization has the potential to significantly improve software integrity and confidentiality properties in the presence of a strong adversary. However, it is also limited by the abstraction being around the current library operational model.