systemdboot

package module
v0.0.0-...-5b7cf0e Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: BSD-3-Clause Imports: 17 Imported by: 1

README

go-bootloaders/systemd-boot

systemd-boot

ci Go Reference

Pure-Go (CGO_ENABLED=0) reader and integration layer for systemd-boot, following the Boot Loader Specification.

It is a production-ready consumer of the pure-Go storage / firmware / boot stack: point it at a whole disk image and it GPT-locates the EFI System Partition, mounts its FAT32 filesystem, and returns resolved BLS boot entries — then optionally reads the UEFI boot-variable state and measures the selected kernel/initrd/cmdline into TPM PCRs, all with no cgo and no shell-outs.

What it does

Capability Backed by
Parse /loader/loader.conf + /loader/entries/*.conf → resolved Boot entries (linux/initrd/options, BLS sort order, wildcard default) this package
Disk image → ESP → resolved entries with zero manual FS plumbing go-volumes/gpt + go-filesystems/detect (fat32reg)
Read UEFI BootOrder / Boot#### LoadOptions (device-path text), set BootNext / BootOrder, find which Boot#### is systemd-boot go-filesystems/uefi
Measured boot: extend kernel (PCR 4) / cmdline (PCR 8) / initrd (PCR 9) into a TPM + emit a TCG event log go-tpm2 (tpm2, attest, common)

Module

github.com/go-bootloaders/systemd-boot

Disk-image → BLS entries

OpenImage does the whole pipeline — GPT-locate the ESP, detect + mount its FAT32 filesystem, adapt it to the reader, parse loader.conf — behind a single call. The caller MUST Close the returned Image.

im, err := systemdboot.OpenImage("/dev/sda")     // or a disk-image file
if err != nil { /* ... */ }
defer im.Close()

def, _ := im.Default()        // the active entry (loader.conf default, or BLS-highest)
all, _ := im.Entries()        // every entry, newest first
// def.LinuxPath  = "/EFI/nixos/<hash>-linux.efi"
// def.InitrdPath = "/EFI/nixos/<hash>-initrd"
// def.Options    = "init=/nix/store/… root=/dev/disk/by-label/nixos"

The Image.FS field is the mounted ESP adapted to the minimal FS interface (ReadFile + ListDir). Callers who already hold a mounted ESP filesystem (any go-filesystems driver, i.e. a filesystem.Filesystem) skip the disk plumbing with NewFS:

fs := systemdboot.NewFS(mountedESP)          // filesystem.Filesystem → FS
boot, _ := systemdboot.Default(fs, "/")      // espRoot within the mount

NewFS is the adapter that bridges the driver's ListDir ([]filesystem.DirEntry) to the reader's ListDir ([]string).

UEFI boot-variable state

systemd-boot installs itself as a UEFI Boot#### LoadOption; the BLS entries it then presents are a separate stage. ReadBootState reads the firmware side from a variable store (a real OVMF_VARS.fd, or one minted with go-filesystems/uefi):

store, _ := uefi.Open("OVMF_VARS.fd")
defer store.Close()

state, _ := systemdboot.ReadBootState(store)
// state.Order              — the firmware BootOrder
// state.Entries[n]         — Boot####n: Description, DevicePathText, Active, …
// state.SystemdBootSlots   — which Boot#### point at systemd-boot
// state.BootNext           — one-shot next boot, if set

systemdboot.SetBootNext(store, state.SystemdBootSlots[0])  // boot sd-boot once
systemdboot.MakeSystemdBootFirst(store)                    // make it default

An entry is recognised as systemd-boot when its device path's File() node is \EFI\systemd\systemd-boot{x64,aa64}.efi or the removable-media fallback \EFI\BOOT\BOOT{X64,AA64}.EFI.

Measured boot (optional, TPM-free by default)

Given a selected entry and the mounted ESP, MeasureBoot reads the kernel and initrd from the ESP, hashes them and the command line, extends them into the conventional systemd PCR registry indices, and returns a TCG event log:

// nil Extender → pure event-log dry run, no TPM touched.
res, _ := systemdboot.MeasureBoot(im, def, nil, 512<<20)

// or measure into hardware over a crb/tis transport:
tpm := tpm2.New(transport)
res, _ := systemdboot.MeasureBoot(im, def, tpm, 512<<20)
// res.Measurements — PCR 4 kernel, PCR 8 cmdline, PCR 9 initrd (digest + event type)
// res.EventLog     — serialised TCG event log (parse with attest.ParseEventLog)

The TPM dependency sits behind the injected Extender interface (PCRExtend(pcr int, hash uint16, digest []byte) error, satisfied by *tpm2.TPM), so the tool builds and runs with no TPM present.

Why this exists

The conventional /boot/{vmlinuz,Image}-* glob misses every systemd-boot install — NixOS, Clear Linux, and bespoke Arch / Gentoo setups put the kernel at content-addressed /EFI/<distro>/<hash>-* paths selected through /loader/entries/*.conf. Reading the canonical BLS path covers the whole ecosystem at once, and wiring it to the storage / firmware / TPM stack turns it into a complete, pure-Go systemd-boot inspector for the cloud-boot / weft toolchain.

Building

GOWORK=off go build ./...

All dependencies, including go-filesystems/interface, resolve from the public Go module proxy by pseudo-version — no replace => ../sibling, no local sibling clone, and no vendoring required.

Validated on all six 64-bit Go architectures (amd64 / arm64 / riscv64 / loong64 / ppc64le / s390x, the last big-endian).

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package systemdboot is a pure-Go reader for systemd-boot configurations following the Boot Loader Specification: https://uapi-group.org/specifications/specs/boot_loader_specification/

The package solves kernel discovery for Linux installs that ship the kernel + initrd at content-addressed paths in /EFI/<distro>/<hash>-* (NixOS, Clear Linux, Fedora CoreOS w/ systemd-boot, custom Arch/Gentoo setups). The conventional /boot/{vmlinuz,Image}-* glob doesn't find them; this package reads /loader/loader.conf + /loader/entries/*.conf and returns the resolved linux/initrd paths.

The core BLS reader does no I/O of its own — callers pass an FS abstraction that any of go-filesystems' concrete readers satisfies via Go's structural typing.

On top of the reader, this package is a production consumer of the pure-Go storage / firmware / boot stack:

  • OpenImage (image.go) GPT-locates the EFI System Partition with go-volumes/gpt, mounts its FAT32 filesystem with go-filesystems/detect, adapts it to FS, and returns resolved BLS entries.
  • ReadBootState and the BootNext/BootOrder helpers (uefi.go) read and manage the UEFI boot variables via go-filesystems/uefi.
  • MeasureBoot (measure.go) measures the selected kernel/initrd/cmdline into the conventional TPM PCRs via go-tpm2, behind an injected Extender so the package stays TPM-free when no TPM is present.

Index

Constants

View Source
const (
	// PCRKernel is the PCR into which the kernel image is measured (the
	// boot-loader / loaded-image PCR).
	PCRKernel = 4
	// PCRCmdline is the PCR into which the kernel command line is measured.
	PCRCmdline = 8
	// PCRInitrd is the PCR into which the initrd is measured.
	PCRInitrd = 9
	// PCRUKI is the PCR into which unified-kernel-image (sd-stub) sections
	// are measured.
	PCRUKI = 11
)

Conventional PCR indices used by the systemd measured-boot chain. They are literals here because the go-tpm2 libraries deliberately do not encode the systemd registry — the mapping is policy, supplied by the consumer.

Variables

View Source
var ErrNotFound = fmt.Errorf("systemd-boot: not found")

ErrNotFound is returned when the requested configuration file doesn't exist on the FS.

Functions

func MakeSystemdBootFirst

func MakeSystemdBootFirst(store uefi.VariableStore) (uint16, error)

MakeSystemdBootFirst rewrites BootOrder so the first Boot#### that points at systemd-boot is evaluated first, preserving the relative order of the rest. Returns the chosen systemd-boot slot. Returns an error if no Boot#### points at systemd-boot.

func SetBootNext

func SetBootNext(store uefi.VariableStore, n uint16) error

SetBootNext sets the one-shot UEFI BootNext variable to n (the next boot, and only the next boot, will use Boot####=n). Thin pass-through to go-filesystems/uefi so callers don't import it directly for the common case.

func SetBootOrder

func SetBootOrder(store uefi.VariableStore, order []uint16) error

SetBootOrder replaces the UEFI BootOrder with the given sequence.

Types

type Boot

type Boot struct {
	// EntryID is the filename of the .conf entry without the
	// ".conf" suffix, e.g. "nixos-generation-42".
	EntryID string

	// Title is the human-readable name for the entry, e.g. "NixOS".
	Title string

	// Version sorts entries within the same sort-key group, e.g.
	// "Generation 42 NixOS 23.11.20240115.abc".
	Version string

	// SortKey groups entries across distros (systemd-boot sorts
	// entries with the same sort-key together, then by version).
	// Empty if not set in the entry file.
	SortKey string

	// LinuxPath is the linux= field — the kernel binary's path
	// relative to the ESP root, e.g. "/EFI/nixos/<hash>-linux.efi".
	LinuxPath string

	// InitrdPath is the initrd= field, similar.
	InitrdPath string

	// Options is the kernel command line, concatenated from any
	// options= lines in the entry file (BLS allows multiple).
	Options string

	// Raw retains the unparsed key/value pairs the parser saw — useful
	// for less-common keys (machine-id, devicetree, efi, architecture).
	Raw map[string]string
}

Boot is a resolved boot entry — the BLS .conf file's content with the most useful fields lifted out.

func All

func All(fs FS, espRoot string) ([]*Boot, error)

All returns every entry under <espRoot>/loader/entries/, sorted by BLS rules (sort-key descending, then version descending).

func Default

func Default(fs FS, espRoot string) (*Boot, error)

Default reads <espRoot>/loader/loader.conf and the entry it points at, returning the resolved Boot. If loader.conf's `default` is a wildcard (e.g. "nixos-generation-*"), the entry with the highest BLS sort order wins.

type BootEntry

type BootEntry struct {
	// Number is the Boot#### index (e.g. 0x0001 for Boot0001).
	Number uint16

	// Description is the human-readable label (UEFI "Description" field).
	Description string

	// DevicePathText is the device path rendered to the standard UEFI text
	// representation, e.g.
	// "HD(1,GPT,<guid>,…)/File(\EFI\systemd\systemd-bootx64.efi)".
	DevicePathText string

	// Active reflects the LOAD_OPTION_ACTIVE attribute bit.
	Active bool

	// InBootOrder is true if this entry's number appears in BootOrder.
	InBootOrder bool

	// IsSystemdBoot is true if the device path's File() segment matches one
	// of systemd-boot's canonical EFI paths.
	IsSystemdBoot bool
}

BootEntry is a UEFI Boot#### LoadOption lifted into a friendly shape, with the device path already rendered to text and the systemd-boot match pre-computed.

type BootState

type BootState struct {
	Order            []uint16
	Entries          map[uint16]*BootEntry
	BootNext         uint16
	BootNextSet      bool
	SystemdBootSlots []uint16
}

BootState is the firmware boot-variable picture: the ordered BootOrder, the resolved Boot#### entries (keyed by number), BootNext if set, and the numbers (in BootOrder sequence) that point at systemd-boot.

func ReadBootState

func ReadBootState(store uefi.VariableStore) (*BootState, error)

ReadBootState reads the full UEFI boot-variable state from store (a varstore opened/minted via go-filesystems/uefi). It lists every Boot#### LoadOption, renders device paths to text, cross-references BootOrder and BootNext, and flags entries that point at systemd-boot.

type Extender

type Extender interface {
	PCRExtend(pcr int, hash uint16, digest []byte) error
}

Extender is the minimal TPM surface the measured-boot helper needs: extend a digest into a PCR bank. *github.com/go-tpm2/tpm2.TPM satisfies it (PCRExtend(pcr int, hash uint16, digest []byte) error), so a caller passes a real TPM to measure into hardware, or a fake for dry-runs / tests. Keeping the dependency behind this interface is what lets the package build and run with no TPM present.

type FS

type FS interface {
	ReadFile(path string) ([]byte, error)
	ListDir(path string) ([]string, error)
}

FS is the minimal filesystem surface the parser needs. Any type matching this signature (in particular the concrete drivers from go-filesystems) satisfies it without explicit declaration.

func NewFS

func NewFS(fs filesystem.Filesystem) FS

NewFS adapts a go-filesystems driver (anything implementing filesystem.Filesystem — fat32, ext4, …) to the local FS interface so it can be passed to Default / All / LoadLoaderConf. Callers who already hold a mounted ESP filesystem use this instead of OpenImage.

type Image

type Image struct {
	// FS is the ESP's FAT32 filesystem adapted to the local FS interface.
	// Pass it (with EspRoot) to Default / All for resolved entries.
	FS FS

	// EspRoot is the path within FS at which the loader/ directory lives.
	// For a freshly mounted ESP this is "/" (the ESP root is the FS root).
	EspRoot string

	// Partition is the GPT EFI System Partition entry that was located.
	Partition gpt.Partition

	// LoaderConf is the parsed /loader/loader.conf (nil if absent).
	LoaderConf *LoaderConf
	// contains filtered or unexported fields
}

Image is a mounted disk image: an ESP filesystem adapted to the FS interface, the disk geometry that located it, and the parsed loader.conf. Close releases the underlying filesystem and any staged temp file.

func OpenImage

func OpenImage(diskPath string) (*Image, error)

OpenImage opens a whole disk image at path, locates its EFI System Partition via GPT, mounts the partition's FAT32 filesystem, and returns an Image whose FS yields resolved BLS boot entries. The caller MUST Close the returned Image.

loader.conf is parsed eagerly and exposed as Image.LoaderConf; its absence is not an error (an ESP may carry entries without a loader.conf), in which case LoaderConf is nil.

func (*Image) Close

func (im *Image) Close() error

Close releases the mounted filesystem and the opened disk file.

func (*Image) Default

func (im *Image) Default() (*Boot, error)

Default returns the active BLS entry (loader.conf's default, or the BLS-highest entry when no default is set).

func (*Image) Entries

func (im *Image) Entries() ([]*Boot, error)

Entries returns every BLS entry on the mounted ESP, BLS-sorted.

func (*Image) ReadESPFile

func (im *Image) ReadESPFile(espRelPath string, limit int64) ([]byte, error)

ReadESPFile reads a file from the mounted ESP, resolving an ESP-relative path (as found in a BLS linux=/initrd= field) against the ESP root. The returned slice is bounded by limit so an untrusted/corrupt FAT directory entry cannot make a caller process an unexpectedly huge file.

limit caps the returned bytes; pass a value comfortably above the largest kernel/initrd you expect (e.g. 512 MiB). A file larger than limit is an error rather than a silent truncation. A non-positive limit reads with no cap (use only for trusted images).

type LoaderConf

type LoaderConf struct {
	Default string // possibly with a "*" suffix wildcard
	Timeout string // string form, e.g. "5" or "menu-force"
	Raw     map[string]string
}

LoaderConf is the parsed /loader/loader.conf — currently only `default` and `timeout` are surfaced (the rest are UI knobs we don't care about as a non-interactive reader).

func LoadLoaderConf

func LoadLoaderConf(fs FS, espRoot string) (*LoaderConf, error)

LoadLoaderConf reads <espRoot>/loader/loader.conf and parses it. Returns ErrNotFound if the file is absent.

type MeasureResult

type MeasureResult struct {
	Measurements []Measurement
	EventLog     []byte
}

MeasureResult is the outcome of measuring a Boot entry: the ordered list of measurements performed and the serialised TCG event log describing them.

func MeasureBoot

func MeasureBoot(im *Image, entry *Boot, ext Extender, fileLimit int64) (*MeasureResult, error)

MeasureBoot measures the selected Boot entry into the conventional PCRs via ext and returns the measurements plus a TCG event log. The kernel and initrd are read from the mounted ESP (im); the command line is taken from the entry's Options. fileLimit bounds each file read (see Image.ReadESPFile).

ext may be nil for a pure event-log dry run (digests are still computed and logged, but nothing is extended into a TPM) — this is the TPM-free path. To measure into hardware, pass a *tpm2.TPM built over a crb/tis transport.

The digest of each measured object is SHA-256 over its bytes, which is the digest the event log records and what a SHA-256 PCR bank is extended with.

type Measurement

type Measurement struct {
	PCR         int
	Digest      [32]byte
	EventType   uint32
	Description string
}

Measurement is one PCR extension that was (or would be) performed: the PCR index, the SHA-256 digest extended into it, the TCG event type, and a human description of what was measured.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL