metropolis/node: move misplaced packages out of core
abloader, bios_bootcode and minit don't run as part of the core process,
so it doesn't make sense to have them in //metropolis/node/core.
This changes moves these three to //metropolis/node.
Change-Id: I908efb311a138f07a9f1de8e3c23437ff00131ee
Reviewed-on: https://review.monogon.dev/c/monogon/+/4196
Reviewed-by: Tim Windelschmidt <tim@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/node/abloader/BUILD.bazel b/metropolis/node/abloader/BUILD.bazel
new file mode 100644
index 0000000..20f26db
--- /dev/null
+++ b/metropolis/node/abloader/BUILD.bazel
@@ -0,0 +1,32 @@
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+load("//osbase/build:def.bzl", "ignore_unused_configuration_target")
+
+rust_binary(
+ name = "abloader_bin",
+ srcs = ["main.rs"],
+ edition = "2021",
+ platform = select({
+ "@platforms//cpu:x86_64": "//build/platforms:uefi_x86_64",
+ "@platforms//cpu:aarch64": "//build/platforms:uefi_aarch64",
+ }),
+ # rust_binary depends on the status files by default, even if no stamp
+ # variables are used, which causes unnecessary rebuilds when the stable
+ # status file changes.
+ stamp = 0,
+ target_compatible_with = [
+ "@platforms//os:uefi",
+ ],
+ visibility = ["//visibility:private"],
+ deps = [
+ "//metropolis/node/abloader/spec:abloader_proto_rs",
+ "@crate_index_efi//:prost",
+ "@crate_index_efi//:uefi",
+ "@crate_index_efi//:uefi-services",
+ ],
+)
+
+ignore_unused_configuration_target(
+ name = "abloader",
+ dep = ":abloader_bin",
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/abloader/main.rs b/metropolis/node/abloader/main.rs
new file mode 100644
index 0000000..f0f61ee
--- /dev/null
+++ b/metropolis/node/abloader/main.rs
@@ -0,0 +1,175 @@
+/*
+ * Copyright The Monogon Project Authors.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#![no_main]
+#![no_std]
+
+extern crate alloc;
+
+use alloc::vec::Vec;
+use core::result::Result;
+use core::fmt;
+use prost::Message;
+use uefi::fs::FileSystem;
+use uefi::proto::device_path::build::media::FilePath;
+use uefi::proto::device_path::build::DevicePathBuilder;
+use uefi::proto::device_path::{DeviceSubType, DeviceType, LoadedImageDevicePath};
+use uefi::table::boot;
+use uefi::{prelude::*, CStr16};
+use uefi_services::println;
+
+use abloader_proto::metropolis::node::abloader::spec::*;
+
+const A_LOADER_PATH: &CStr16 = cstr16!("\\EFI\\metropolis\\boot-a.efi");
+const B_LOADER_PATH: &CStr16 = cstr16!("\\EFI\\metropolis\\boot-b.efi");
+
+const LOADER_STATE_PATH: &CStr16 = cstr16!("\\EFI\\metropolis\\loader_state.pb");
+
+enum ValidSlot {
+ A,
+ B,
+}
+
+impl ValidSlot {
+ // other returns B if the value is A and A if the value is B.
+ fn other(&self) -> Self {
+ match self {
+ ValidSlot::A => ValidSlot::B,
+ ValidSlot::B => ValidSlot::A,
+ }
+ }
+ // path returns the path to the slot's EFI payload.
+ fn path(&self) -> &'static CStr16 {
+ match self {
+ ValidSlot::A => A_LOADER_PATH,
+ ValidSlot::B => B_LOADER_PATH,
+ }
+ }
+}
+
+enum ReadLoaderStateError {
+ FSReadError(uefi::fs::Error),
+ DecodeError(prost::DecodeError),
+}
+
+impl fmt::Display for ReadLoaderStateError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ ReadLoaderStateError::FSReadError(e) => write!(f, "while reading state file: {}", e),
+ ReadLoaderStateError::DecodeError(e) => write!(f, "while decoding state file contents: {}", e),
+ }
+ }
+}
+
+fn read_loader_state(fs: &mut FileSystem) -> Result<AbLoaderData, ReadLoaderStateError> {
+ let state_raw = fs.read(&LOADER_STATE_PATH).map_err(|e| ReadLoaderStateError::FSReadError(e))?;
+ AbLoaderData::decode(state_raw.as_slice()).map_err(|e| ReadLoaderStateError::DecodeError(e))
+}
+
+fn load_slot_image(slot: &ValidSlot, boot_services: &BootServices) -> uefi::Result<Handle> {
+ let mut storage = Vec::new();
+
+ // Build the path to the slot payload. This takes the path to the loader
+ // itself, strips off the file path and following element(s) and appends
+ // the path to the correct slot payload.
+ let new_image_path = {
+ let loaded_image_device_path = boot_services
+ .open_protocol_exclusive::<LoadedImageDevicePath>(boot_services.image_handle())?;
+
+ let mut builder = DevicePathBuilder::with_vec(&mut storage);
+
+ for node in loaded_image_device_path.node_iter() {
+ if node.full_type() == (DeviceType::MEDIA, DeviceSubType::MEDIA_FILE_PATH) {
+ break;
+ }
+
+ builder = builder.push(&node).unwrap();
+ }
+
+ builder = builder
+ .push(&FilePath {
+ path_name: slot.path(),
+ })
+ .unwrap();
+
+ builder.finalize().unwrap()
+ };
+
+ boot_services
+ .load_image(
+ boot_services.image_handle(),
+ boot::LoadImageSource::FromDevicePath {
+ device_path: new_image_path,
+ from_boot_manager: false,
+ },
+ )
+}
+
+#[entry]
+fn main(_handle: Handle, mut system_table: SystemTable<Boot>) -> Status {
+ uefi_services::init(&mut system_table).unwrap();
+
+ let boot_services = system_table.boot_services();
+
+ let boot_slot_raw = {
+ let mut esp_fs = boot_services
+ .get_image_file_system(boot_services.image_handle())
+ .expect("image filesystem not available");
+
+ let mut loader_data = match read_loader_state(&mut esp_fs) {
+ Ok(d) => d,
+ Err(e) => {
+ println!("Unable to load A/B loader state, using default slot A: {}", e);
+ AbLoaderData {
+ active_slot: Slot::A.into(),
+ next_slot: Slot::None.into(),
+ }
+ }
+ };
+
+ // If next_slot is set, use it as slot to boot but clear it in the
+ // state file as the next boot should not use it again. If it should
+ // be permanently activated, it is the OS's job to put it into
+ if loader_data.next_slot != Slot::None.into() {
+ let next_slot = loader_data.next_slot;
+ loader_data.next_slot = Slot::None.into();
+ let new_loader_data = loader_data.encode_to_vec();
+ esp_fs
+ .write(&LOADER_STATE_PATH, new_loader_data)
+ .expect("failed to write back abdata");
+ next_slot
+ } else {
+ loader_data.active_slot
+ }
+ };
+
+ let boot_slot = match Slot::try_from(boot_slot_raw) {
+ Ok(Slot::A) => ValidSlot::A,
+ Ok(Slot::B) => ValidSlot::B,
+ _ => {
+ println!("Invalid slot ({}) active, falling back to A", boot_slot_raw);
+ ValidSlot::A
+ }
+ };
+
+ let payload_image = match load_slot_image(&boot_slot, boot_services) {
+ Ok(img) => img,
+ Err(e) => {
+ println!("Error loading intended slot, falling back to other slot: {}", e);
+ match load_slot_image(&boot_slot.other(), boot_services) {
+ Ok(img) => img,
+ Err(e) => {
+ panic!("Loading from both slots failed, second slot error: {}", e);
+ },
+ }
+ }
+ };
+
+ // Boot the payload.
+ boot_services
+ .start_image(payload_image)
+ .expect("failed to start payload");
+ Status::SUCCESS
+}
diff --git a/metropolis/node/abloader/spec/BUILD.bazel b/metropolis/node/abloader/spec/BUILD.bazel
new file mode 100644
index 0000000..3190c57
--- /dev/null
+++ b/metropolis/node/abloader/spec/BUILD.bazel
@@ -0,0 +1,44 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@rules_proto_grpc_buf//:defs.bzl", "buf_proto_lint_test")
+load("@rules_rust_prost//:defs.bzl", "rust_prost_library")
+
+buf_proto_lint_test(
+ name = "abloader_proto_lint_test",
+ except_rules = [
+ "PACKAGE_VERSION_SUFFIX",
+ "ENUM_ZERO_VALUE_SUFFIX",
+ ],
+ protos = [":abloader_proto"],
+ use_rules = [
+ "DEFAULT",
+ "COMMENTS",
+ ],
+)
+
+proto_library(
+ name = "abloader_proto",
+ srcs = ["abloader.proto"],
+ visibility = ["//visibility:public"],
+)
+
+rust_prost_library(
+ name = "abloader_proto_rs",
+ proto = ":abloader_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+ name = "abloader_go_proto",
+ importpath = "source.monogon.dev/metropolis/node/abloader/spec",
+ proto = ":abloader_proto",
+ visibility = ["//visibility:public"],
+)
+
+go_library(
+ name = "spec",
+ embed = [":abloader_go_proto"],
+ importpath = "source.monogon.dev/metropolis/node/abloader/spec",
+ visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/abloader/spec/abloader.proto b/metropolis/node/abloader/spec/abloader.proto
new file mode 100644
index 0000000..9704ce6
--- /dev/null
+++ b/metropolis/node/abloader/spec/abloader.proto
@@ -0,0 +1,22 @@
+syntax = "proto3";
+
+package metropolis.node.abloader.spec;
+
+enum Slot {
+ SLOT_NONE = 0;
+ SLOT_A = 1;
+ SLOT_B = 2;
+}
+
+// ABLoaderData contains data the A/B loader needs to chose which slot to boot
+// as well as for auto-rollback functionality.
+message ABLoaderData {
+ // The currently-active slot. This slot will be booted unless next_slot is
+ // set to a non-default value.
+ Slot active_slot = 1;
+ // The slot to boot next, but only once. Once the the slot has been selected
+ // to be booted by the loader, this value is reset before booting into that
+ // slot. If the OS boots successfully, it will update the active_slot to
+ // permanently boot from the new slot.
+ Slot next_slot = 2;
+}
diff --git a/metropolis/node/abloader/spec/gomod-generated-placeholder.go b/metropolis/node/abloader/spec/gomod-generated-placeholder.go
new file mode 100644
index 0000000..ca2145c
--- /dev/null
+++ b/metropolis/node/abloader/spec/gomod-generated-placeholder.go
@@ -0,0 +1,4 @@
+// Copyright The Monogon Project Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package spec