grub

package module
v0.0.0-...-ae808db Latest Latest
Warning

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

Go to latest
Published: Jun 22, 2026 License: BSD-3-Clause Imports: 16 Imported by: 0

README

grub

ci Go Reference

Pure-Go (CGO=0) GRUB administration toolkit for disk images. It is a production consumer of the storage / firmware / boot stack:

  • go-volumes/gpt locates the EFI System Partition and the Linux /boot (or root) partition inside a GPT disk image.
  • go-filesystems/detect probes each partition's filesystem type, then grub mounts it read/write behind the common filesystem.Filesystem API through the matching in-place driver — fat32 for the ESP and ext4 or btrfs for /boot. So grub.cfg is edited as a real file, not by raw byte surgery on the image, on both the ESP and a Debian-style /boot.
  • go-filesystems/uefi registers a GRUB UEFI boot entry (Boot#### / BootOrder).
  • go-tpm2/efitcg2 measures the grub.cfg and the kernels/initrds it references into the conventional TPM PCRs for measured boot — optional, behind an injected firmware Caller, so the tool stays TPM-free when no TPM is present.

Module

github.com/go-bootloaders/grub

What it does

Capability ESP entry point /boot (ext4/btrfs) entry point
Open + mount ESP and /boot OpenImage(path) (*Image, error) same call; Boot(), BootType()
Read / locate grub.cfg (*Image).ReadGrubCfg, LocateGrubCfg (*Image).ReadGrubCfgOnBoot
De-quiet / add consoles (*Image).PatchQuiet, PatchQuietImage (*Image).PatchQuietOnBoot
Generate a grub.cfg (*Image).MkConfig, GenerateConfig (*Image).MkConfigOnBoot
Discover kernels/initrds DiscoverKernels(im.ESP()) DiscoverKernels(im.Boot())
UEFI boot entry BuildBootEntry, RegisterBootEntry
Measured boot (TPM) NewMeasurer, (*Image).MeasureBoot (*Image).MeasureBootOnBoot
BIOS boot-code install InstallToSectors, InstallMBR

PatchQuietImage is a one-call convenience that patches both the ESP and a mounted /boot grub.cfg. The /boot filesystem is mounted in place by the ext4/btrfs driver, so WriteFile (and therefore PatchQuietOnBoot / MkConfigOnBoot) persists back to the partition — full read and write, not the read-only temp-file staging the detect adapter would give.

All grub.cfg editing is performed through the mounted filesystem's ReadFile / WriteFile. There is no same-length raw-byte patching of the disk image; the previous quiet→spaces hack is gone. (RawReplaceAll survives only as a documented, deliberately-fenced low-level fallback for raw, filesystem-less regions such as a string baked into core.img.)

Usage

Strip quiet/splash, add serial consoles
changed, err := grub.PatchQuietImage("disk.img")
// removes quiet/splash and ensures console=tty0 console=hvc0 on every
// linux line of the ESP's grub.cfg, persisting through the FAT32 driver.
Generate a fresh grub.cfg from on-disk kernels
im, err := grub.OpenImage("disk.img")
defer im.Close()

cfgPath, n, err := im.MkConfig(grub.MkConfigOptions{
    Distributor: "Debian GNU/Linux",
    Cmdline:     "ro console=tty0 console=hvc0",
})
// scans /boot (and the ESP) for vmlinuz*/initrd*, emits proper
// set/insmod headers + one menuentry{linux,initrd} block per kernel,
// newest first, and writes it to the ESP.
Register a UEFI boot entry
store, _ := uefi.Open("OVMF_VARS.fd")
defer store.Close()

lo := grub.BuildBootEntry("GRUB", 1, partGUID, esp, grub.DefaultGrubLoaderPath)
num, err := grub.RegisterBootEntry(store, lo) // appends to BootOrder
Measure boot into the TPM (optional)
m := grub.NewMeasurer(firmwareCaller) // efitcg2.Caller; omit when no TPM
count, err := im.MeasureBoot(m)
// grub.cfg -> PCR 8, each kernel/initrd -> PCR 9 (GRUB TCG conventions).

Architecture support

Validated on all six 64-bit Go targets: amd64, arm64, riscv64, loong64, ppc64le, and big-endian s390x (the byte-order canary for the GPT / FAT32 / UEFI decoders). Native runners run the full race + coverage suite; the four non-native arches run the cross-compiled test binaries under QEMU.

/boot mounting (ext4 / btrfs)

OpenImage mounts the Linux /boot (or root) partition read/write when one is present, alongside the ESP. The partition's filesystem type is probed with go-filesystems/detect and dispatched to the matching in-place driver (ext4.Open or btrfs.Open); both expose a real WriteFile, so grub.cfg discovery, patching, regeneration and TPM measurement all work against /boot/grub/grub.cfg (and /boot/grub2/grub.cfg, /grub/grub.cfg) exactly as they do against the ESP:

im, _ := grub.OpenImage("disk.img")
defer im.Close()

if im.HasBoot() {                      // a Linux partition was mounted
    fmt.Println(im.BootType())         // detect.Ext4 or detect.Btrfs
    cfg, content, _ := im.ReadGrubCfgOnBoot()
    kernels, _ := grub.DiscoverKernels(im.Boot())
    im.PatchQuietOnBoot()              // persists to the ext4/btrfs partition
    im.MkConfigOnBoot(grub.MkConfigOptions{Distributor: "Debian"})
}

A Linux partition holding an unsupported filesystem (neither ext4 nor btrfs) is reported as ErrUnsupportedBootFS rather than silently skipped; an image with no Linux partition simply has no /boot mount (HasBoot() is false, the *OnBoot methods return ErrNoBoot). All deps resolve cleanly from the public Go proxy by pseudo-version — no replace => ../sibling, no vendoring.

License

BSD-3-Clause — see LICENSE. Copyright (c) 2026, the go-bootloaders/grub authors.

Documentation

Overview

Package grub is a pure-Go (CGO=0) GRUB administration toolkit for disk images. It is a production consumer of the go-volumes / go-filesystems / go-tpm2 stack:

  • go-volumes/gpt locates the EFI System Partition and the Linux /boot (or root) partition inside a GPT disk image.
  • go-filesystems/detect probes each partition's filesystem type; grub then mounts it writably through the matching concrete driver (go-filesystems/fat32 for the ESP, go-filesystems/ext4 or go-filesystems/btrfs for /boot) behind the common filesystem.Filesystem interface.
  • go-filesystems/uefi registers a GRUB UEFI boot entry (Boot####/BootOrder).
  • go-tpm2/efitcg2 measures the grub.cfg + referenced kernel/initrd into the TPM PCRs for measured boot (optional, behind an injected Caller).

All grub.cfg editing is performed through the mounted filesystem's ReadFile/WriteFile — there is no raw same-length byte patching of the disk image. (RawReplaceAll survives only as a documented, deliberately-fenced low-level fallback; see raw.go.)

Both the ESP and the /boot filesystem are opened in place via each driver's path+partition-index entry point (fat32.Open / ext4.Open / btrfs.Open), so WriteFile changes persist back to the partition's region of the disk image. The drivers expose a real WriteFile, so grub.cfg regeneration (MkConfig) and in-place patching (PatchQuiet) work against the /boot ext4/btrfs filesystem exactly as they do against the FAT32 ESP — no read-only temp-file staging.

Index

Constants

View Source
const (
	PCRLoadedImage = 4
	PCRGrubConfig  = 8
	PCRGrubFiles   = 9
)

Conventional TPM PCR indices for GRUB measured boot, per the GRUB TCG spec and the EFI boot measurement conventions:

  • PCR 4: the loaded boot image (the GRUB EFI binary / kernel image).
  • PCR 8: GRUB's configuration and typed commands (grub.cfg contents).
  • PCR 9: files GRUB loads on behalf of the OS (kernel, initrd).
View Source
const DefaultGrubLoaderPath = `\EFI\debian\grubx64.efi`

DefaultGrubLoaderPath is the conventional ESP path of the GRUB UEFI image.

Variables

View Source
var ErrNoBoot = errors.New("grub: no /boot filesystem mounted")

ErrNoBoot is returned by the *OnBoot methods when no Linux /boot filesystem is mounted (the image has no Linux partition).

View Source
var ErrNoESP = errors.New("grub: no EFI System Partition found in image")

ErrNoESP is returned when the image has no EFI System Partition.

View Source
var ErrNoGrubCfg = errors.New("grub: no grub.cfg found on filesystem")

ErrNoGrubCfg is returned when no grub.cfg can be located on a mounted FS.

View Source
var ErrUnsupportedBootFS = errors.New("grub: unsupported /boot filesystem type")

ErrUnsupportedBootFS is returned (wrapped) when the Linux partition exists but holds a filesystem grub cannot mount for /boot administration (i.e. not ext4 or btrfs).

Functions

func BuildBootEntry

func BuildBootEntry(description string, partNumber uint32, partGUID [16]byte, part gpt.Partition, loaderPath string) *uefi.LoadOption

BuildBootEntry constructs the UEFI EFI_LOAD_OPTION for a GRUB loader living on the given GPT partition. The device path is

HD(<partno>,GPT,<part-guid>,<startLBA>,<sizeLBA>)/File(<loaderPath>)

matching what firmware writes for a GRUB install. loaderPath is a backslash-separated EFI path such as DefaultGrubLoaderPath. partNumber is the 1-based GPT partition index; partGUID is the partition's unique GUID (raw 16 bytes, GPT mixed-endian as stored on disk).

func CmdlineRemoveWord

func CmdlineRemoveWord(cmdline, word string) string

CmdlineRemoveWord removes a single word from a kernel command line. It preserves the order of other tokens and returns the original string unchanged if the word is not present.

func FindBootEntry

func FindBootEntry(store uefi.VariableStore, description string) (num uint16, ok bool, err error)

FindBootEntry returns the Boot#### number of an existing entry whose description matches description, or ok=false when none is present. It lets callers detect (and avoid duplicating) a previously-registered GRUB entry.

func GenerateConfig

func GenerateConfig(kernels []Kernel, opts MkConfigOptions) string

GenerateConfig renders a grub.cfg from a list of kernels. The output is a complete, valid GRUB configuration: a header (set default/timeout, load the needed modules), one menuentry per kernel with a linux + initrd line, and proper quoting. It does not touch the filesystem.

func InstallMBR

func InstallMBR(imagePath string, code []byte) error

InstallMBR writes a minimal MBR boot sector. `code` must be at most 446 bytes (the MBR boot-code area). The function ensures the boot signature 0x55AA is set at offsets 510-511. Like InstallToSectors this targets a raw, filesystem-free region (sector 0) and is a legitimate boot-code install.

func InstallToSectors

func InstallToSectors(imagePath string, code []byte, startSector, numSectors int64) error

InstallToSectors writes `code` into `numSectors` sectors starting at `startSector` (0-based). This is the legitimate BIOS boot-code install path: GRUB's core.img / boot.img live in the post-MBR gap (the sectors between the MBR and the first partition) on a BIOS/GPT-hybrid layout, which is a raw region with no filesystem. It returns an error if `code` is larger than the provided sector range.

func LocateGrubCfg

func LocateGrubCfg(fs filesystem.Filesystem) (string, error)

LocateGrubCfg returns the path of the grub.cfg on the given filesystem, trying the conventional locations and then any /EFI/<vendor>/grub.cfg. It returns ErrNoGrubCfg if none is present.

func PatchGrubCfgContent

func PatchGrubCfgContent(input string) string

PatchGrubCfgContent edits a single grub.cfg "linux" or "linux16" line to remove quiet/splash tokens and ensure console arguments are present. If no linux line is found the input is returned unchanged. Indentation is preserved and existing console arguments are not duplicated.

func PatchGrubDefaultsContent

func PatchGrubDefaultsContent(input string) string

PatchGrubDefaultsContent ensures `/etc/default/grub` contains the desired terminal and kernel cmdline settings. Keys are overridden if present or appended if missing.

func PatchQuietImage

func PatchQuietImage(imagePath string) (bool, error)

PatchQuietImage is the convenience wrapper matching the historical package-level entry point: open the image, FS-patch its grub.cfg, close. It patches the ESP grub.cfg, then the /boot grub.cfg when one is mounted, so a single call covers both the ESP-resident and the Debian-style /boot-resident configuration. It reports changed=true if either was modified. If neither the ESP nor /boot carries a grub.cfg at all it returns ErrNoGrubCfg (matching the historical single-target behaviour); a grub.cfg present on only one side is patched and the missing side is not an error.

func RawReplaceAll

func RawReplaceAll(f *os.File, from, to []byte)

RawReplaceAll scans f in overlapping 1 MiB chunks and replaces every occurrence of from with to (which MUST be the same length) in-place.

DEPRECATED / LOW-LEVEL FALLBACK. This is NOT how grub.cfg should be edited: the production path is OpenImage -> PatchQuiet (filesystem-based ReadFile/ WriteFile through the mounted ESP), which can shorten the kernel command line correctly instead of padding it with spaces. RawReplaceAll is retained only for the rare case of patching a raw, filesystem-less region (e.g. a string baked into core.img) where no driver can mount the bytes. Prefer the FS path for anything residing in a real filesystem.

func ReadGrubCfgFS

func ReadGrubCfgFS(fs filesystem.Filesystem) (cfgPath string, content string, err error)

ReadGrubCfgFS locates and reads the grub.cfg on an arbitrary mounted filesystem (the ESP or /boot). It is the shared core behind ReadGrubCfg and ReadGrubCfgOnBoot.

func RegisterBootEntry

func RegisterBootEntry(store uefi.VariableStore, lo *uefi.LoadOption) (uint16, error)

RegisterBootEntry writes a GRUB boot entry into the given UEFI variable store and appends it to BootOrder, returning the assigned Boot#### number. The store is typically obtained via uefi.Open(<varstore.fd>); the grub package stays decoupled from how the store is materialised (an OVMF NvVar region, a firmware-backed store, etc.). It does not mount or modify the disk image.

Types

type Image

type Image struct {
	// contains filtered or unexported fields
}

Image is a mounted GRUB-administration handle over a GPT disk image. It owns the opened backing file, the mounted ESP filesystem, and — when present and of a supported type — the mounted Linux /boot filesystem; call Close to release them all. The /boot filesystem is mounted by OpenImage when a Linux partition is present and its filesystem (ext4 or btrfs) resolves cleanly.

func OpenImage

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

OpenImage opens a GPT disk image read/write, locates the EFI System Partition, and mounts its FAT32 filesystem for read/write. The returned Image exposes the mounted ESP via ESP(); callers must Close it.

The ESP is mounted writably through the go-filesystems/fat32 driver, which opens the partition in place inside the disk image (partition selected by its GPT index) so WriteFile changes persist to the underlying image. The FS type is first verified as FAT32 via go-filesystems/detect; a non-FAT32 ESP is rejected rather than mis-mounted.

The Linux /boot partition (LinuxFilesystemGUID), when present, is mounted writably too: its filesystem type is probed via go-filesystems/detect and dispatched to the matching in-place driver (ext4.Open or btrfs.Open). A Linux partition holding an unsupported filesystem (neither ext4 nor btrfs) is reported as a hard error rather than silently skipped. An image with no Linux partition at all simply has no /boot mount (Boot() returns nil); this is the normal ESP-only case and not an error.

func (*Image) Boot

func (im *Image) Boot() filesystem.Filesystem

Boot returns the mounted Linux /boot filesystem, or nil when the image has no Linux partition. Use HasBoot to disambiguate a missing /boot from a nil check, and BootType to learn which driver (ext4/btrfs) backs it.

func (*Image) BootPartition

func (im *Image) BootPartition() (gpt.Partition, bool, error)

BootPartition returns the GPT geometry of the Linux /boot partition and whether one is present. The partition is already mounted by OpenImage when of a supported type; this exposes its geometry for callers that need it.

func (*Image) BootType

func (im *Image) BootType() detect.Type

BootType reports the detected filesystem type of the mounted /boot (detect.Ext4 or detect.Btrfs), or the empty Type when none is mounted.

func (*Image) Close

func (im *Image) Close() error

Close releases the mounted ESP and /boot filesystems, flushing any pending writes back to the disk image. Both are closed even if the first errors; the first error encountered is returned.

func (*Image) ESP

func (im *Image) ESP() filesystem.Filesystem

ESP returns the mounted EFI System Partition filesystem.

func (*Image) ESPPartition

func (im *Image) ESPPartition() gpt.Partition

ESPPartition returns the GPT geometry of the EFI System Partition.

func (*Image) HasBoot

func (im *Image) HasBoot() bool

HasBoot reports whether a Linux /boot filesystem was mounted.

func (*Image) MeasureBoot

func (im *Image) MeasureBoot(m *Measurer) (int, error)

MeasureBoot measures a complete GRUB boot into the conventional PCRs from a mounted Image: the ESP grub.cfg into PCR 8, and every discovered kernel/initrd into PCR 9. It is the convenience entry point that ties OpenImage to the TPM. Returns the number of measurements made.

func (*Image) MeasureBootOnBoot

func (im *Image) MeasureBootOnBoot(m *Measurer) (int, error)

MeasureBootOnBoot measures the grub.cfg + kernels/initrds resident on the mounted /boot filesystem (ext4/btrfs) into the conventional PCRs. It returns ErrNoBoot when no /boot is mounted.

func (*Image) MkConfig

func (im *Image) MkConfig(opts MkConfigOptions) (cfgPath string, entries int, err error)

MkConfig discovers the kernels on the mounted ESP, generates a grub.cfg, and writes it to the conventional ESP location (/grub/grub.cfg, or the path of an existing grub.cfg if one is already present). It returns the path written and the number of menu entries emitted.

This replaces the former no-op stub: it produces a real, valid configuration from on-disk kernels rather than returning nil.

func (*Image) MkConfigOnBoot

func (im *Image) MkConfigOnBoot(opts MkConfigOptions) (cfgPath string, entries int, err error)

MkConfigOnBoot is MkConfig against the mounted /boot filesystem (ext4/btrfs): it scans /boot for vmlinuz*/initrd* kernels and writes the generated grub.cfg to /boot/grub/grub.cfg (or an existing config path). It returns ErrNoBoot when no /boot is mounted. The ext4/btrfs drivers persist the write to the disk image.

func (*Image) PatchQuiet

func (im *Image) PatchQuiet() (changed bool, err error)

PatchQuiet removes the "quiet"/"splash" kernel flags and ensures serial console arguments on every linux line of the grub.cfg, editing the file through the mounted ESP filesystem (NOT raw disk bytes). It locates the grub.cfg, applies PatchGrubCfgContent, and writes it back. If the content is unchanged the file is left untouched.

This replaces the old same-length raw-byte hack: the rewritten cmdline is shorter than the original (quiet/splash removed), which the FAT32 driver handles by re-truncating the file — something raw patching could never do without padding spaces into the kernel command line.

func (*Image) PatchQuietOnBoot

func (im *Image) PatchQuietOnBoot() (changed bool, err error)

PatchQuietOnBoot is PatchQuiet against the mounted /boot filesystem's grub.cfg (ext4/btrfs). It returns ErrNoBoot when no /boot is mounted. Like PatchQuiet, the rewritten (shorter) cmdline is truncated in place by the driver, and the change persists to the disk image.

func (*Image) Path

func (im *Image) Path() string

Path reports the backing image path.

func (*Image) ReadGrubCfg

func (im *Image) ReadGrubCfg() (cfgPath string, content string, err error)

ReadGrubCfg locates and reads the grub.cfg on the ESP.

func (*Image) ReadGrubCfgOnBoot

func (im *Image) ReadGrubCfgOnBoot() (cfgPath string, content string, err error)

ReadGrubCfgOnBoot locates and reads the grub.cfg on the mounted /boot filesystem (where Debian/Fedora/etc. keep /boot/grub/grub.cfg or /boot/grub2/grub.cfg). It returns ErrNoBoot when no /boot is mounted.

func (*Image) Size

func (im *Image) Size() int64

Size reports the backing image size in bytes.

func (*Image) WriteGrubCfg

func (im *Image) WriteGrubCfg(cfgPath, content string) error

WriteGrubCfg writes content to the given grub.cfg path on the ESP.

func (*Image) WriteGrubCfgOnBoot

func (im *Image) WriteGrubCfgOnBoot(cfgPath, content string) error

WriteGrubCfgOnBoot writes content to cfgPath on the mounted /boot filesystem. It returns ErrNoBoot when no /boot is mounted. The ext4/btrfs drivers expose a real WriteFile, so the change persists to the disk image.

type Kernel

type Kernel struct {
	// Version is the release string parsed from the vmlinuz name, e.g.
	// "6.1.0-18-amd64" from "vmlinuz-6.1.0-18-amd64". Empty for an
	// un-versioned "vmlinuz".
	Version string
	// KernelPath is the absolute path of the kernel image on the FS.
	KernelPath string
	// InitrdPath is the absolute path of the matching initrd/initramfs, or
	// empty when none was found.
	InitrdPath string
}

Kernel is a discovered (kernel, initrd) pair on a mounted filesystem.

func DiscoverKernels

func DiscoverKernels(fs filesystem.Filesystem) ([]Kernel, error)

DiscoverKernels scans the conventional directories of a mounted filesystem for kernel images and their matching initrds. Results are sorted by version descending (newest first) so the default entry is the newest kernel.

type Measurer

type Measurer struct {
	// contains filtered or unexported fields
}

Measurer performs GRUB measured-boot extensions through an injected efitcg2.Caller (the firmware TPM2 protocol). Constructing it without a real TPM is a no-op-free design: the tool stays entirely TPM-free unless a Caller is supplied, mirroring efitcg2's own injection seam.

func NewMeasurer

func NewMeasurer(caller efitcg2.Caller) *Measurer

NewMeasurer wraps an efitcg2.Caller (the firmware-call mechanism the loader implements; in tests a fake Caller) into a Measurer.

func (*Measurer) MeasureConfig

func (m *Measurer) MeasureConfig(cfg []byte) error

MeasureConfig extends PCR 8 with the grub.cfg contents (EV_IPL), exactly as GRUB measures its configuration before acting on it.

func (*Measurer) MeasureFile

func (m *Measurer) MeasureFile(path string, data []byte) error

MeasureFile extends PCR 9 with a file GRUB loads (kernel or initrd), tagging the event with the file's path as its description.

func (*Measurer) MeasureLoadedImage

func (m *Measurer) MeasureLoadedImage(desc string, image []byte) error

MeasureLoadedImage extends PCR 4 with a loaded boot image (the kernel or the GRUB EFI binary itself), using the EFI boot-services-application event type.

type MkConfigOptions

type MkConfigOptions struct {
	// Default is the index of the default menu entry. Defaults to 0.
	Default int
	// Timeout is the menu timeout in seconds. Defaults to 5.
	Timeout int
	// Cmdline is appended to every linux line (after the root= argument the
	// caller bakes in). Typical value: "ro console=tty0 console=hvc0".
	Cmdline string
	// Distributor labels the menu entries, e.g. "Debian GNU/Linux".
	Distributor string
}

MkConfigOptions tunes generated grub.cfg output.

Jump to

Keyboard shortcuts

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