Skip to content

Unikernels

Technical Overview

A unikernel is a single-address-space operating system image in which an application and the minimal OS libraries it needs are compiled together into a single executable that runs directly on hardware or a hypervisor. There is no user space, no kernel space — a single privilege level, a single address space, a single purpose.

The unikernel proposition: if you only run one application per VM, the OS abstraction (multiple users, processes, protection domains) is unnecessary overhead. Strip it out, link only the OS components you actually need, and deploy a ~1-10MB binary that boots in under 10ms with a minimal attack surface.

Prerequisites

  • Virtual machine architecture (hypervisor, guest OS, ring 0/3)
  • Operating system structure (kernel vs userspace split)
  • Library OS concept (from exokernel material, 04-exokernels.md)
  • Basic understanding of a typed functional language is helpful (for MirageOS)
  • Container and VM isolation models

Core Concepts

The Fundamental Design

Traditional VM Stack          Unikernel Stack
======================        ==================

+-------------------+         +-------------------+
| App (user space)  |         | App + libOS       |
+-------------------+         | (single image,    |
| OS (kernel+libs)  |         |  single address   |
+-------------------+         |  space)           |
| Hypervisor (Type1)|         +-------------------+
+-------------------+         | Hypervisor (Type1)|
| Hardware          |         +-------------------+
+-------------------+         | Hardware          |
                              +-------------------+

Traditional:                  Unikernel:
  OS: 100-500 MB              Image: 1-20 MB
  Boot: 30-60 seconds         Boot: 10-100 ms
  Idle RAM: 50-200 MB         Idle RAM: 4-64 MB
  Processes: many             Processes: 1 (application)
  Users: many                 Users: none
  Attack surface: full OS     Attack surface: minimal

Memory Layout

Unikernel Memory Layout (MirageOS on Xen example)
===================================================

+--------------------------------------------------+
| 0x000000: Application code (OCaml compiled)      |
| ...                                              |
| Stack region (single stack, no per-thread)       |
| Heap region (OCaml GC heap)                      |
| ...                                              |
| Mini-OS stubs (Xen hypercall interface)          |
| Network driver (mirage-net-xen library)          |
| Block driver (mirage-block-xen library)          |
| TCP/IP stack (mirage-tcpip library)              |
| ...                                              |
| [ENTIRE IMAGE IS SINGLE ADDRESS SPACE]           |
| [NO KERNEL/USER SPLIT. NO RING 3/RING 0 SPLIT]  |
+--------------------------------------------------+
| Xen Hypervisor (provides virtualized hardware)   |
+--------------------------------------------------+
| Physical Hardware                                |
+--------------------------------------------------+

Total binary size: typically 2-15 MB for typical apps

Because there's no user/kernel boundary, there's no syscall overhead. An application function call to the "network send" operation is just a function call to a library. The actual hypervisor interaction (hypercall to Xen, or VirtIO notification to KVM) happens only when hardware I/O is needed.

MirageOS (OCaml, Cambridge 2013)

MirageOS is the canonical unikernel implementation, developed at Cambridge University by Anil Madhavapeddy and team. It uses OCaml as the primary language, which provides: - Type safety: Memory safety without garbage collection overhead (mostly) - Modular functor-based design: OS components are OCaml module functors — swappable based on target - Cooperative threading via Lwt: Lightweight threads without OS scheduler involvement

(* MirageOS network stack application example *)
open Mirage

(* Define what this unikernel needs *)
let main =
  let packages = [
    package "mirage-protocols";
    package "mirage-net";
    package "tcpip";
  ] in
  foreign ~packages "Dispatch.Main"
    (console @-> stackv4v6 @-> job)

(* config.ml - compiled to specific target *)
let () =
  let stack = generic_stackv4v6 default_network in
  register "dispatch" [
    main $ default_console $ stack
  ]

(* dispatch.ml - the actual application *)
module Main (C: Mirage_console.S) (S: Tcpip.Stack.V4V6) = struct

  module TCP = S.TCP

  let start _console stack =
    (* Start TCP listener — no OS involvement, just library calls *)
    TCP.listen (S.tcp stack) ~port:80 (fun flow ->
      TCP.read flow >>= function
      | Ok (`Data buf) ->
        let response = "HTTP/1.1 200 OK\r\n\r\nHello" in
        TCP.write flow (Cstruct.of_string response) >>= fun _ ->
        TCP.close flow
      | _ -> Lwt.return_unit
    );
    S.listen stack
end

The same OCaml code, with a different config.ml, compiles to: - A Xen VM image (boots on Xen hypervisor) - A KVM/QEMU image (boots on Linux KVM) - A Unix process (for development, runs on Linux)

HalVM (Haskell)

Galois Inc.'s HalVM (Haskell Lightweight Virtual Machine) applies the same concept to Haskell. Haskell's strong type system and purity guarantees provide additional assurance properties. HalVM targets Xen and is used for security research applications where type-level invariants can be proved.

IncludeOS (C++)

IncludeOS allows existing C++ applications to be compiled as unikernels. It provides: - A partial C++ standard library implementation - VirtIO/Xen drivers - A simple TCP/IP stack - POSIX-like API stubs

IncludeOS is notable for demonstrating that unikernels are not limited to functional languages — existing C++ codebases can be ported with varying effort.

// IncludeOS HTTP server (simplified)
#include <net/interfaces.hpp>
#include <net/http/server.hpp>

void Service::start() {
  auto& inet = net::Interfaces::get(0);
  inet.network_config(
    {10, 0, 0, 42},    // IP
    {255, 255, 255, 0}, // Mask
    {10, 0, 0, 1}       // Gateway
  );

  // Start HTTP server — no fork(), no accept() syscall
  // Just library calls in single address space
  auto server = std::make_shared<http::Server>(inet.tcp());
  server->on_request([](http::Request_ptr req, http::Response_writer_ptr rw) {
    rw->write_header(http::OK);
    rw->write("Hello from unikernel\r\n");
    rw->end();
  });
  server->listen(8080);
}

Unikraft (Modular Unikernel Framework)

Unikraft (2021, NEC Research + University of Manchester) addresses a major unikernel limitation: lack of POSIX compatibility. Unikraft provides: - A microlibrary architecture: each OS component is a minimal, configurable library - POSIX compatibility layer: existing applications can run with minimal source modification - Multiple platform targets: Xen, KVM, bare metal - Automated build system: select only required components

Unikraft Component Selection
==============================

Application needs:
  - TCP server
  - File I/O (in-memory)
  - JSON parsing

Unikraft selects:
  [x] lib/uknetdev     (network device abstraction)
  [x] lib/lwip         (TCP/IP stack)
  [x] lib/ramfs        (in-memory filesystem)
  [x] lib/posix-socket (POSIX socket API)
  [x] lib/posix-stdio  (printf, etc.)
  [ ] lib/ext4fs       (NOT included — not needed)
  [ ] lib/9pfs         (NOT included — not needed)
  [ ] lib/vfscore      (NOT included — not needed)

Result: ~5 MB image vs. ~100 MB Ubuntu minimal

Unikernel vs Container vs VM Comparison

Comparison: Unikernel vs Container vs VM
==========================================

Property          | VM (Ubuntu) | Container  | Unikernel
------------------|-------------|------------|----------
Image size        | 500MB-2GB   | 10-200MB   | 1-20MB
Boot time         | 30-60s      | 100-500ms  | 10-100ms
Idle memory       | 100-300MB   | 10-50MB    | 4-64MB
Isolation level   | Strong (HW) | Weak (NS)  | Strong (HW)
Attack surface    | Full OS     | Full OS    | Minimal
POSIX compat      | Full        | Full       | Partial (Unikraft: good)
Processes/users   | Many        | Container  | None
Live debugging    | Yes         | Yes        | Very Hard
Language freedom  | Any         | Any        | Limited*
Shared kernel     | No          | Yes (host) | No
Per-app kernel    | No          | No         | Yes (LibOS)
Production maturity| Very High  | Very High  | Low-Medium

*MirageOS: OCaml; HalVM: Haskell; IncludeOS/Unikraft: C/C++;
 Unikraft adds limited Python, Go, Lua support

Historical Context

Research Lineage

The unikernel concept descends directly from: 1. Exokernel LibOS (MIT 1995): OS as library in application address space 2. Nemesis (Cambridge 1997): Single-process OS for multimedia, motivated latency guarantees 3. Singularity (Microsoft Research 2003-2012): .NET CLR as OS — type-safe single address space 4. MirageOS (Cambridge 2013): First practical unikernel for cloud deployment

The Microsoft Singularity project is particularly relevant — it demonstrated that a type-safe language (rather than hardware protection rings) could enforce OS isolation guarantees. A process-isolated Singularity program couldn't corrupt another process's memory because the language's type system guaranteed it, not the CPU's ring protection.

Cloud Timing

MirageOS appeared in 2013, coinciding with the explosion of cloud microservices and serverless computing. The unikernel's properties (tiny, fast boot, minimal overhead) aligned perfectly with the "function-as-a-service" deployment model where each function could be a separate unikernel image.

Anil Madhavapeddy's 2013 blog post and paper created significant buzz. Docker had just been released. The question was whether unikernels or containers would win for microservices.

Production Examples

Cloudius Systems / ScyllaDB

Cloudius Systems built OSv, a unikernel targeting JVM and general application compatibility. OSv includes a JVM without a host OS. ScyllaDB (a Cassandra-compatible NoSQL database) was originally developed for OSv before being ported to run on Linux. The ScyllaDB port retains unikernel-style architecture: user-space networking (DPDK), user-space I/O, custom memory management — a "unikernel philosophy" without requiring actual unikernel deployment.

Amazon Firecracker

AWS Firecracker (2018) is not a unikernel itself but is the hypervisor designed to host microVMs efficiently. Firecracker enables unikernel-like properties (fast boot, small memory footprint) for standard Linux guests: - Boot time: ~125ms for a minimal Linux guest - Memory overhead: ~5MB per microVM baseline - Deployed at: AWS Lambda, AWS Fargate (millions of instances)

The Firecracker team explicitly cited unikernel research as motivation for their approach to minimizing VM overhead.

NEC / Unikraft Production Use

NEC Research has deployed Unikraft-based services for internal applications. The combination of POSIX compatibility (allowing existing software to run) with unikernel isolation and boot speed makes Unikraft the most production-viable unikernel framework as of 2024.

Debugging Notes

Unikernel debugging is a known weak point of the architecture:

# MirageOS: run as Unix process for development debugging
mirage configure -t unix
mirage build
./_build/default/main.exe  # runs as normal Unix process, debuggable with gdb

# Production Xen unikernel: limited to serial console output
xl console unikernel-domain

# KVM unikernel via QEMU: gdb stub support
qemu-system-x86_64 \
  -kernel unikernel.elf \
  -s -S  # start paused, gdb server on port 1234
# then: gdb unikernel.elf -ex 'target remote :1234'

# Unikraft: GDB debugging (if compiled with debug symbols)
qemu-system-x86_64 -kernel app.elf -s -S -nographic
# gdb: target remote :1234
#      break main
#      continue

# Core issue: no /proc, no strace, no ptrace in production unikernel
# Debugging requires instrumentation at compile time

The standard debugging tools (strace, perf, gdb attach to running process) don't apply to unikernels. You must: 1. Debug in the Unix/development mode 2. Add extensive logging before deployment 3. Use hypervisor-level tracing (Xen's xenctx, QEMU's monitor)

Security Implications

Minimal Attack Surface

The compelling security argument for unikernels: if the image doesn't contain a shell, sshd, cron, or any other service, those cannot be exploited. A typical Linux OS contains hundreds of potentially exploitable services, configurations, and binaries. A unikernel contains exactly what was compiled in.

Attack Surface Comparison
==========================

Ubuntu 22.04 minimal server:
  Processes running: ~30 (systemd, journald, sshd, cron, ...)
  Syscalls exposed: ~400
  Binaries in /usr/bin: ~1000
  Third-party packages: varies

MirageOS unikernel:
  "Processes" running: 1 (the application)
  Syscalls: N/A (no syscall interface — single address space)
  External attack paths: only open network ports
  "Packages": exactly what was linked

No User/Kernel Separation: Security Risk

The single-address-space design is also a security risk: if the application is exploited (buffer overflow, use-after-free), the attacker has full memory access — there's no kernel space that's protected from a compromised user process. In a traditional OS, a compromised application still needs a local privilege escalation to reach kernel memory.

Mitigations: - Type-safe languages (OCaml, Haskell) prevent most memory safety bugs - No writable/executable memory (NX enforced at the only privilege level) - External isolation via hypervisor

No Dynamic Code Loading

Most unikernel frameworks don't support dynamic library loading (dlopen). This eliminates entire classes of attacks (DLL injection, LD_PRELOAD hijacking) but also limits application flexibility.

Performance Implications

Boot Time

System Cold Boot Time Notes
EC2 t3.micro (Amazon Linux) ~40 seconds Full Linux boot
Docker container ~100-500 ms Linux already running
Firecracker microVM ~125 ms Minimal Linux kernel
MirageOS on Xen ~10-30 ms Unikernel boot
Unikraft on KVM ~10-50 ms Modular unikernel
IncludeOS on KVM ~5-15 ms Minimal unikernel

For serverless-style functions that run for <1 second, the difference between 40s boot and 10ms boot is the difference between viable and impractical.

Runtime Performance

Network benchmark (HTTP GET, small response, MirageOS vs Linux): - MirageOS: ~50,000 req/s on 1 core - nginx on Linux (same hardware): ~100,000 req/s on 1 core

MirageOS underperforms nginx in raw throughput — the OCaml runtime and cooperative threading model have overhead. But the comparison with a full Linux OS is closer than expected, and for I/O-bound microservices, the difference may be acceptable given the security and size benefits.

Memory

A typical MirageOS unikernel using ~4MB baseline RAM vs. ~50MB for a minimal Ubuntu container running the equivalent process. For cloud billing, this can be significant at scale.

Failure Modes and Real Incidents

The Debugging Deficit in Production

A recurring theme in unikernel deployments: when something goes wrong in production, diagnosis is extremely difficult. There's no way to attach a debugger, no shell to exec into, no /proc to inspect. The only output is what was explicitly logged before deployment.

Teams that deployed MirageOS/IncludeOS for production services consistently report that the debugging deficit was the primary operational challenge, often outweighing the security and performance benefits.

Upgrade Complexity

Updating a unikernel requires recompiling and redeploying the entire image. There's no apt-get update equivalent. For security patches to library dependencies, the entire image must be rebuilt. This is theoretically solvable with CI/CD automation but requires more mature tooling than patching a running container.

Memory Safety in Non-Type-Safe Unikernels

IncludeOS and Unikraft use C/C++. A buffer overflow in application code has no kernel protection boundary to stop it. The single-address-space model means a compromised application owns the entire VM. This is arguably worse than running in a container where the container's kernel namespace at least limits the blast radius.

Modern Usage

Unikraft (2021+): Most production-viable. POSIX compatibility, active development, NEC/university backing.

OSv: Running JVM and containerized apps as unikernels on Xen/KVM. Used by some organizations for Java microservices.

MirageOS: Used for DNS-over-HTTPS resolvers (Cloudflare experimented with it), network function virtualization, and security research.

Nanos/Ops (2019): A Linux-compatible unikernel targeting running existing Linux binaries without modification. The most "drop-in" replacement approach.

Cloud Provider Support: AWS, GCP, and Azure do not have native unikernel support as of 2024. All require wrapping unikernels in hypervisors (Xen/KVM/Hyper-V). Firecracker brings the most unikernel-like properties to standard Lambda/Fargate.

Future Directions

WebAssembly as Unikernel Alternative: WASM with WASI (WebAssembly System Interface) provides many unikernel benefits — tiny, fast-starting, minimal attack surface — with better toolchain support and language compatibility. The debate about whether WASM/WASI replaces unikernels for cloud functions is active.

Unikernel Linux (UKL): Research at Boston University on running the Linux kernel in a unikernel configuration — single address space, compiled with the application, no user/kernel split. Maintains Linux's vast driver and API ecosystem while gaining unikernel boot time and overhead benefits.

CrosVM / Virtio-fs: Google's CrosVM (Chrome OS's VM monitor) and virtio-fs allow efficient host filesystem access from minimal VMs, addressing one of the unikernel deployment practicalities.

Exercises

  1. MirageOS HTTP Server: Install MirageOS and OCaml. Build and run the mirage-skeleton/applications/http example as a Unix process. Measure response latency vs. a standard nginx container. Then build the same code for Xen or KVM target and compare image sizes.

  2. Unikraft Hello-World: Build a minimal Unikraft application (HTTP server or key-value store) using the provided tutorials. Measure boot time and idle memory usage. Compare to the Docker equivalent.

  3. Attack Surface Analysis: Take a standard Ubuntu 22.04 minimal cloud image and a Unikraft unikernel running the same application. Use nmap and strace to enumerate the attack surface of each. Count: open ports, running processes, exposed syscalls, writable binaries. Quantify the difference.

  4. Debugging a Production Failure: Introduce a deliberate bug in a MirageOS application (e.g., incorrect HTTP response handling for a specific content type). Debug it first in Unix mode (where debuggers work), then reproduce the failure in a KVM target where only serial output is available. Document the debugging methodology difference.

  5. Performance Profiling: Write a unikernel (Unikraft or IncludeOS) that performs a tight compute loop and a I/O-intensive workload. Profile using hypervisor-level performance counters (QEMU's perf integration). Compare the cycle profile to the same workload in a Linux process. Identify where unikernel overhead appears.

References

  • Madhavapeddy, A., et al. "Unikernels: Library Operating Systems for the Cloud." ASPLOS '13. 2013. [The founding paper]
  • Madhavapeddy, A. and Scott, D. "Unikernels: Rise of the Virtual Library Operating System." Queue, ACM, 2013.
  • Kuenzer, S., et al. "Unikraft: Fast, Specialized Unikernels the Easy Way." EuroSys '21. 2021.
  • Engler, D., et al. "Exokernel: An OS Architecture for Application-Level Resource Management." SOSP '95. 1995. [LibOS foundation]
  • Hunt, G. and Larus, J. "Singularity: Rethinking the Software Stack." ACM SIGOPS Operating Systems Review, 2007.
  • MirageOS documentation: https://mirage.io/docs/
  • Unikraft documentation: https://unikraft.org/docs/
  • OSv project: https://github.com/cloudius-systems/osv
  • Nanos/Ops: https://nanos.org/
  • Firecracker design: https://github.com/firecracker-microvm/firecracker/blob/main/docs/design.md