ProductsLamBootDocs › Architecture (deep dive)

Architecture (deep dive)

The eight layers and the ten-phase boot flow, in full.

This deep dive describes how LamBoot is built: the 10-phase boot flow, its memory and NVRAM models, the protocols it speaks to firmware and kernel, and the eight-layer dependency architecture that the codebase is mechanically held to. At v0.15.2 (June 2026) LamBoot is about 16,000 lines of Rust across 46 modules, producing a binary of about 650 KB on x86_64 (662,016 bytes measured) and about 570 KB on aarch64. It targets x86_64 and aarch64 UEFI, is licensed MIT OR Apache-2.0, and reads ext4, btrfs, and FAT natively, including on LVM, read in place.

Boot Flow

LamBoot executes a 10-phase boot sequence:

Phase 1: Health Assessment
    Read previous BootState from NVRAM
    If prev=Booting: increment crash counter (previous boot failed)
    If prev=BootedOK/Fresh: reset crash counter
    Set state=Booting, write timestamp
    Set Boot Loader Interface variables (LoaderInfo, LamBootVersion)

Phase 2: Security Initialization
    Read SecureBoot EFI variable
    Check for ShimLock protocol (shim loaded?)
    Log Secure Boot state: disabled / active+shim / active+direct
    Initialize TPM context (check TCG2 protocol, tpm_present())

Phase 3: Mount ESP
    Get LoadedImage protocol from our image handle
    Get device handle from LoadedImage
    Open SimpleFileSystem on device handle
    Open volume root directory -> EspVolume

Phase 4: Load Policy
    Read \EFI\LamBoot\policy.toml
    Parse with section-aware TOML parser (qualified keys)
    On failure: use defaults (4s timeout, threshold=2)
    Measure config into TPM PCR 5

Phase 5: Load Filesystem Drivers
    Scan \EFI\LamBoot\drivers\ for *.efi files
    For each: LoadImage() + StartImage() (driver registers DriverBinding)
    ConnectController(recursive=true) on ALL handles
    New SimpleFileSystem handles now available for ext4/btrfs partitions

Phase 6: Enumerate Volumes
    find_handles::<SimpleFileSystem>() -> all filesystem handles
    Open each as EspVolume
    Result: ESP + any newly accessible partitions

Phase 7: Discover Boot Entries
    For each volume: scan /loader/entries/*.conf (BLS Type 1)
        Parse each .conf file (14 fields, multi-value initrd/options)
        Filter by architecture, apply policy allowlist/denylist
        Sort by: bad-entries-last, sort-key, machine-id, version (UAPI.10)
    ESP fallback: scan for Windows, UKI, GRUB, rEFInd, tools
    Legacy distro scanning only if no BLS entries found
    Deduplicate by path (BLS entries take precedence)

Phase 8: Crash Loop Check
    If crash_counter >= crash_threshold:
        Try fallback_order entries from policy
        If no fallback found: fall through to menu

Phase 9: Interactive Menu
    If GOP available: graphical menu (double-buffered framebuffer)
        Render to off-screen Vec<BltPixel> buffer
        Single BltOp::BufferToVideo per frame (~60 FPS)
        Mouse + keyboard input
    If no GOP: text console (SimpleTextOutput)
        Numbered entries, 0-9 keys + arrow keys
    Auto-boot on timeout (configurable, disabled for tool-only entries)
    System actions always visible at bottom of menu:
        F2: Reboot to Firmware Setup (sets OsIndications, cold resets)
        F12: Cold reboot
    If no bootable entries found: show recovery screen with system actions
    If crash loop detected: disable auto-boot, wait for manual selection

Phase 10: Boot Handoff
    Record boot entry to NVRAM (LamBootLastEntry, LoaderEntrySelected)
    Write boot report to ESP (\EFI\LamBoot\reports\boot.json)
    If boot-counted entry: decrement tries_left, rename .conf, set LoaderBootCountPath
    Measure kernel cmdline into TPM PCR 12
    For Linux: search ALL volumes (ESP + driver-exposed) for kernel/initrd
    For Linux: read initrd, register LoadFile2 protocol (LINUX_EFI_INITRD_MEDIA_GUID)
    Set kernel load options (command line)
    start_image() -> kernel takes control
    If kernel returns: mark_boot_success(), cleanup initrd handle
    If menu returns error: reboot to firmware setup (safety net)

Memory Model

LamBoot uses UEFI Boot Services memory allocation:

  • global_allocator feature provides Rust's alloc via UEFI pool allocation
  • Framebuffer: Vec<BltPixel> of width * height (~8MB at 1920x1080)
  • Initrd: Box<[u8]> leaked via Box::into_raw() for stable addresses during LoadFile2
  • Font: Terminus Bold bitmap fonts. 16px (8x16, 4KB) and 32px (16x32, 16KB), compiled into .rodata

Heap fragmentation stays a non-issue because UEFI apps run single-threaded with a flat memory model. The bootloader exits Boot Services before the kernel, so all UEFI memory is reclaimed.

NVRAM Variable Protocol

All LamBoot variables use:

  • Vendor GUID: 4C414D42-4F4F-5400-0000-000000000001 (ASCII "LAMBOOT")
  • Attributes: BOOTSERVICE_ACCESS | RUNTIME_ACCESS
  • RUNTIME_ACCESS allows the running OS to read/write variables

The Boot Loader Interface variables use the systemd vendor GUID (4a67b082-...) for compatibility with bootctl, systemd-bless-boot, and other tools that expect these standard variables.

LoadFile2 Initrd Protocol

The initrd delivery mechanism follows the same pattern as systemd-boot and Sprout:

1. Read initrd file into Vec<u8>
2. Leak the Vec via Box::into_raw() (stable address)
3. Build VenMedia device path with LINUX_EFI_INITRD_MEDIA_GUID
4. Create InitrdProvider struct (first field = LoadFile2 function pointer)
5. Install DevicePathProtocol on new handle
6. Install LoadFile2Protocol on same handle
7. start_image(kernel) -> kernel EFI stub discovers initrd via LocateDevicePath
8. Kernel calls our callback:
   - NULL buffer -> return BUFFER_TOO_SMALL with size
   - Valid buffer -> memcpy data, return SUCCESS
9. On cleanup (Drop): uninstall both protocols, reclaim memory

RAII via Rust's Drop trait ensures cleanup even on early returns.

Native PE Loader

LamBoot loads the kernel itself with a native PE loader rather than relying on firmware LoadImage. The Layer-7 boot module dispatches across chainload, UKI, native-PE, and firmware-LoadImage paths, with the native PE loader giving LamBoot direct control over how the kernel image is mapped and started.

Filesystem Driver Loading

The driver loading model follows the UEFI specification's DriverBinding pattern:

1. Read .efi driver file from ESP
2. LoadImage() -> firmware creates image handle
3. StartImage() -> driver's entry point runs, registers DriverBindingProtocol
4. After all drivers loaded:
   ConnectController(recursive=true) on ALL handles in the system
5. Firmware matches DriverBinding to block devices
6. New SimpleFileSystem handles appear for supported partitions

This is the same approach used by rEFInd and Sprout. The key insight is that LamBoot does not need to manually match drivers to devices: the UEFI firmware's controller connection logic handles this automatically. LamBoot also reads ext4, btrfs, and FAT natively, including on LVM, read in place, so native partitions are accessible without these legacy filesystem drivers.

BLS Entry Sorting

The sort algorithm implements the full UAPI Group specification:

Tier 1: Boot count state
    Bad entries (tries_left == 0) sort LAST

Tier 2: Sort-key presence
    Entries WITH sort-key sort BEFORE entries without

Tier 3: Multi-field comparison (both have sort-key)
    sort-key: ascending strcmp
    machine-id: ascending strcmp
    version: DESCENDING UAPI.10 comparison (newest first)

Tier 4: Filename fallback (no sort-key or all fields equal)
    Entry ID: DESCENDING UAPI.10 comparison

UAPI.10 Version Comparison

The version comparison algorithm handles:

  • ~ creates pre-release: 1.0~rc1 < 1.0
  • ^ creates post-release: 1.0 < 1.0^post1
  • Numeric segments compared as integers (leading zeros stripped)
  • Alphabetic segments: uppercase sorts LOWER than lowercase
  • Separators (_, +) are skipped

Security Model

Secure Boot

Three modes of operation:

  1. Shim 16.1+: Shim overrides SystemTable's LoadImage/StartImage. LamBoot's standard boot::load_image() calls go through shim's verification hooks transparently, with no bootloader code changes needed.
  2. Legacy shim: LamBoot detects ShimLock protocol and uses shim_lock.verify() to validate images before loading.
  3. Direct signing: LamBoot binary is signed with sbsign against the machine's db key. Firmware verifies the signature during initial load.

For production Secure Boot deployment today, enrolling LamBoot's key via MOK enrollment is the supported path. See Secure Boot Deployment and MOK Enrollment Guide. LamBoot's first GPG-signed release shipped at v0.10.0, and signing applies from that release onward.

TPM Measured Boot

Measurements follow the Linux TPM PCR Registry:

  • PCR 4: Kernel image (using PE_COFF_IMAGE flag for Authenticode hash)
  • PCR 5: Boot configuration (policy.toml raw bytes)
  • PCR 12: Kernel command line (UTF-16 without trailing NUL)

All measurements use hash_log_extend_event which hashes the data, extends the PCR, and logs the event in the TCG event log. This is compatible with systemd-cryptenroll for TPM-bound disk encryption.

Module Manifest

Diagnostic modules in \EFI\LamBoot\modules\ are discovered via discover_tools(). An optional manifest.toml provides friendly names. The working diagnostic modules today are diag-shell, pci-inventory, and mem-quick:

[modules.pci-inventory]
name = "PCI Inventory"

[modules.mem-quick]
name = "Quick Memory Test"

An nvme-diag module is present as a stub and is not yet implemented. Modules with Icon::Tools are excluded from the auto-boot timeout: only real OS entries (BLS, UKI, Windows, other bootloaders) trigger auto-boot.

Error Handling

LamBoot follows these principles:

  • Keep optional features from blocking boot: TPM absent? Skip. No drivers directory? ESP-only mode. ShimLock unavailable? Use standard LoadImage.
  • Always leave the user a way out: Recovery options (F2 firmware setup, F12 reboot) stay visible at all times. The no-entries screen shows diagnostic info, and a menu error triggers an automatic reboot to firmware setup.
  • Cascade to simpler modes: No GOP? Text console. No BLS entries? Legacy scanning. Crash loop? Fallback entry.
  • Search all volumes: Kernels and initrds may live on ext4/btrfs partitions exposed by filesystem drivers, in addition to the FAT ESP.
  • Log everything: All errors are logged via the log crate, captured by UEFI's debug infrastructure.
  • Report to ESP: Boot reports with timestamps go to \EFI\LamBoot\reports\boot.json.

Hardware Detection (Phases 2.5 to 2.8)

Added in v0.2.0, these phases run between security init and ESP mount:

Phase 2.5: SMBIOS. Walks SMBIOS 2.x/3.x structure table for Type 1 (System Information: manufacturer, product, serial) and Type 11 (OEM Strings). OEM strings with lamboot.KEY=VALUE prefix are parsed for VMID, fleet-id, and other host-injected tags.

Phase 2.6: Hypervisor Detection. CPUID leaf 0x40000000 to detect KVM, Hyper-V, VMware, Xen, Parallels, VirtualBox. x86_64 only.

Phase 2.7: IOMMU Detection. Walks ACPI RSDP, then XSDT/RSDT, then DMAR (Intel VT-d) or IVRS (AMD-Vi) tables. Extracts DRHD/IVHD hardware unit descriptions with PCI device scope paths. Reports RMRR reserved memory regions.

Phase 2.8: fw_cfg Data Channel. Reads QEMU fw_cfg I/O ports (0x510/0x511, x86_64 only). Reads VM Generation ID from etc/vmgenid_guid for snapshot detection. Reads optional opt/lamboot/config for host-injected configuration.

Persistent Boot Log

The bootlog.rs module provides crash-safe boot logging to \EFI\LamBoot\reports\boot.log:

  • Write-through mode (phases 1 to 8): Every log entry is appended to the ESP file immediately. If LamBoot crashes during init, the log captures how far it got.
  • Buffered mode (phase 9, menu): Log entries accumulate in memory. This reduces I/O during the interactive menu where crashes are unlikely.
  • Flush: All buffered content is written to ESP before booting the selected entry.
  • Size cap: 64 KB maximum. The previous boot's log is overwritten on each boot.

Two-Column GUI Layout

The GUI (gui.rs) uses a two-column layout:

  • Left column (55%): Boot entries (kernels, UKIs, EFI loaders). Selection index 0..boot_count.
  • Right column (40%): Tools plus system actions. Separate scroll state.
  • Navigation: Up/Down within a column, Left/Right to switch columns.
  • Header: Logo plus title (left), VMID plus hypervisor plus build info (right).
  • Footer: Status message (left), keyboard hints (right).

The GUI opens GOP with open_protocol_exclusive, which disconnects OVMF's GraphicsConsole driver. This is necessary for direct framebuffer access but has a critical side effect.

GraphicsConsole Reconnection

When the GUI opens GOP exclusively, OVMF's GraphicsConsole driver (which renders text via GOP) is disconnected. After the GUI closes, text-mode ConOut becomes invisible: child images appear to hang but are actually running with no visible output.

Fix: Before every start_image call (in chainload_efi, boot_uki, boot_linux), LamBoot calls connect_controller(gop_handle, None, None, true) to reconnect the GraphicsConsole driver. This restores text rendering for child applications.

This pattern is documented in boot.rs::reconnect_console_drivers().

Extra Volume Scanning Scope

After loading filesystem drivers and reconnecting controllers, enumerate_volumes() discovers all SimpleFileSystem handles, including the root filesystem partition. UEFI filesystem drivers for Linux-native formats (ext4, btrfs) are slow at directory traversal on large populated filesystems. Operations like read_dir("\\loader\\entries") on a 37 GB ext4 root partition hang indefinitely under UEFI's single-threaded I/O model.

Current scope: BLS entry scanning is ESP-only, and extra-volume BLS scanning plus OS identification stay off for that reason. This is sufficient for Fedora and Debian, which write BLS entries to the ESP regardless of XBOOTLDR partition presence.

Impact: Kernels and initrds on non-ESP partitions remain accessible for loading, because targeted file reads work fine. Only directory enumeration is affected.

UKI Two-Pass PE Parsing

Unified Kernel Images (UKIs) embed kernel, initrd, and metadata in a single PE binary (60 to 100 MB on Fedora). Reading the entire file into memory for metadata extraction would exhaust UEFI pool memory.

Solution (uki.rs::read_uki_metadata):

  1. Pass 1: Read 4 KB (PE headers plus section table). Parse section offsets and sizes.
  2. Pass 2: For each target section (.osrel, .cmdline, .uname), seek to its offset and read only that section. Sections > 64 KB are skipped (these are .linux and .initrd, the large binary payloads).

This reads ~5 KB total instead of 60+ MB.

Menu to Boot Loop

When a chainloaded tool (diagnostic module) returns Status::SUCCESS, LamBoot re-enters the menu loop instead of rebooting. The run_bootloader function wraps the menu plus boot sequence in a loop:

  1. Display menu, wait for selection
  2. Record selection, write reports, flush boot log
  3. Call boot_entry()
  4. If the entry was a tool (Icon::Tools) and start_image returned: reclaim volumes, loop to step 1
  5. If the entry was a kernel/UKI: kernel takes over (never returns)

This allows running multiple diagnostic tools in succession without rebooting.


Layer Architecture: the Authoritative Model

Status: normative. Every module declares its layer, and the contract is mechanically enforced by tools/check-layers.py (run in the pre-commit hook and in CI). Audience: LamBoot developers, architecture reviewers, and SDS authors.

Why this model exists

LamBoot is organized into eight dependency layers. Every module declares its layer in a module-level doc comment, and dependencies flow one direction: toward the firmware boundary. This is enforced, not aspirational. tools/layer-map.toml is the machine-readable source of truth, and tools/check-layers.py fails any build where a module lacks its declaration or imports a higher layer.

Rule: higher-numbered layers may depend on lower-numbered layers, and dependencies must always point downward. A module that violates this fails the CI gate.

History. Earlier revisions of this document described a planned structure that the code had outgrown: it listed ~28 modules, placed fs.rs at Layer 1, named bls.rs as the Layer-3 pure parser, and asserted the declaration mechanism without any enforcement. The shipped code differed on all of those points. A later revision reconciled the two: the shared boot-entry types were extracted into boot_types.rs, select_default_entry moved to the policy layer, every module got its //! Layer: declaration, the map was corrected to match reality (fs.rs is Layer 2; bls_parse.rs is the pure parser; bls.rs is a Layer-4 coordinator), and the whole thing was put under a CI gate. The graph is now a verified acyclic DAG.

The eight layers

The map of every module to its layer lives in tools/layer-map.toml. The layers, low to high:

Layer 0: Platform Introspection

Pure-read discovery of the environment. No side effects, no trust decisions. acpi, hypervisor, smbios, fw_cfg, fw_cfg_config, secure (Secure Boot state query), input (raw key/pointer event source).

Layer 1: Firmware Boundary

Direct UEFI protocol access that carries no policy, parsing, or UI. security_override (Security2/SecurityArch hooks), tpm (TCG2 measured boot), firmware_quirks.

Layer 2: Storage & Filesystems

A filesystem-agnostic read API plus the write path. Consumers above this layer read FAT, ext4, Btrfs, or LVM through one uniform interface. fs_types, fs_backend (the FsBackend trait), the backend family (fs_backend_fat, _ext4, _btrfs, _lvm, _lvm_btrfs, _lvm_dispatch), fs (the coordinator that dispatches to the right backend per volume), fs_writer (the ESP write path), and initrd (the LoadFile2 provider, which reads via fs).

Layer 3: Parsers & Shared Types

Pure parsers (bytes in, structured data out) and the structured types they yield. No I/O, no firmware calls, no state. bls_parse (the pure BLS parser), pe_loader_pure, discovery_pure, boot_types (the shared BootEntry/EntryKind/Icon plus preflight result types), uki, and the I/O shell pe_loader (over pe_loader_pure).

Layer 4: Policy & State

Config-driven decisions and persistent state. policy (parse plus apply policy.toml, including select_default_entry), autodiscovery, preflight, health (NVRAM state machine), partitions (GPT/XBOOTLDR discovery), drivers (policy-gated legacy FS-driver loader), and bls (the BLS discovery coordinator: an I/O shell over the Layer-3 bls_parse that drives the boot counter and autodiscovery, which is why it sits here rather than at Layer 3).

Layer 5: Trust & Audit

Append-only records of decisions. The audit modules (trust_log, trust_log_pure, telemetry, diag, version) are cross-cutting: any layer may write to them, but nothing reads their state to make a decision. They are exempt from the direction rule as dependency targets. report and bootlog are the non-cross-cutting Layer-5 emitters.

Layer 6: Presentation

Everything the user sees or types. gui (GOP double-buffered menu), console (serial/text fallback).

Layer 7: Orchestration

The conductor. Assembles the boot flow from the layers below. Nothing depends on Layer 7. main (the 10-phase boot flow), boot (chainload / UKI / native-PE / firmware-LoadImage dispatch), discovery (cross-backend entry aggregation; an I/O shell over discovery_pure).

Dependency rules (normative, enforced)

  1. A module may use from its own layer or any lower layer.
  2. A module must not use from a higher layer.
  3. Cross-cutting modules (diag, version, telemetry, trust_log, trust_log_pure) may be used from any layer. They are written-to and observed from above, and are never read as control state. They are tagged //! Layer: N (cross-cutting).
  4. Pure pairs: a pure half and its I/O shell at the same layer (pe_loader_pure with pe_loader, trust_log_pure with trust_log, discovery_pure with discovery) may reference each other. The pure half is tagged //! Layer: N (pure). (bls_parse with bls is a different case: bls.rs is genuinely Layer 4, so bls → bls_parse is an ordinary downward edge.)
  5. Layer 0 modules must import only from Layer 0.

These rules are checked by tools/check-layers.py against tools/layer-map.toml on every commit (pre-commit hook) and every push/PR (CI). The module dependency graph is verified acyclic by check-layers.py --graph.

Introducing new code: where does it go?

Decision tree for any new module:

  1. Does it touch UEFI protocols directly? Layer 1.
  2. Does it parse bytes without doing I/O, or define a shared data type? Layer 3.
  3. Does it read/write files via the FS-agnostic API? Layer 2.
  4. Does it make a decision based on config plus discovered state? Layer 4.
  5. Does it record a decision for audit? Write to a Layer-5 cross-cutting module via the trust log; ship a new module only when the record shape is structurally new.
  6. Does it draw pixels or read keystrokes? Layer 6 (presentation) or Layer 0 (raw input source).
  7. Does it schedule the boot phases? Layer 7 (main).

Then: add the module to tools/layer-map.toml, add its //! Layer: N doc comment, and run python3 tools/check-layers.py. The gate tells you immediately if a dependency points the wrong way.

Constraints the gate (and reviewers) enforce

The gate keeps the architecture clean by requiring that:

  • UEFI protocol calls live in Layer 1.
  • Every module stays within a single layer; a "utility" or "helper" module that crosses layers is rejected (also forbidden by the naming rules below).
  • Layer-5 audit modules are written to and observed, and their state is never queried to make a decision.
  • Layer-4 policy code reaches UEFI variables through Layer 0/1 rather than reading them directly.
  • Boot-phase-sequencing logic lives in main.
  • Every module carries a //! Layer: declaration (the gate fails without it).
  • An abstraction ships with at least two implementations, or with a second one imminent in the same PR series.

File naming conventions

  • Names carry meaning: avoid generic suffixes such as -manager, -helper, -utility, -common, -core (the one exception is lamboot-core/, the crate).
  • Domain-specific verbs in function names.
  • Each module owns one responsibility; helper modules are folded into the module they serve.
  • One responsibility per module; split if a module grows past ~500 lines and the responsibilities are separable.

Current module counts and layer totals

46 modules, about 16,000 lines of Rust (lamboot-core). By layer:

LayerNameModules
0Platform Introspectionacpi, hypervisor, smbios, fw_cfg, fw_cfg_config, secure, input
1Firmware Boundarysecurity_override, tpm, firmware_quirks
2Storage & Filesystemsfs_types, fs_backend, fs_backend_{fat,ext4,btrfs,lvm,lvm_btrfs,lvm_dispatch}, fs, fs_writer, initrd
3Parsers & Shared Typesbls_parse, pe_loader_pure, discovery_pure, boot_types, uki, pe_loader
4Policy & Statepolicy, autodiscovery, preflight, health, partitions, drivers, bls
5Trust & Audittrust_log, trust_log_pure, report, bootlog, telemetry, diag, version
6Presentationgui, console
7Orchestrationboot, discovery, main

LamBoot is a medium-sized codebase by bootloader standards (GRUB ~40kLOC of C, systemd-boot ~10kLOC of C, rEFInd ~30kLOC of C). It is smaller than all of them while reading filesystems natively in a way the small ones do not. That compactness is a deliberate property of the design.


This document is normative and machine-enforced. New modules must declare their layer in their module-level doc comment (//! Layer: N — <name>.) and appear in tools/layer-map.toml. tools/check-layers.py checks the declaration and the dependency direction first, in the pre-commit hook and in CI.

See Also