m/node: build microcode payloads

This adds a builder for loadable microcode payloads for the Linux
kernel and microcode for Intel and AMD CPUs. It also adds a rule
generating a microcode payload for Metropolis at
//metropolis/node:ucode but does not integrate it yet.

Change-Id: I00145e4c983d9ff3e81881e92cbecc3e09392665
Reviewed-on: https://review.monogon.dev/c/monogon/+/546
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
diff --git a/metropolis/node/BUILD.bazel b/metropolis/node/BUILD.bazel
index 72a8e11..1168cf5 100644
--- a/metropolis/node/BUILD.bazel
+++ b/metropolis/node/BUILD.bazel
@@ -2,6 +2,7 @@
 load("//metropolis/node/build:def.bzl", "erofs_image", "verity_image")
 load("//metropolis/node/build:efi.bzl", "efi_unified_kernel_image")
 load("//metropolis/node/build/fwprune:def.bzl", "fsspec_linux_firmware")
+load("//metropolis/node/build/mkucode:def.bzl", "cpio_ucode")
 load("@rules_pkg//:pkg.bzl", "pkg_zip")
 
 go_library(
@@ -28,6 +29,15 @@
     kernel = "//third_party/linux",
 )
 
+cpio_ucode(
+    name = "ucode",
+    ucode = {
+        "@linux-firmware//:amd_ucode": "AuthenticAMD",
+        "@intel_ucode//:fam6h": "GenuineIntel",
+    },
+    visibility = ["//metropolis:__subpackages__"],
+)
+
 erofs_image(
     name = "rootfs",
     extra_dirs = [
diff --git a/metropolis/node/build/mkucode/BUILD.bazel b/metropolis/node/build/mkucode/BUILD.bazel
new file mode 100644
index 0000000..5f310c2
--- /dev/null
+++ b/metropolis/node/build/mkucode/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = ["main.go"],
+    importpath = "source.monogon.dev/metropolis/node/build/mkucode",
+    visibility = ["//visibility:private"],
+    deps = [
+        "//metropolis/node/build/mkucode/spec:go_default_library",
+        "@com_github_cavaliergopher_cpio//:go_default_library",
+        "@org_golang_google_protobuf//encoding/prototext:go_default_library",
+    ],
+)
+
+go_binary(
+    name = "mkucode",
+    embed = [":go_default_library"],
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/build/mkucode/def.bzl b/metropolis/node/build/mkucode/def.bzl
new file mode 100644
index 0000000..3faa775
--- /dev/null
+++ b/metropolis/node/build/mkucode/def.bzl
@@ -0,0 +1,45 @@
+def _cpio_ucode_impl(ctx):
+    ucode_spec = ctx.actions.declare_file(ctx.label.name + "_spec.prototxt")
+
+    vendors = []
+    inputs = []
+    for label, vendor in ctx.attr.ucode.items():
+        files = label[DefaultInfo].files.to_list()
+        inputs += files
+        vendors.append(struct(id = vendor, file = [f.path for f in files]))
+
+    ctx.actions.write(ucode_spec, proto.encode_text(struct(vendor = vendors)))
+
+    output_file = ctx.actions.declare_file(ctx.label.name + ".cpio")
+    ctx.actions.run(
+        outputs = [output_file],
+        inputs = [ucode_spec] + inputs,
+        tools = [ctx.executable._mkucode],
+        executable = ctx.executable._mkucode,
+        arguments = ["-out", output_file.path, "-spec", ucode_spec.path],
+    )
+    return [DefaultInfo(files = depset([output_file]))]
+
+cpio_ucode = rule(
+    implementation = _cpio_ucode_impl,
+    doc = """
+        Builds a cpio archive with microcode for the Linux early microcode loader.
+    """,
+    attrs = {
+        "ucode": attr.label_keyed_string_dict(
+            mandatory = True,
+            allow_files = True,
+            doc = """
+                Dictionary of Labels to String. Each label is a list of microcode files and the string label
+                is the vendor ID corresponding to that microcode.
+            """,
+        ),
+
+        # Tool
+        "_mkucode": attr.label(
+            default = Label("//metropolis/node/build/mkucode"),
+            executable = True,
+            cfg = "exec",
+        ),
+    },
+)
diff --git a/metropolis/node/build/mkucode/main.go b/metropolis/node/build/mkucode/main.go
new file mode 100644
index 0000000..1cd8960
--- /dev/null
+++ b/metropolis/node/build/mkucode/main.go
@@ -0,0 +1,71 @@
+// This assembles standalone microcode files into the format expected by the
+// Linux microcode loader. See
+// https://www.kernel.org/doc/html/latest/x86/microcode.html for further
+// information.
+package main
+
+import (
+	"flag"
+	"io"
+	"log"
+	"os"
+
+	"github.com/cavaliergopher/cpio"
+	"google.golang.org/protobuf/encoding/prototext"
+
+	"source.monogon.dev/metropolis/node/build/mkucode/spec"
+)
+
+var (
+	specPath = flag.String("spec", "", "Path to prototext specification (metropolis.node.build.mkucode.UCode)")
+	outPath  = flag.String("out", "", "Output path for cpio to be prepend to initrd")
+)
+
+// Usage: -spec <ucode.prototxt> -out <ucode.cpio>
+func main() {
+	flag.Parse()
+	specRaw, err := os.ReadFile(*specPath)
+	if err != nil {
+		log.Fatalf("Failed to read spec: %v", err)
+	}
+	var ucodeSpec spec.UCode
+	if err := prototext.Unmarshal(specRaw, &ucodeSpec); err != nil {
+		log.Fatalf("Failed unmarshaling ucode spec: %v", err)
+	}
+	out, err := os.Create(*outPath)
+	if err != nil {
+		log.Fatalf("Failed to create cpio: %v", err)
+	}
+	defer out.Close()
+	cpioWriter := cpio.NewWriter(out)
+	for _, vendor := range ucodeSpec.Vendor {
+		var totalSize int64
+		for _, file := range vendor.File {
+			data, err := os.Stat(file)
+			if err != nil {
+				log.Fatalf("Failed to stat file for vendor %q: %v", vendor.Id, err)
+			}
+			totalSize += data.Size()
+		}
+		if err := cpioWriter.WriteHeader(&cpio.Header{
+			Mode: 0444,
+			Name: "kernel/x86/microcode/" + vendor.Id + ".bin",
+			Size: totalSize,
+		}); err != nil {
+			log.Fatalf("Failed to write cpio header for vendor %q: %v", vendor.Id, err)
+		}
+		for _, file := range vendor.File {
+			f, err := os.Open(file)
+			if err != nil {
+				log.Fatalf("Failed to open file for vendor %q: %v", vendor.Id, err)
+			}
+			if _, err := io.Copy(cpioWriter, f); err != nil {
+				log.Fatalf("Failed to copy data for file %q: %v", file, err)
+			}
+			f.Close()
+		}
+	}
+	if err := cpioWriter.Close(); err != nil {
+		log.Fatalf("Failed writing cpio: %v", err)
+	}
+}
diff --git a/metropolis/node/build/mkucode/spec/BUILD.bazel b/metropolis/node/build/mkucode/spec/BUILD.bazel
new file mode 100644
index 0000000..f41e2e9
--- /dev/null
+++ b/metropolis/node/build/mkucode/spec/BUILD.bazel
@@ -0,0 +1,30 @@
+load("@rules_proto//proto:defs.bzl", "proto_library")
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
+
+proto_library(
+    name = "mkucode_proto",
+    srcs = ["spec.proto"],
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "mkucode_go_proto",
+    importpath = "source.monogon.dev/metropolis/node/build/mkucode",
+    proto = ":mkucode_proto",
+    visibility = ["//visibility:public"],
+)
+
+go_library(
+    name = "go_default_library",
+    embed = [":spec_go_proto"],
+    importpath = "source.monogon.dev/metropolis/node/build/mkucode/spec",
+    visibility = ["//visibility:public"],
+)
+
+go_proto_library(
+    name = "spec_go_proto",
+    importpath = "source.monogon.dev/metropolis/node/build/mkucode/spec",
+    proto = ":mkucode_proto",
+    visibility = ["//visibility:public"],
+)
diff --git a/metropolis/node/build/mkucode/spec/spec.proto b/metropolis/node/build/mkucode/spec/spec.proto
new file mode 100644
index 0000000..ed537c5
--- /dev/null
+++ b/metropolis/node/build/mkucode/spec/spec.proto
@@ -0,0 +1,17 @@
+syntax = "proto3";
+
+package metropolis.node.build.mkucode;
+option go_package = "source.monogon.dev/metropolis/node/build/mkucode/spec";
+
+message UCode {
+  repeated UCodeVendor vendor = 1;
+}
+
+message UCodeVendor {
+  // The vendor id (as given in cpuid) of the CPU the microcode is for, like
+  // GenuineIntel or AuthenticAMD.
+  string id = 1;
+
+  // List of paths to microcode files from for CPUs from the vendor.
+  repeated string file = 2;
+}
\ No newline at end of file