Every operating system hard-wires its isolation strategy at design time — monolithic kernels trust all kernel code, microkernels pay for hardware protection everywhere, unikernels drop protection entirely — and revisiting the choice after deployment costs years (Meltdown's KPTI), so FlexOS makes isolation a build-time knob instead, turning a modular library OS into compartments connected by abstract call gates and shared-data annotations that a source-transformation toolchain instantiates as plain calls, Intel MPK domain switches, or per-compartment VMs with RPC — same code, radically different safety — plus a partial-order exploration technique that prunes 80 Redis configurations down to the 5 safest meeting a performance budget.
The argument in one glance
-
Problem
Isolation strategy is the one OS decision nobody gets to revisit
Operating systems commit at design time to a protection philosophy: monolithic kernels (Linux) isolate applications from each other but trust every line of kernel code; microkernels put hardware walls around everything and pay in performance; single-address-space and unikernel designs drop internal protection entirely for speed. Changing the choice later is heroic engineering — retrofitting Meltdown mitigations (KPTI) into Linux, or the multi-year debate about introducing Rust components into a kernel that remains overwhelmingly memory-unsafe.
-
What people did
Pick a point, or bolt on a framework
The design space is populated by fixed points — each OS is one safety/performance trade, chosen for all workloads forever. Compartmentalization frameworks (HODOR, ERIM, CubicleOS) retrofit isolation into existing software, but each is welded to one mechanism (usually Intel MPK) and one strategy.
-
Why it fell short
One size fits nobody, and the hardware keeps changing
Three pressures break the fixed-point world. Applications differ: this paper measures that isolating the scheduler costs Redis 43% of its throughput but Nginx only 6% — the right compartment boundaries depend on each application's communication patterns, so any fixed mapping is wrong for someone. Hardware churns: CPU vendors keep shipping new isolation primitives (MPK, CHERI capabilities, TEEs) that existing OSes cannot adopt without redesign. And protections fail: when Meltdown broke the user/kernel boundary, the fix required invasive kernel surgery and a long vulnerability window. Nothing in the stack lets you simply SWAP the isolation mechanism.
-
This paper's idea
Defer isolation to build time, behind an abstract API
FlexOS (Manchester, Politehnica Bucharest, Lancaster, KIT, NEC Labs — the Unikraft lineage) extends the library-OS idea of specialization from performance to safety. The OS is fine-grained micro-libraries; a build-time configuration file assigns each to a compartment, picks the isolation mechanism per boundary (Intel MPK domains or per-compartment VMs via EPT — CHERI sketched), the data-sharing strategy, and per-compartment software hardening (CFI, sanitizers). In the source, cross-library calls are abstract gates and shared data carries annotations; a Coccinelle-based toolchain instantiates them — same compartment: a plain function call, zero overhead; MPK: an inlined protection-key switch; EPT: shared-memory RPC between VMs. Because no one can hand-navigate the resulting space, partial safety ordering organizes configurations into a partially ordered set, labels them with measured performance, and returns the safest ones within a budget — pruning 80 Redis configurations to 5. Under equivalent configurations FlexOS matches or beats Linux, SeL4, and CubicleOS.
Why this problem is worth solving
Security engineering inside an OS is a bet placed decades in
advance. The
an OS built as a collection of libraries
linked directly with one application into a single bootable
image (a unikernel) — no user/kernel boundary, no system-call
overhead, only the components the application actually needs.
Historically specialized for performance; Unikraft, the base
here, decomposes the OS into fine-grained micro-libraries
(scheduler, TCP/IP stack, filesystem…) behind clean
interfaces
Memory Protection Keys — a mechanism that
tags each memory page with one of 16 keys and gives every
thread a user-writable register (PKRU) of per-key permissions,
checked by the MMU on each access. Switching protection
domains costs one register write (tens of cycles) instead of
an address-space switch — cheap intra-address-space isolation,
but with quirks: only 16 domains, no execute protection, and
any code can write the PKRU unless you verify the binary
The paper’s own measurements sharpen the cost of rigidity into one comparison: the identical compartmentalization decision — “isolate the scheduler” — costs Redis 43% of its throughput and Nginx 6%. Communication patterns, not principles, decide what isolation costs, and they differ per application. A world where the isolation strategy is a design-time constant is a world where almost every deployment is either less safe than it could afford to be, or slower than it needs to be. FlexOS’s wager is that isolation can be made a late-bound parameter — decided per deployment, changed by rebuild — without paying for the flexibility at runtime.
What people did before, and where it broke
- Monolithic kernels (Linux). Process isolation via address spaces, but the kernel is one trust domain: a bug in any driver owns the machine. Restructuring (breaking processes into isolated pieces, removing user/kernel separation for speed) is each a research project.
- Microkernels (SeL4 + Genode). Hardware isolation around every service, formally verifiable — and in this paper’s SQLite benchmark, 3.1× slower than FlexOS’s three-compartment MPK configuration achieving comparable structure.
- Unikernels / SASOSes (Unikraft and kin). Top performance, zero internal protection — one bad memcpy in the TCP stack owns the database sharing its address space.
- Compartmentalization frameworks (HODOR, ERIM, CubicleOS). Retrofit domains into existing software, but each hard-codes one mechanism and strategy. CubicleOS — the closest relative, also Unikraft-based — runs as a Linux userland process, makes MPK transitions via pkey_mprotect system calls, and lands an order of magnitude slower than FlexOS on SQLite; its trap-and-map sharing automates porting but cannot be turned off or swapped.
- LibrettOS. Can switch between SASOS and microkernel modes — two points, not a space.
How FlexOS works
Abstract isolation, instantiated by the toolchain
-
The configuration file (the entire user interface) new
A short YAML assigns micro-libraries to compartments, picks each compartment's mechanism (intel-mpk, ept), and lists per-compartment hardening (CFI, KASan, UBSan, stack protector). Rebuilding with a different file yields the same application under a different safety regime — the engineering cost of switching mechanisms is nil.
-
Abstract gates and shared-data annotations (the porting API) new
Cross-library calls become flexos_gate(lib, fn, args...) — inserted AUTOMATICALLY by static analysis over the call graph (function pointers need manual annotation). Data is private by default; developers annotate exceptions with __shared(lib) whitelists, found by running tests until access-violation crashes point at the symbol. Porting: 10 minutes (time subsystem) to 2–5 days (TCP/IP stack); LwIP's whole patch is +542/−275 lines with 23 shared variables.
-
Build-time source transformation new
Coccinelle rewrites the annotated source for the chosen configuration: same-compartment gates become plain function calls (zero overhead — you pay only for what you configure); the result is ordinary C, inspectable with diff, fully optimizable by the compiler — no runtime abstraction layer, the historical performance killer of flexible OS toolkits (Flux OSKit's COM).
-
MPK backend (cheap domains, one address space) new
One protection key per compartment plus shared domains; PKRU-write integrity by static binary analysis + W⊕X (sound because a unikernel loads no code after build). Two gate flavors: full (save/clear registers, switch to a per-thread-per-compartment stack — 108 cycles) and light (ERIM-style raw PKRU write, shared stacks — 62 cycles). Private heaps and per-compartment data/bss sections, protected at boot.
-
Data Shadow Stacks (sharing stack variables at stack speed) new
Prior systems converted shared stack variables to heap allocations — 100–300+ cycles each, as costly as the domain crossing itself. DSS doubles each thread's stack: the lower half stays private, the upper half lives in the shared domain, and the shadow of variable x is simply &x + STACK_SIZE — the toolchain rewrites references. Compile-time bookkeeping is preserved, so shared stack allocation costs the same constant 2 cycles as normal; memory cost for 8-thread Redis: 288 KB.
-
EPT backend (strong walls, separate VMs) new
One VM per compartment — each with its own copy of the trusted computing base — communicating by shared-memory RPC with busy-waiting service threads; the callee checks that the requested function is a legal entry point (a stronger CFI than MPK's: entry AND exit controlled). Gate cost ~462 cycles, about a Linux system call; the QEMU/KVM patch for inter-VM shared memory is under 90 lines. A CHERI backend is sketched: annotations become __capability, gates use CInvoke.
The idea
Late-bind the isolation decision, like the LibOS late-bound the feature set. The library OS already proved that OS composition can wait until build time; FlexOS extends the same move to safety. The key to making it free is doing the binding by source transformation rather than runtime indirection: a gate between co-located libraries literally disappears into a function call, so the un-isolated configuration measures identical to vanilla Unikraft, and every isolation boundary you do configure compiles to exactly the mechanism-specific minimum. Genericity’s classic tax — the abstraction layer — is paid at compile time, where it costs nothing.
An isolation mechanism is just domains + switches + shared memory. The portability claim rests on a minimal contract: any mechanism that offers protection domains, a switching primitive, and some shared memory can back FlexOS — which covers MPK’s privilege-switching world, EPT’s separate-systems world, and (sketched) CHERI’s capability world, three genuinely different isolation philosophies under one API. The backend differences that remain (per-compartment TCB copies for VM-style backends, stack strategies for MPK) are absorbed by hooks in the core libraries, so adding a mechanism means implementing gates and hooks, not redesigning the OS. When hardware protection breaks — the Meltdown scenario — the response is a rebuild, not a rewrite.
When you cannot rank safety, order what you can and measure the rest. Is 3 compartments + MPK + no hardening safer than 2 compartments + EPT + CFI? There is no defensible scalar answer — so FlexOS refuses to invent one. Partial safety ordering places configurations in a partially ordered set where edges follow only defensible dominance rules (more compartments ≥ fewer; stacked hardening ≥ none; stronger mechanism ≥ weaker), leaving incomparable branches incomparable. Performance, unlike safety, is measurable: the toolchain benchmarks configurations (pruning paths early, since performance decreases monotonically along them), drops those under budget, and returns the poset’s maximal surviving elements — for Redis, 5 configurations out of 80. The human picks from five, not eighty, and every survivor is undominated.
From YAML to hardened image
Redis, isolated three ways in an afternoon
Gate = abstract cross-library call, instantiated per configuration. DSS = data shadow stack. PKRU = MPK's per-thread permission register. Numbers from the paper's Xeon Silver 4114 evaluation.
Step 1 of 6
Port once: gates by analysis, sharing by crash
Static analysis over the call graph replaces every cross-library call with an abstract gate automatically. Then the crash-driven loop: run redis-benchmark, hit an access violation, read the faulting symbol, mark it __shared(lwip), repeat. Redis needs 16 shared variables (+279/−90 lines); occasionally a crash exposes a genuine design smell — ramfs turned out so entangled with vfscore that isolating them apart would share most of their state, so they become one compartment.
porting is per-library, one-time, and mechanism-agnostic the annotation records a security decision, not just an access fact
Why it works
- Unikraft’s micro-library decomposition already draws clean interfaces → compartment boundaries can reuse them at any granularity (merge or split), and the abstract-gate API is just those interfaces made explicit → flexible compartmentalization without a modularization project.
- Source-to-source instantiation happens before the compiler runs → co-located gates become plain calls (zero overhead, measured), isolated gates inline the mechanism’s minimum sequence, and all compiler optimizations apply → the flexibility itself costs nothing at runtime.
- Isolation mechanisms reduce to domains + switch + shared memory → MPK, EPT/VMs, and CHERI fit one API, with mechanism-specific kernel needs absorbed by core-library hooks → a new backend is gates-plus-hooks, and swapping after a hardware break is a rebuild.
- Stack allocation is cheap because bookkeeping happens at compile time → the DSS reuses that bookkeeping for shared variables (shadow = &x + STACK_SIZE in a shared upper stack half) → shared stack data at 2 cycles instead of 100–300+, for kilobytes of memory.
- Safety dominance is partially decidable even when scoring is impossible → a poset ordered by defensible rules, labeled with measurable performance, pruned monotonically → the design space FlexOS opens becomes navigable by non-experts (80 → 5).
What it costs
- Porting is real, and sharing annotations are judgment calls. Days per complex component, with the security quality of the compartmentalization resting on human-marked whitelists; automated shared-data analysis is future work, and the crash-driven loop finds what is accessed, not everything that should be private.
- Unikernel-shaped assumptions. The static, single-app, no-code-loading world makes key tricks sound (PKRU integrity by binary analysis, entry-point checking against known addresses). General-purpose multi-process systems get none of this for free.
- Trust concentrates in the TCB and toolchain. Boot, memory manager, scheduler, interrupt handling, and backend (~3,000 LoC) must be correct under every configuration — compromise there voids all walls; the compiler and hardware are trusted too (Coccinelle is not: transformations are compile-time checked).
- Mechanism limits leak through. MPK offers at most 16 domains, no execute protection, and its CFI is incomplete (a ROP into another compartment only faults on local-data access); EPT pays syscall-class gate latency and duplicates the TCB per compartment; small-payload, chatty boundaries stay expensive on every backend.
- Exploration needs a representative benchmark. Partial safety ordering ranks by measured performance; users who cannot benchmark their workload cannot use it, and the safety ordering is probabilistic dominance, not a guarantee.
- Prototype scope. Two backends complete (CHERI/SGX sketched), four applications ported, and “towards” in the title is honest — this is an architecture and an argument, demonstrated rather than industrialized.
The evaluation
Real hardware (Xeon Silver 4114), four applications (Redis, Nginx, SQLite, iPerf), 160+ auto-generated configurations via the Wayfinder benchmarking platform, and cross-OS comparisons against Linux, vanilla Unikraft, SeL4/Genode, and CubicleOS at structurally equivalent isolation profiles.
Isolation as a constant vs isolation as a parameter
| Property | Fixed-design OSes (Linux / SeL4 / unikernels) | CubicleOS & one-mechanism frameworks | FlexOS |
|---|---|---|---|
| When the isolation strategy is chosen | |||
| Cost of the flexibility itself | — | CubicleOS: MPK transitions via Linux pkey_mprotect syscalls, orders of magnitude costlier; runs in a userland debug platform | |
| SQLite, 5000 INSERTs (structurally comparable configs) | Linux 0.177 s; SeL4/Genode 0.333–0.702 s (2–3 compartments) | ||
| Domain-crossing and sharing primitives | Syscall ~146–470 cycles (no-KPTI/KPTI); address-space switches for processes | MPK via kernel round-trips | |
| Navigating the design space | No space to navigate | One point |
Check yourself
Check your understanding
1. Suppose an attack like Meltdown lands tomorrow and breaks Intel MPK's isolation guarantees. Predict the response timeline for Linux, for an MPK-welded framework like CubicleOS, and for a FlexOS deployment.
2. Sharing a stack variable across MPK compartments is awkward: the stack is private per compartment. Prior systems converted such variables to heap allocations. Why is that a false economy, and how do Data Shadow Stacks fix it?
3. Why does FlexOS's exploration tool refuse to assign each configuration a numeric safety score, and what does it do instead?
4. Isolating the scheduler costs Redis 43% of its throughput but Nginx only 6% — same OS, same mechanism, same wall. What explains the gap, and what does it prove about OS design?
Sources
- arXiv preprint 2112.06566 — full text
- ACM DL: FlexOS: Towards Flexible OS Isolation — ASPLOS 2022
- Project FlexOS — source code