No description
Find a file
Davíð Steinn Geirsson abc5f3c2a8 cleanup: remove dead code, fix inconsistencies, update README
- Remove unused flock from stage1 storePaths
- Remove duplicate screen package in minimal.nix
- Delete docs/superpowers/ planning artifacts (completed work)
- Fix stage1 shell scripts to use full Nix store paths consistently
  (touch, rm, od, tr were bare; add missing ln/rm/touch to storePaths;
  remove unused echo from storePaths; sort alphabetically)
- Remove unreachable else branch in flake.nix inspect app
- Fix fragile build check in run-tests.sh (use exit code, not grep)
- Rewrite README for current erofs/dm-verity/overlayfs architecture
  (was still describing old SquashFS/tmpfs extraction approach)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:38:48 +00:00
configs cleanup: remove dead code, fix inconsistencies, update README 2026-04-02 17:38:48 +00:00
lib eliminate double initrd/UKI build with PE-aware archive finder 2026-04-01 12:22:38 +00:00
modules cleanup: remove dead code, fix inconsistencies, update README 2026-04-02 17:38:48 +00:00
secureboot-test-keys Rename project to nixos-uki 2025-12-29 00:35:46 +00:00
tests Automated testing (not working yet) 2025-12-25 15:09:12 +00:00
tools find-archive: handle signed PEs, switch to virtio console 2026-04-01 17:42:51 +00:00
CLAUDE.md Rename project to nixos-uki 2025-12-29 00:35:46 +00:00
flake.lock Bump nixpkgs 2026-03-31 18:40:22 +00:00
flake.nix cleanup: remove dead code, fix inconsistencies, update README 2026-04-02 17:38:48 +00:00
README.md cleanup: remove dead code, fix inconsistencies, update README 2026-04-02 17:38:48 +00:00
run-tests.sh cleanup: remove dead code, fix inconsistencies, update README 2026-04-02 17:38:48 +00:00
test-boot.sh find-archive: handle signed PEs, switch to virtio console 2026-04-01 17:42:51 +00:00

nixos-uki

This repo contains tooling to build a NixOS system configuration into a self-contained Unified Kernel Image that can be directly booted from UEFI. This is useful for example to build a full-featured linux rescue system to recover from boot problems.

Architecture

This project builds self-contained Unified Kernel Images (UKIs) that boot NixOS systems entirely from RAM:

  1. UKI Structure: A UKI is a UEFI executable containing:

    • Linux kernel
    • Initramfs (with archive discovery scripts, tools, and dm-verity metadata)
    • Kernel command line
    • OS release information
    • Optional: Secure Boot signatures
  2. System Archive: The complete NixOS system is packaged as an erofs archive with:

    • Full /nix/store with all dependencies
    • Nix database for package management
    • System profile and configuration
    • dm-verity hash tree for cryptographic integrity verification
    • 4KiB identifier block for archive matching
  3. Boot Process:

    • UEFI firmware loads and executes the UKI
    • Kernel boots with systemd in initrd (boot.initrd.systemd.enable = true)
    • The uki-system.service runs in initrd:
      • Phase 1: Checks EFI boot variables to identify the boot partition
      • Phase 2: Scans existing block devices for the system archive
      • Phase 3: Polls for hotplugged devices (60s timeout)
      • Sets up dm-verity for integrity verification
      • Mounts erofs as read-only lower layer
      • Mounts tmpfs as writable upper layer
      • Creates overlayfs root filesystem combining both
    • systemd switches root to the overlayfs mount
  4. Deployment Modes:

    • Embedded Mode (unified): System archive appended to UKI after the PE data (single-file boot)
    • Split Mode: UKI + separate .erofs archive on FAT filesystem
      • Enables >2GB systems (UEFI PE binary size limit workaround)
      • Archive can be on USB drive, ESP partition, or any FAT volume
      • Archives >4GB are automatically split into chunks for FAT32 compatibility
      • Hotplug support: insert USB drive after boot starts
  5. Features:

    • Complete write access via tmpfs overlay on read-only erofs root
    • Full Nix package management (nix-env, nix-shell)
    • Variable system sizes (minimal rescue to full desktop)
    • Hotplug device support when searching for split system archive
    • Cryptographic verification of the system archive via dm-verity
    • Optional Secure Boot signing

Usage

Building a UKI

Use the Nix flake to build a UKI from a NixOS configuration:

# Build the example minimal rescue system (embedded mode - single file)
nix build .#minimal
# Result: ./result/minimal.efi

# Build in split mode (UKI + separate archive)
nix build .#minimal-split
# Result: ./result/minimal.efi and ./result/minimal.erofs

# Build with Secure Boot signing
nix build .#minimal-signed

# The UKI is created at ./result/
ls -lh ./result/

Custom Configurations

Create your own NixOS configuration and build it as a UKI:

# my-config.nix
{ config, pkgs, ... }:
{
  networking.hostName = "my-rescue";

  environment.systemPackages = with pkgs; [
    vim htop git
  ];

  services.openssh.enable = true;
  # ... more configuration
}

Build your custom UKI:

# flake.nix
{
  inputs.uefi-rescue.url = "github:yourname/nixos-uki";

  outputs = { self, uefi-rescue, nixpkgs }: {
    packages.x86_64-linux.my-uki = uefi-rescue.lib.buildUKI {
      nixosConfig = ./my-config.nix;
      name = "my-rescue";
    };
  };
}

Build Options

The buildUKI function accepts:

  • nixosConfig (required): Path to NixOS configuration file
  • name (optional): UKI name, default: "nixos-uki"
  • splitImage (optional): Build split mode (UKI + .erofs file), default: false
  • enableSecureBoot (optional): Enable Secure Boot signing, default: false
  • secureBootKey (optional): Path to signing key (required if enableSecureBoot)
  • secureBootCert (optional): Path to certificate (required if enableSecureBoot)
  • compression (optional): erofs compression (zstd/lzma/lz4hc/deflate/lz4), default: "zstd"
  • compressionLevel (optional): Compression level, default: max for chosen compressor
  • maxSize (optional): Maximum UKI size in bytes, default: 4294967295 (4 GiB - 1, FAT32 limit)

Example with split mode:

buildUKI {
  nixosConfig = ./my-config.nix;
  name = "large-rescue";
  splitImage = true;  # Outputs both .efi and .erofs files
}

Example with Secure Boot:

buildUKI {
  nixosConfig = ./my-config.nix;
  name = "secure-rescue";
  enableSecureBoot = true;
  secureBootKey = ./db.key;
  secureBootCert = ./db.crt;
}

Included Modules

The build system automatically includes these NixOS modules:

  1. uki-system.nix: General NixOS options for all built images

    • Enables Nix with flakes support
    • Disables documentation (size optimization)
    • Configures networking (DHCP, systemd-networkd)
    • Serial console support (hvc0) for debugging
  2. stage1.nix: Initramfs/stage1 boot logic

    • systemd-initrd: Enables systemd in initrd for robust device management
    • uki-system.service: Hotplug-aware archive discovery and mounting
      • Three-phase search: EFI variables -> existing devices -> hotplug polling
      • dm-verity integrity verification
      • Overlayfs mount (erofs lower + tmpfs upper)
    • Kernel patches for erofs compression and virtio console
    • find-archive C utility for locating archives in PE files
  3. stage2-activation.nix: Post-switch-root NixOS activation service

  4. tmpfs-boot.nix: tmpfs-aware configuration

    • Volatile storage for journals
    • Persistent machine-id
    • tmpfs temporary directories

You can override these settings in your configuration as needed.

Testing

Automated Tests

Run the test suite to verify images build and boot correctly:

./run-tests.sh

This script:

  • Builds the minimal rescue image
  • Boots it in QEMU using test-boot.sh
  • Monitors the serial console for the root login prompt [root@nixos-rescue:~]#
  • Automatically stops QEMU once autologin is detected
  • Times out after 120 seconds if boot fails

Available tests:

  • minimal-boot: Verifies the minimal rescue image boots and autologin works

Manual Testing

To boot a UEFI binary interactively (such as the built UKI), use the provided test script:

./test-boot.sh <path-to-efi-binary> [<additional files...>]

The script performs a full UEFI boot process:

  • Creates a temporary disk image with a GPT/FAT32 partition
  • Copies the UKI to /EFI/BOOT/bootx64.efi
  • Boots QEMU with OVMF (UEFI firmware) and KVM acceleration
  • Connects the serial console to a UNIX socket and either a logfile or stdout
  • Connects the QEMU monitor to a UNIX socket

The serial and monitor socket paths will be printed at launch.

Graphical mode

Same as above, but also opens a GTK window with access to the VGA console:

./test-boot.sh --graphical <path-to-efi-binary> [<additional files...>]

Logging output to file

./test-boot.sh --logfile <log-file-path> <path-to-efi-binary> [<additional files...>]

File Structure

.
├── flake.nix                    # Main entry point
├── lib/
│   ├── build-uki.nix           # UKI orchestration (embedded & split modes)
│   └── system-archive.nix      # erofs builder with Nix DB initialization
├── modules/
│   ├── uki-system.nix          # Common NixOS options used in all built images
│   ├── stage1.nix              # NixOS options specific to our stage1 initrd
│   ├── stage2-activation.nix   # Post-switch-root activation service
│   └── tmpfs-boot.nix          # tmpfs-aware configuration
├── tools/
│   └── find-archive.c          # C utility to locate archives in PE files
├── configs/
│   ├── minimal.nix             # Minimal rescue system
│   └── fat.nix                 # KDE desktop image
├── test-boot.sh                # QEMU test script with serial console
├── run-tests.sh                # Automated test suite
├── README.md                   # This file
└── CLAUDE.md                   # Assistant-specific documentation

Limitations

UEFI Binary Size Limit

UEFI binaries are 32-bit PE executables with a theoretical maximum size of 2GiB. In practice, many UEFI implementations have much smaller limits (often 512MB-1GB).

Solution: Use split mode to work around this limitation:

  • The UKI contains only the kernel and minimal initrd
  • The system archive is stored separately as a .erofs file
  • Archives >4GB are automatically split into FAT32-compatible chunks
  • Both files can reside on the same FAT partition (ESP or USB drive)

This enables building full-featured systems that would exceed UEFI size limits in embedded mode.