Home · ASPLOS 2022 · Operating Systems & Virtualization

FlexOS: towards flexible OS isolation

Hugo Lefeuvre, Vlad-Andrei Badoiu, Alexander Jung, Stefan Lucian Teodorescu, Sebastian Rauch, Felipe Huici and 2 others

Conference
ASPLOS 2022
Published
2022
Pages
467-482
Citations
33

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

  1. 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.

  2. 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.

  3. 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.

  4. 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
tradition shows what specialization buys when you can rebuild the OS per application — but it specialized only for speed, discarding protection wholesale. Meanwhile the hardware floor keeps moving:
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
arrived in commodity CPUs, CHERI capabilities are arriving, TEEs multiplied — and no deployed OS could adopt any of them without redesign, because isolation assumptions soak through every subsystem.

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

  1. 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.

  2. 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.

  3. 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).

  4. 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.

  5. 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.

  6. 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.

TCB: boot code, memory manager, scheduler, interrupt context-switch, and the backend — ~3,000 LoC for MPK (the scheduler formally verified in Dafny). Prototype: Unikraft v0.5 + ~3,250 LoC kernel changes + 2,300 LoC toolchain. Open source.

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

PropertyFixed-design OSes (Linux / SeL4 / unikernels)CubicleOS & one-mechanism frameworksFlexOS
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 primitivesSyscall ~146–470 cycles (no-KPTI/KPTI); address-space switches for processesMPK via kernel round-trips
Navigating the design spaceNo space to navigateOne point
Xeon Silver 4114 @ 2.2 GHz, isolated cores, QEMU/KVM. Redis/Nginx space: MPK+DSS fixed, varying 1–3 compartments × component placement × per-component hardening (stack protector, UBSan, KASan) = 2×80 configurations via Wayfinder. iPerf: MPK reaches baseline throughput from 128 B buffers; EPT ~90% from 256 B — batching amortizes gate latency. Redis vs Nginx: identical walls, different prices (scheduler isolation 43% vs 6%).Underlined values explain themselves when clicked.

Check yourself

Check your understanding

  1. 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. 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. 3. Why does FlexOS's exploration tool refuse to assign each configuration a numeric safety score, and what does it do instead?

  4. 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

Related papers on Operating Systems & Virtualization

See all 170 →