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/def.bzl b/metropolis/node/build/def.bzl
index 7baa16b..d71279c 100644
--- a/metropolis/node/build/def.bzl
+++ b/metropolis/node/build/def.bzl
@@ -13,6 +13,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
+load("@bazel_skylib//lib:paths.bzl", "paths")
def _build_pure_transition_impl(settings, attr):
"""
@@ -338,3 +339,75 @@
),
},
)
+
+# From Aspect's bazel-lib under Apache 2.0
+def _transition_platform_impl(_, attr):
+ return {"//command_line_option:platforms": str(attr.target_platform)}
+
+# Transition from any input configuration to one that includes the
+# --platforms command-line flag.
+_transition_platform = transition(
+ implementation = _transition_platform_impl,
+ inputs = [],
+ outputs = ["//command_line_option:platforms"],
+)
+
+
+def _platform_transition_binary_impl(ctx):
+ # We need to forward the DefaultInfo provider from the underlying rule.
+ # Unfortunately, we can't do this directly, because Bazel requires that the executable to run
+ # is actually generated by this rule, so we need to symlink to it, and generate a synthetic
+ # forwarding DefaultInfo.
+
+ result = []
+ binary = ctx.attr.binary[0]
+
+ default_info = binary[DefaultInfo]
+ files = default_info.files
+ new_executable = None
+ original_executable = default_info.files_to_run.executable
+ runfiles = default_info.default_runfiles
+
+ if not original_executable:
+ fail("Cannot transition a 'binary' that is not executable")
+
+ new_executable_name = ctx.attr.basename if ctx.attr.basename else original_executable.basename
+
+ # In order for the symlink to have the same basename as the original
+ # executable (important in the case of proto plugins), put it in a
+ # subdirectory named after the label to prevent collisions.
+ new_executable = ctx.actions.declare_file(paths.join(ctx.label.name, new_executable_name))
+ ctx.actions.symlink(
+ output = new_executable,
+ target_file = original_executable,
+ is_executable = True,
+ )
+ files = depset(direct = [new_executable], transitive = [files])
+ runfiles = runfiles.merge(ctx.runfiles([new_executable]))
+
+ result.append(
+ DefaultInfo(
+ files = files,
+ runfiles = runfiles,
+ executable = new_executable,
+ ),
+ )
+
+ return result
+
+platform_transition_binary = rule(
+ implementation = _platform_transition_binary_impl,
+ attrs = {
+ "basename": attr.string(),
+ "binary": attr.label(allow_files = True, cfg = _transition_platform),
+ "target_platform": attr.label(
+ doc = "The target platform to transition the binary.",
+ mandatory = True,
+ ),
+ "_allowlist_function_transition": attr.label(
+ default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
+ ),
+ },
+ executable = True,
+ doc = "Transitions the binary to use the provided platform.",
+)
\ No newline at end of file
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
}