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
 }