Skip to content
 
 

Repository files navigation

Unikraft logo

The fast, secure and open-source
Unikernel Development Kit

Unikraft powers the next-generation of cloud native, containerless applications by enabling you to radically customize and build custom OS/kernels; unlocking best-in-class performance, security primitives and efficiency savings.

Homepage · Documentation · Report Bug · Feature Request · Join Our Discord · X.com



Features

  • Instantaneous Cold-boots

    • While Linux-based systems might take tens of seconds to boot, Unikraft will be up in milliseconds.
  • Modular Design 🧩

    • Unikraft boasts a modular design approach, allowing developers to include only necessary components, resulting in leaner and more efficient operating system configurations.
  • Optimized for Performance 🚀

    • Built for performance, Unikraft minimizes overheads and leverages platform-specific optimizations, ensuring applications achieve peak performance levels.
  • Flexible Architecture Support 💻

    • With support for multiple hardware architectures including x86, ARM, (and soon RISC-V), Unikraft offers flexibility in deployment across diverse hardware platforms.
  • Broad Language and Application Support 📚

    • Unikraft offers extensive support for multiple programming languages and hardware architectures, providing developers with the flexibility to choose the tools and platforms that best suit your needs.
  • Cloud and Edge Compatibility ☁️

    • Designed for cloud and edge computing environments, Unikraft enables seamless deployment of applications across distributed computing infrastructures.
  • Reduced Attack Surface 🛡️

    • By selectively including only necessary components, Unikraft reduces the attack surface, enhancing security in deployment scenarios. Unikraft also includes many additional modern security features.
  • Developer Friendly 🛠️

    • Unikraft's intuitive toolchain and user-friendly interface simplify the development process, allowing developers to focus on building innovative solutions.
  • Efficient Resource Utilization 🪶

    • Unikraft optimizes resource utilization, leading to smaller footprints (meaning higher server saturation) and improved efficiency in resource-constrained environments.
  • Community-Driven Development 👥

    • Unikraft is an open-source project driven by a vibrant community of over 100 developers, fostering collaboration and innovation from industry and academia.

Quick Start

Install the companion command-line client kraft:

# Install on macOS, Linux, and Windows:
curl -sSfL https://get.kraftkit.sh | sh

See additional installation instructions.

Run your first ultra-lightweight unikernel virtual machine:

kraft run unikraft.org/helloworld:latest

View its status and manage multiple instances:

kraft ps --all

View the community image catalog in your CLI for more apps:

kraft pkg ls --update --apps

Or browse through one of the many starter example projects.

Why Unikraft?

Unikraft is a radical, yet Linux-compatible with effortless tooling, technology for running applications as highly optimized, lightweight and single-purpose virtual machines (known as unikernels).

In today's computing landscape, efficiency is paramount. Unikraft addresses this need with its modular design, enabling developers to create customized, lightweight operating systems tailored to specific application requirements. By trimming excess overhead and minimizing attack surfaces, Unikraft enhances security and performance in cloud and edge computing environments.

Unikraft's focus on optimization ensures that applications run smoothly, leveraging platform-specific optimizations to maximize efficiency. With support for various hardware architectures and programming languages, Unikraft offers flexibility without compromising performance. In a world where resources are precious, Unikraft provides a pragmatic solution for streamlined, high-performance computing.

Getting Started

There are two ways to get started with Unikraft:

  1. (Recommended) Using the companion command-line tool kraft (covered below).

  2. Using the GNU Make-based system. For this, see our advanced usage guide.

Zig build facade and experimental Zig C compiler support

Zig 0.16.0 provides a compatibility facade for the existing GNU Make build. GNU Make remains the backend; this facade only maps Zig build steps and options to Make targets and assignments. It does not yet model Unikraft's components as a native Zig build graph.

The facade also exposes an isolated native configuration parser and header generator. It consumes an already solved Kconfig .config; it does not evaluate Config.uk, resolve dependencies/defaults, or replace the Kconfig solver. The non-destructive native steps are:

zig build config-inspect -Dapp=/absolute/path/to/app
zig build config-validate -Dapp=/absolute/path/to/app
zig build config-header -Dapp=/absolute/path/to/app

config-inspect prints the selected architecture and platform, and config-validate checks typed values plus the single-architecture and single-platform invariants. Boot-entry validation is intentionally deferred until native component registration can prove that the configured provider is present. config-header generates <output>/include/uk/bits/config.h without invoking Make. The steps use the existing Kconfiglib model only to export authoritative symbol types (including application and external component Config.uk files), and pair them with existing platform addplat_s registrations. Zig parses .config and writes the header deterministically. Use -Dconfig for a non-default .config, or -Dprefix to place the generated header below a prefix other than the build output.

From the Unikraft repository, build an application with:

zig build \
  -Dapp=/absolute/path/to/app \
  -Dconfig=/absolute/path/to/app/.config

When -Doutput is omitted, it safely defaults to <app>/build. The facade creates one fresh canonical output identity by resolving existing symlinks and the nearest existing ancestor of a new output. That same identity is used for validation, the runner argument, and Make's O= assignment; the runner requires those two arguments to match exactly before marker handling and again immediately before execution. The facade rejects repository/application ancestors, existing source directories, filesystem roots, and unmarked existing directories. New output directories receive a small marker so subsequent builds can distinguish them from source trees.

The default step delegates to Make's all target. Named steps include images, libs, objs, preprocess, prepare, fetch, configuration targets such as menuconfig and defconfig. The compatibility names clean, clean-libs, properclean, and distclean are registered but intentionally refuse to run; see the cleanup safety limitation below. For example:

zig build menuconfig -Dapp=/absolute/path/to/app
zig build images -Dapp=/absolute/path/to/app -Dverbose=1

-Dapp, -Doutput, -Dconfig, and -Dimage-name map to Make's A, O, C, and N variables. Repeat -Dexternal-lib, -Dexternal-platform, or -Dexclude to construct the L, P, or E path lists. Relative paths are resolved from the Unikraft repository root and canonicalized before they are forwarded. The runner revalidates A, O, C, L, P, and E immediately before executing a non-destructive Make target and refuses changed path identities. Because the existing Make backend does not quote these values end to end, the facade also applies a conservative ASCII allowlist. Paths accept letters, digits, /, -_.+@; joined L, P, and E lists additionally use : as their intentional separator. Image names accept letters, digits, and -_.+@. Tool/flag values additionally accept spaces, :, ,, and =. Whitespace in paths and Make/shell syntax such as quotes, backslashes, $(), backticks, ;&|<>, globs, #, %, and control characters are rejected. Additional Make assignments can be forwarded as individual, safely separated arguments with repeated -Dmake-arg=NAME=VALUE. This option uses a strict allowlist for compiler flags and non-path tools such as AR, NM, OBJCOPY, OBJDUMP, READELF, STRIP, UK_CFLAGS, and UK_LDFLAGS. Facade-managed, path, cleanup, configuration, and internal graph variables are rejected and must use dedicated facade options where available.

On Linux, -Dnative-make-environment=/absolute/private/environment.json provides an opt-in machine-generated environment for private native producers. The file must be current-user-owned, single-link 0600 in a private 0700 directory. Its schema is unikraft_native_make_environment_v1, with required bison_data, m4, shell, tmp, xdg_cache, xdg_config, zig_global_cache, and zig_local_cache absolute canonical paths. JSON is compact, has sorted keys and exactly one final LF, and rejects unknown, duplicate, or missing fields. Tools and Bison data must already exist under trusted directory chains; cache/temp directories must already be private. This emits only fixed-name Make assignments, including UMASK=0077, SHELL/CONFIG_SHELL, M4, BISON_PKGDATADIR, and the explicit temp/cache paths. GNU Make propagates these command-line assignments to its children. It neither forwards arbitrary environment variables nor changes canonical passwd HOME or the stable facade lock. Without the option, behavior is unchanged. This is configuration plumbing, not tool/runtime admission or a Python-free isolation guarantee: callers must separately bind the exact contract, executable/runtime closure, source, and resulting artifacts.

Make runs with a newly constructed environment rather than inheriting the caller's environment. Only the Make-safe canonical passwd HOME, a validated absolute-entry PATH (or /usr/bin:/bin fallback), and validated LANG/LC_ALL locale values are supplied. GNU Make control channels including MAKEFLAGS, GNUMAKEFLAGS, MAKEFILES, MFLAGS, MAKEOVERRIDES, MAKELEVEL, jobserver/restart/terminal state, compiler variables, and internal build variables therefore cannot rewrite goals or bypass the facade's assignment allowlist. Toolchain and flag overrides must use the dedicated Zig options or an allowlisted -Dmake-arg. Before process replacement, the runner resolves a bare Make command itself using only that validated child PATH; commands containing / are canonicalized directly. The selected backend must resolve to an absolute regular executable owned by root or the current user, without group/other write access. A final symlink is accepted only when its non-symlink parent and fully resolved target chains are both trusted. Canonical directory chains are walked with descriptor-relative, no-follow opens; ancestors must be root- or current-user-owned and not group/other-writable. A root-owned 01777 sticky boundary is accepted only when its protected child entry is trusted and not group/other-writable. Linux retains the opened executable descriptor and uses fexecve, so replacing the final directory entry cannot change the executed backend. Zig 0.16's macOS and OpenBSD libc targets do not export fexecve, so descriptor execution is conservatively gated to Linux. Other supported hosts retain the original descriptor, re-open the fully trusted chain at the last possible point, require matching device/inode identity, and then replace the process using the canonical absolute path. The runner's original inherited PATH is never consulted for execution.

Invoke only one Make-backed named step per zig build command. A portable, non-blocking file lock rejects overlapping Make processes rather than allowing selected steps such as clean all to race. For each effective UID, facade invocations use one lock in a private unikraft-zig-facade-<uid> directory beneath a stable, trusted per-user namespace. Linux prefers /run/user/<effective-uid> and falls back to the canonical home directory from the passwd database. macOS uses its OS-provided per-user temporary directory; other supported POSIX hosts use the canonical passwd home. HOME, XDG_RUNTIME_DIR, TMPDIR, TEMP, and TMP do not select the lock root. Every production root and ancestor is opened without following its final component and must be owned by root or the current user without group/other write access; the per-user root must be current-user-owned. The private directory must have mode 0700, and the regular, single-link lock mode 0600; symlinks and unsafe ownership or permissions are refused. This preserves same-user serialization across parent/nested outputs, caches, applications, and checkouts without allowing shared-temporary-directory pre-creation or environment changes to lock out a user or split the lock.

The private runtime directory must be outside every output tree; its persistent lock file is never unlinked while held. Before invoking Make, the runner makes the locked descriptor inheritable and replaces its own process image with the backend. Make and its descendants therefore retain the same lock even if the original runner PID is killed; the lock is released normally after the complete backend tree exits. Hosts without process-image replacement are refused rather than running without this lifetime guarantee. Run separate commands when multiple phases are needed.

The facade creates .unikraft-zig-build relative to a validated canonical output-directory handle. New markers use exclusive creation; existing markers must be current-user-owned, single-link regular files with safe permissions and the exact expected contents. No-follow, descriptor-relative operations prevent marker symlinks or replacement races from redirecting writes.

Non-destructive builds may use an external -Dconfig. Destructive Make targets cannot safely consume mutable pathnames after Zig's validation: properclean recursively removes O, distclean also removes C and companion metadata, and configured clean/clean-libs rules contain further generated paths. Until cleanup is implemented using portable descriptor-relative deletion, the facade refuses all four steps instead of exposing a check-then-delete race. Use manual cleanup, or invoke GNU Make directly only after independently ensuring the application, output, and configuration path components cannot be replaced. zig build test runs the facade's path, argument, and lock unit checks. It also links the production runner for x86_64/aarch64 macOS and x86_64 OpenBSD, catching target-libc symbol availability rather than stopping after code generation. python3 -m unittest -v support.scripts.tests.test_zig_facade adds end-to-end metacharacter, cross-checkout parent/nested output, destructive path replacement refusal, and orphaned backend-tree lock-lifetime coverage, including hostile inherited Make environments, output-identity replacement, runtime/build-marker entries, and differing temporary-directory environments.

The experimental QEMU/x86_64 native Zig image pipeline becomes:

zig build native-images \
  -Dnative-profile=qemu-x86_64 \
  -Dapp=/absolute/path/to/app \
  -Dconfig=/absolute/path/to/solved-qemu-x86_64.config \
  '-Dcompiler=zig cc -target x86_64-freestanding-none' \
  -Dcompiler-targeted=true \
  '-Dhost-cc=zig cc' \
  '-Dhost-cxx=zig c++' \
  -Dhost-cflags=-fno-sanitize=null \
  '-Dmake-arg=AR=zig ar' \
  -Dmake-arg=NM=llvm-nm \
  -Dmake-arg=OBJCOPY=llvm-objcopy \
  -Dmake-arg=OBJDUMP=llvm-objdump \
  -Dmake-arg=READELF=llvm-readelf \
  -Dmake-arg=STRIP=llvm-strip \
  -Dmake-arg=UK_CFLAGS=-std=gnu17 \
  -Dmake-arg=UK_LDFLAGS=-rtlib=compiler-rt

This step asks GNU Make only for compiled objects, generated linker scripts, and archives. build.zig executes the ordered zig cc -r library links, merges linker scripts, runs the final zig cc link, performs image post-processing, and publishes the image, debug image, bootinfo, and compile database under the build output directory. The x86_64 build requires Python 3 and the listed LLVM binary tools, but not GCC. The host-cflags exception accommodates Kconfig's kernel-style, null-derived list sentinel.

For QEMU/ARM64, use the same GCC-free tool contract with an explicit freestanding AArch64 target:

zig build native-images \
  -Dnative-profile=qemu-arm64 \
  -Dapp=/absolute/path/to/app \
  -Dconfig=/absolute/path/to/solved-qemu-arm64.config \
  '-Dcompiler=zig cc -target aarch64-freestanding-none' \
  -Dcompiler-targeted=true \
  '-Dhost-cc=zig cc' \
  '-Dhost-cxx=zig c++' \
  -Dhost-cflags=-fno-sanitize=null \
  '-Dmake-arg=AR=zig ar' \
  -Dmake-arg=NM=llvm-nm \
  -Dmake-arg=OBJCOPY=llvm-objcopy \
  -Dmake-arg=OBJDUMP=llvm-objdump \
  -Dmake-arg=READELF=llvm-readelf \
  -Dmake-arg=STRIP=llvm-strip \
  -Dmake-arg=UK_CFLAGS=-std=gnu17 \
  -Dmake-arg=UK_LDFLAGS=-rtlib=compiler-rt

Neither target requires GCC or GNU cross-binutils. Both require Python 3 and the listed LLVM binary tools. Passing -Dnative-profile also routes the images and default all steps through this native link pipeline. Other profiles and invocations without that option retain the GNU Make backend. The legacy -Dnative-qemu-graph option remains supported.

The hyperv-x86_64-efi profile extends the same compatibility pipeline with first-class target-side Zig objects and EFI post-processing. Its solved configuration must select the standalone x86_64 PLAT_HYPERV, PIE, and LIBUKPAGING; GNU Make continues to provide the shared x86 UEFI C/assembly objects. When the solved configuration enables LIBSTORVSC, the graph also adds LIBUKBLKDEV, the StorVSC C binding, and its freestanding Zig core:

The platform captures wall-clock time from UEFI before ExitBootServices, uses the Hyper-V reference-TSC page when available (falling back to the partition reference counter), and uses SynIC STimer0 for one-shot scheduler wakeups. SMP images allocate Kconfig-bounded, page-aligned SIMP/SIEFP storage for every logical CPU and program SynIC/STimer MSRs on the CPU that owns them. Hyper-V VP indices are discovered from HV_X64_MSR_VP_INDEX; logical CPU indices are never used as host VP identifiers. Secondary CPUs currently run the framework's idle/IPI entry rather than independent schedcoop workloads. VMBus control and primary StorVSC/NetVSC channels remain pinned to the BSP because schedcoop has no SMP-safe cross-CPU worker wake primitive. Subchannels, RSS, and multiqueue remain deferred until that scheduler support and live-host measurements justify their topology and queue policy.

SynIC IRQ entry saves only general-purpose registers. The VMBus bus C object uses Unikraft's |isr variant; the Hyper-V runtime and VMBus protocol Zig objects disable x87/MMX/SSE/AVX code generation in both build backends. Freestanding targeting alone is not sufficient. Queue publication, legacy event routing, and the ISR scheduler wake callback stay on this restricted path; protocol processing and channel/driver callbacks run in the worker.

zig build test-hyperv-irq runs the targeted hosted correctness tests, including 8-, 196-, and 240-byte receive payloads. Native Hyper-V image publication also checks the final linked IRQ call graph for unsaved FP/SIMD use and strong VMBus hook resolution using compiled Zig proof tools. For GNU Make images, build the native checker and run the same check explicitly:

zig build build-hyperv-image-proofs -j2
./zig-out/bin/hyperv-image-proof irq --image /path/to/image.dbg

The checker follows direct calls/tail branches, the native/controller SynIC dispatch, and schedcoop's registered ISR wake callback, rejecting unreviewed indirect edges. Only terminal assertion logging immediately leading to a fatal trap is excluded: it cannot return to the interrupted context. This compiler/register check is not live Hyper-V I/O or AP workload acceptance.

zig build test-hyperv-image-proofs -j2 runs the Python-free proof aggregate: native parser/register tests, real C/Zig objects and linked x86-64 ELF fixtures, and refusal mutations. It does not execute a guest or invoke Make. Set -Dproof-nm=/path/to/llvm-nm and -Dproof-objdump=/path/to/llvm-objdump when these native tools are not on PATH. The production image gates retain the configured NM/objdump commands, maximum CPU count, and StorVSC/NetVSC selection. See native image proof coverage for the SMP/IRQ/driver CLI, assertion mapping, and evidence limits.

zig build native-images \
  -Dnative-profile=hyperv-x86_64-efi \
  -Dapp=/absolute/path/to/app \
  -Dconfig=/absolute/path/to/solved-x86_64-efi.config \
  '-Dcompiler=zig cc -target x86_64-freestanding-none' \
  -Dcompiler-targeted=true \
  '-Dhost-cc=zig cc' \
  '-Dhost-cxx=zig c++' \
  -Dhost-cflags=-fno-sanitize=null \
  '-Dmake-arg=AR=zig ar' \
  -Dmake-arg=NM=llvm-nm \
  -Dmake-arg=OBJCOPY=llvm-objcopy \
  -Dmake-arg=OBJDUMP=llvm-objdump \
  -Dmake-arg=READELF=llvm-readelf \
  -Dmake-arg=STRIP=llvm-strip \
  -Dmake-arg=UK_CFLAGS=-std=gnu17 \
  -Dmake-arg=UK_LDFLAGS=-rtlib=compiler-rt

The separate hyperv-x86_64-efi-wamr profile registers the source-pinned native WAMR AOT application, its explicitly initialized VM capabilities, trusted embedded fixture and optional minimal WASI/CoreMarks. It retains the existing image safety gates and packaging/local-boot machinery. Build/link evidence is not native boot, hardware, networking or benchmark acceptance.

The hyperv-x86_64-efi-netvsc profile extends that graph with libuknetdev, the NetVSC C binding, and the freestanding NVS/RNDIS Zig protocol object. Its solved configuration must additionally select CONFIG_LIBUKNETDEV=y and CONFIG_LIBNETVSC=y; use the same command above with -Dnative-profile=hyperv-x86_64-efi-netvsc. If the solved configuration accepts uknetdev's CONFIG_LIBUKLIBPARAM implication, the native graph includes that library and its generated linker script as well.

For the first local boot, use a single-CPU hello-world configuration with CONFIG_LIBUKPRINT_KLVL_INFO=y. EFI configurations now default to 512 bootinfo memory descriptors: the firmware memory map and Memory Attributes Table must both fit before coalescing. Existing solved configurations retain their old value; update CONFIG_UKPLAT_MEMREGION_MAX_COUNT to at least 512 if it is still 128, then regenerate the configuration and rebuild.

With KVM access, OVMF, and QEMU's vmbus-bridge device available:

python3 support/build/tests/hyperv-efi-boot-test.py \
  --image /absolute/path/to/helloworld_hyperv-x86_64 \
  --ovmf-code /usr/share/OVMF/OVMF_CODE.fd \
  --ovmf-vars /usr/share/OVMF/OVMF_VARS.fd \
  --work-dir /absolute/path/to/build/hyperv-efi-smoke \
  --expect 'Hello world!'

This bounded smoke run requires hypercall/SynIC initialization, the application marker, a zero return from main, and normal QEMU exit. It keeps the serial log in the work directory and removes its temporary firmware and FAT staging files. The FAT directory is only an EFI launch fixture, not a GPT/ESP or Azure image. Passing does not establish VMBus channel or storage/network operation: bus errors remain visible in the serial log. Real Hyper-V I/O, fixed-VHD packaging through miz, and Azure Boot Diagnostics remain separate acceptance gates.

The Azure acceptance controller uses native miz packaging, locally boots both GPT/raw and fixed-VHD images, and manages a private, ownership-tagged Gen2 deployment with separate platform and real-I/O acceptance markers. Its separate exact two-boot StorVSC controller is destructive and default-off: credential-free fixtures do not replace an approved exact-image private x86 preflight or an explicitly authorized one-VM/two-disk Azure run.

NetVSC never guesses past a structurally malformed VMBus ring record. It disables channel callbacks and requests bounded VMBus reconnect, then closes and tears down the channel after the active callback/data-path operation has unwound. Queued TX ownership is released only after confirmed close/reset, while an ambiguous close quarantines GPA-direct buffers until the next connection generation. This preserves memory safety at the cost of a temporary interface outage when the host corrupts the shared ring.

Receive-section bounds are computed from the validated start, slot size, and slot count; the host's unused EndOffset is treated as informational. Inbound transfer ranges may be unaligned or span multiple slots, but every range must remain wholly inside one validated receive section and its registered GPADL. Transfer-page descriptors may contain 4-byte-aligned trailing padding between the range array and payload; parsers ignore only that bounded padding.

NetVSC completion callbacks mark TX contexts complete without freeing their uk_netbuf. The optional uknetdev post-return hook records that the public wrapper has finished statistics accounting. Reclaim waits until every concurrent successful wrapper has crossed that handoff, then runs from a later TX operation or quiesced teardown and frees each packet exactly once. Transaction sequences rotate to a fresh nonzero generation only with no live TX, control, NVS, or pending-ACK state; generation exhaustion takes the device offline.

Target Zig modules receive generated Kconfig headers through tracked build dependencies and can opt into narrow Unikraft include roots for @cImport. The resulting objects feed the normal library partial-link and final-link pipelines as LazyPath inputs. The EFI pipeline runs the repository's mkukreloc.py helper against the unstripped PIE image, updates .uk_reloc, and only then strips and converts the image to PE/COFF. Configuration and metadata inputs for target headers are content-tracked, so changing a solved configuration in place invalidates the generated header.

Self-relocation requires a loadable .uk_reloc section with space for the signature, static and dynamic records, and sentinel before post-processing. The linker keeps GOT entries in initialized data, before .bss, so their relocations are retained. On x86, the early relocator uses explicit RIP-relative addresses rather than consulting a GOT that has not yet been relocated.

Experimental LTO (QEMU/x86_64 native pipeline only)

The Zig 0.16 native-images pipeline supports a whole-program flat LTO path for the qemu-x86_64 profile. Selecting CONFIG_OPTIMIZE_LTO=y with the qemu-arm64, hyperv-x86_64-efi, or hyperv-x86_64-efi-netvsc native profiles is detected at build time and rejected with an explicit error. Non-LTO QEMU/ARM64 builds remain fully supported. The GNU Make backend is not affected: it retains its existing compiler-specific per-library LTO behavior (e.g. -flto forwarded to GCC or Clang) unchanged.

To enable LTO for the QEMU/x86_64 native pipeline, copy the application x86_64 defconfig, append CONFIG_OPTIMIZE_LTO=y, run zig build olddefconfig to fill in remaining defaults, then run zig build native-images with the same tool arguments. Using a shell array avoids repeating the argument list:

config=/absolute/path/to/qemu-x86_64-lto.config
output=/absolute/path/to/build-zig-lto
cp /absolute/path/to/app/defconfigs/qemu-x86_64 "${config}"
printf 'CONFIG_OPTIMIZE_LTO=y\n' >> "${config}"

zig_args=(
  "-Dapp=/absolute/path/to/app"
  "-Doutput=${output}"
  "-Dconfig=${config}"
  "-Dcompiler=zig cc -target x86_64-freestanding-none"
  "-Dcompiler-targeted=true"
  "-Dhost-cc=zig cc"
  "-Dhost-cxx=zig c++"
  "-Dhost-cflags=-fno-sanitize=null"
  "-Dmake-arg=AR=zig ar"
  "-Dmake-arg=NM=llvm-nm"
  "-Dmake-arg=OBJCOPY=llvm-objcopy"
  "-Dmake-arg=OBJDUMP=llvm-objdump"
  "-Dmake-arg=READELF=llvm-readelf"
  "-Dmake-arg=STRIP=llvm-strip"
  "-Dmake-arg=UK_CFLAGS=-std=gnu17"
  "-Dmake-arg=UK_LDFLAGS=-rtlib=compiler-rt"
)

zig build olddefconfig "${zig_args[@]}"
zig build native-images -Dnative-profile=qemu-x86_64 "${zig_args[@]}"

When CONFIG_OPTIMIZE_LTO=y is active the per-library zig cc -r partial-link and objcopy --keep-global-symbols stages are bypassed. All library object and archive inputs are collected in registration order and passed directly to a single zig cc -flto final link, allowing LLVM LTO cross-translation-unit optimization. The build applies only to the registered native graph scope; arbitrary application layouts outside the documented profiles are not guaranteed to work.

Symbol policy and linker arguments. After GNU Make materializes the native link inputs, lto-symbol-policy.py invokes the configured llvm-nm tool on each library's objects and archives, reads per-library export-symbol files, and validates cross-library symbol references. Private-symbol collisions (the same private name defined in multiple libraries) and illegal cross-library references to private symbols cause an explicit build failure. On success it generates a deterministic LLD version script with the global-symbol union and local: *;, plus a response file containing -Wl,-u force-keep arguments for the active export lists. The version script keeps private definitions local, while the response arguments preserve required exports through LTO internalization. Generating both at execution time also supports a clean build output when an export list is itself produced by GNU Make.

Measurement tools. Two scripts under support/scripts/ assist in evaluating the effect of LTO. They must be run against images built from identical configurations and identical source commits, differing only in the CONFIG_OPTIMIZE_LTO setting; results from differing commits or configs are not comparable.

Verify that Zig performs cross-translation-unit LTO at all (compiles two separate C translation units and confirms the callee is inlined and eliminated):

python3 support/scripts/lto-proof.py \
  --work-dir lto-proof-work \
  --zig /path/to/zig \
  --nm llvm-nm

Compare section sizes and defined-symbol counts between a baseline (non-LTO) and an LTO image:

python3 support/scripts/elf-size-diff.py \
  --readelf llvm-readelf \
  --nm llvm-nm \
  /path/to/baseline.dbg \
  /path/to/lto.dbg

Pass --json to get machine-readable output. The tool reports file_size, .text, .rodata, .data, .bss, and symbol_count deltas.

Representative observations. The following measurements were recorded for a hello-world application on QEMU/x86_64 with identical configs differing only in CONFIG_OPTIMIZE_LTO. They are informational observations on a specific workload and commit; they are not guaranteed thresholds and do not imply runtime speedup. Real applications may see different, smaller, or larger changes depending on their code and configuration:

Metric Delta
Debug ELF file size +0.57%
.text -0.22%
.rodata -6.83%
.data -45.57%
.bss +0.00%
Defined symbols -10.89%

The standalone Clang/LLVM 21.1.8 QEMU/ARM64 build uses the GNU Make backend directly:

llvm=/absolute/path/to/llvm/bin
app=/absolute/path/to/app
build=/absolute/path/to/build
make_args=(
  "A=${app}"
  "O=${build}"
  "HOSTCC=${llvm}/clang -fuse-ld=lld -rtlib=compiler-rt -unwindlib=none"
  "HOSTCXX=${llvm}/clang++ -fuse-ld=lld -rtlib=compiler-rt -unwindlib=none"
  "COMPILER=${llvm}/clang --target=aarch64-none-elf"
  "COMPILER_TARGETED=y"
  "LINKER=${llvm}/clang --target=aarch64-none-elf -fuse-ld=lld"
  "PARTIAL_LINKER=${llvm}/ld.lld -m aarch64elf"
  "PARTIAL_LINKER_TYPE=raw"
  "AR=${llvm}/llvm-ar"
  "NM=${llvm}/llvm-nm"
  "OBJCOPY=${llvm}/llvm-objcopy"
  "OBJDUMP=${llvm}/llvm-objdump"
  "READELF=${llvm}/llvm-readelf"
  "STRIP=${llvm}/llvm-strip"
  "UK_CFLAGS=-std=gnu17"
  "UK_LDFLAGS=-rtlib=compiler-rt"
)
make "${make_args[@]}" \
  "UK_DEFCONFIG=${app}/defconfigs/qemu-arm64" \
  defconfig
make "${make_args[@]}" -j"$(nproc)"

Raw ld.lld partial links activate Unikraft's merged linker-script path; the final link still runs through the Clang driver. The build requires GNU Make, Bison, Flex, Python 3, ncurses headers, and LLVM's Clang, LLD, archive, and ELF tools. It does not require GCC, libgcc, or GNU cross-binutils. The x86_64 LLVM release does not ship an AArch64 compiler-rt archive, so applications that emit compiler builtin calls must also supply AArch64 compiler-rt objects. CI builds and links a forced 128-bit division builtin from the matching pinned compiler-rt source release.

Toolchain Installation

You can install the companion command-line client kraft by using the interactive installer:

# Install on macOS, Linux, and Windows:
curl -sSfL https://get.kraftkit.sh | sh

macOS

brew install unikraft/cli/kraftkit

Debian/Fedora/RHEL/Arch/Windows

Use the interactive installer or see additional installation instructions.

Codespaces

Try out one of the examples in GitHub Codespaces:

Open in GitHub Codespaces

Container Build Environment

You can use the pre-built development container environment which has all dependencies necessary for building and trying out Unikraft in emulation mode.

Attach your working directory on your host as a mount path volume mapped to /workspace, e.g.:

docker run --platform linux/x86_64 -it --rm -v $(pwd):/workspace --entrypoint bash kraftkit.sh/base:latest

The above command will drop you into a container shell. Type exit or Ctrl+D to quit.

Testing your Installation

Running unikernels with kraft is designed to be simple and familiar. To test your installation of kraft, you can run the following:

kraft run unikraft.org/helloworld:latest

Build your first unikernel

Building unikernels is also designed to be straightforward. Build your first unikernel by simply placing a Kraftfile into your repo and pointing it to your existing Dockerfile:

spec: v0.6

runtime: base:latest

rootfs: ./Dockerfile

cmd: ["/path/to/my-server-app"]

Learn more about the syntax of a Kraftfile.

Once done, invoke in the context of your working directory:

kraft run .

Example Projects and Pre-built Images

You can find some common project examples below:

Example
Simple "Hello, world!" application written in C
Simple "Hello, world!" application written in C++
Simple "Hello, world!" application written in Rust built via cargo
Simple NodeJS 18 HTTP Web Server with http
Simple Go 1.21 HTTP Web Server with net/http
Simple Flask 3.0 HTTP Web Server
Simple Python 3.10 HTTP Web Server with http.server.HTTPServer

Find more examples and applications in our community catalog!

Cloud Deployment

The creators of Unikraft have built KraftCloud: a next generation cloud platform powered by technology intended to work in millisecond timescales.

Millisecond Scale-to-Zero Millisecond Autoscale Millisecond Cold Boots
Higher Throughput Much Lower Cloud Bill HW-Level Isolation
On-Prem or Cloud-Prem Works with Docker & K8s Terraform Integration

Contributing

Unikraft is open-source and licensed under BSD-3-Clause and the copyright of its authors. If you would like to contribute:

  1. Read the Developer Certificate of Origin Version 1.1.
  2. Sign-off commits as described in the Developer Certificate of Origin Version 1.1.
  3. Grant copyright as detailed in the license header.

This ensures that users, distributors, and other contributors can rely on all the software related to Unikraft being contributed under the terms of the License. No contributions will be accepted without following this process.

Afterwards, navigate to the contributing guide to get started. See also Unikraft's coding conventions.

Additional resources

License

Unikraft Open-Source Project source code and its affiliated projects source code is licensed under a BSD-3-Clause if not otherwise stated. For more information, please refer to COPYING.md.

Affiliation

Unikraft is a member of the Linux Foundation and is a Xen Project Incubator Project. The Unikraft name, logo and its mascot are trademark of Unikraft GmbH.


LinuxFoundation logo     XenProject logo

About

A next-generation cloud native kernel designed to unlock best-in-class performance, security primitives and efficiency savings.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages