metropolis: implement and use A/B preloader
This switches over from using the EFI built-in bootloader for A/B
updates to using our own EFI preloader due to significant issues with
in-the-wild EFI implementations. It is a very minimal design relying
on a single Protobuf state file instead of EFI variables.
Change-Id: Ieebd0a8172ebe3f44c69b3e8c278c53d3fe2eeb4
Reviewed-on: https://review.monogon.dev/c/monogon/+/2203
Tested-by: Jenkins CI
Reviewed-by: Serge Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/build/mkimage/BUILD.bazel b/metropolis/node/build/mkimage/BUILD.bazel
index 2c7d699..ad88acb 100644
--- a/metropolis/node/build/mkimage/BUILD.bazel
+++ b/metropolis/node/build/mkimage/BUILD.bazel
@@ -3,6 +3,9 @@
go_library(
name = "mkimage_lib",
srcs = ["main.go"],
+ embedsrcs = [
+ "//metropolis/node/core/abloader", #keep
+ ],
importpath = "source.monogon.dev/metropolis/node/build/mkimage",
visibility = ["//visibility:private"],
deps = [
diff --git a/metropolis/node/build/mkimage/main.go b/metropolis/node/build/mkimage/main.go
index 077348e..7de951e 100644
--- a/metropolis/node/build/mkimage/main.go
+++ b/metropolis/node/build/mkimage/main.go
@@ -27,6 +27,8 @@
package main
import (
+ "bytes"
+ _ "embed"
"flag"
"log"
"os"
@@ -36,6 +38,9 @@
"source.monogon.dev/metropolis/pkg/blockdev"
)
+//go:embed metropolis/node/core/abloader/abloader_bin.efi
+var abloader []byte
+
func main() {
// Fill in the image parameters based on flags.
var (
@@ -92,6 +97,8 @@
panic(err)
}
+ cfg.ABLoader = bytes.NewReader(abloader)
+
// Write the parametrized OS image.
if _, err := osimage.Create(&cfg); err != nil {
log.Fatalf("while creating a Metropolis OS image: %v", err)
diff --git a/metropolis/node/build/mkimage/osimage/osimage.go b/metropolis/node/build/mkimage/osimage/osimage.go
index 01c13ac..a09f5d1 100644
--- a/metropolis/node/build/mkimage/osimage/osimage.go
+++ b/metropolis/node/build/mkimage/osimage/osimage.go
@@ -19,7 +19,6 @@
package osimage
import (
- "bytes"
"fmt"
"io"
"strings"
@@ -72,8 +71,11 @@
type Params struct {
// Output is the block device to which the OS image is written.
Output blockdev.BlockDev
+ // ABLoader provides the A/B loader which then loads the EFI loader for the
+ // correct slot.
+ ABLoader fat32.SizedReader
// EFIPayload provides contents of the EFI payload file. It must not be
- // nil.
+ // nil. This gets put into boot slot A.
EFIPayload fat32.SizedReader
// SystemImage provides contents of the Metropolis system partition.
// If nil, no contents will be copied into the partition.
@@ -116,16 +118,11 @@
rootInode := fat32.Inode{
Attrs: fat32.AttrDirectory,
}
- efiPayload, err := io.ReadAll(params.EFIPayload)
- if err != nil {
- return nil, fmt.Errorf("while reading EFIPayload: %w", err)
- }
- if err := rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), bytes.NewReader(efiPayload)); err != nil {
+ if err := rootInode.PlaceFile(strings.TrimPrefix(EFIBootAPath, "/"), params.EFIPayload); err != nil {
return nil, err
}
- // Also place a copy of the boot file at the autodiscovery path. This will
- // always boot slot A.
- if err := rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), bytes.NewReader(efiPayload)); err != nil {
+ // Place the A/B loader at the EFI bootloader autodiscovery path.
+ if err := rootInode.PlaceFile(strings.TrimPrefix(EFIPayloadPath, "/"), params.ABLoader); err != nil {
return nil, err
}
if params.NodeParameters != nil {
@@ -181,7 +178,7 @@
// Build an EFI boot entry pointing to the image's ESP.
return &efivarfs.LoadOption{
- Description: "Metropolis Slot A",
+ Description: "Metropolis",
FilePath: efivarfs.DevicePath{
&efivarfs.HardDrivePath{
PartitionNumber: 1,
@@ -191,7 +188,7 @@
PartitionUUID: esp.ID,
},
},
- efivarfs.FilePath(EFIBootAPath),
+ efivarfs.FilePath(EFIPayloadPath),
},
}, nil
}