boot: the capsule — one-file system image first, manifest and walk as fallbacks
Real-firmware finding: the per-file /system tree walk boots in seconds
under OVMF but stalls for MINUTES on real firmware — the cost is not
bytes (USB 3 moves the ~4 MB instantly) but firmware filesystem
OPERATIONS: ~30 opens, each an uncached directory-chain walk in an
unoptimized firmware FAT driver. This is why every real OS loader
(winload, GRUB) reads many files through its own filesystem code over
Block I/O rather than the firmware's file protocol.
The loader now reads boot/system.img — the bundled binaries packed into
ONE v2 initial_ramdisk (tools/pack-system-image.py, derived from the
same bundled list in the same build graph, so tree and capsule cannot
drift) — with a single open + sequential read, the one firmware file
I/O shape that is fast everywhere. The manifest (open each listed path
by name) and the tree walk remain as fallbacks, so a hand-assembled
stick without the capsule still boots. The running system is identical
in all three cases: the kernel receives the same in-RAM table.
The load phase now brackets itself with unconditional on-screen
breadcrumbs ('EFI: loading the system...' / '...starting the kernel'),
because this phase stalling behind a silent black screen — kernel
status is serial-only by design — already cost a real-hardware
debugging session.
Direction (settled with the user): this capsule becomes the BOOTSTRAP
capsule — kernel + init + the storage-bring-up set — once a
spawn-from-memory syscall lets init and the device manager load
everything else from the stick's real file tree at runtime through
danos's own storage stack: file-granular updates (rebuild one binary,
copy one file), the initramfs shape.
This commit is contained in:
+100
-9
@@ -380,11 +380,19 @@ const Bundled = struct {
|
||||
data: []align(8) u8,
|
||||
};
|
||||
|
||||
/// Walk the boot volume's /system tree and pack every regular file (except the
|
||||
/// kernel image itself — the only top-level file) into an in-RAM v2
|
||||
/// initial_ramdisk image, entries named by full FHS path. This is what makes the
|
||||
/// volume's file structure the single source of truth: there is no packed
|
||||
/// ramdisk artifact on disk, and init travels in the table like everything else.
|
||||
/// Gather the boot volume's user binaries into an in-RAM v2 initial_ramdisk
|
||||
/// image, entries named by full FHS path — the volume's file structure is the
|
||||
/// single source of truth (no packed ramdisk artifact; init travels in the
|
||||
/// table like everything else).
|
||||
///
|
||||
/// Two strategies, most portable first:
|
||||
/// 1. /system/manifest (written by the build): each listed path is opened BY
|
||||
/// NAME — the case-insensitive lookup every firmware FAT driver gets
|
||||
/// right, and the only file access the pre-tree loader ever used.
|
||||
/// 2. No manifest: ENUMERATE the /system tree. Portable in principle, but
|
||||
/// firmware differs in what names enumeration returns (bare 8.3 entries
|
||||
/// come back uppercase on some drivers), so this is the fallback for
|
||||
/// hand-assembled sticks, not the primary path.
|
||||
fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void {
|
||||
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
|
||||
return error.NoLoadedImage;
|
||||
@@ -395,12 +403,30 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat
|
||||
const root = try fs.openVolume();
|
||||
defer _ = root.close() catch {};
|
||||
|
||||
const system_directory = try root.open(system_directory_name, .read, .{});
|
||||
defer _ = system_directory.close() catch {};
|
||||
// Unconditional breadcrumb (con_out, independent of -Dserial): this phase
|
||||
// is where a slow firmware stalls, and a silent black screen here already
|
||||
// cost a real-hardware debugging session.
|
||||
log("EFI: loading the system...\r\n");
|
||||
|
||||
// The capsule (boot\system.img) first: one open + one sequential read is
|
||||
// the only firmware file I/O shape that is fast everywhere. It is already
|
||||
// the kernel's wire format — hand it over as-is.
|
||||
if (loadCapsule(bs, root, boot_information)) {
|
||||
log("EFI: system image loaded, starting the kernel\r\n");
|
||||
return;
|
||||
}
|
||||
|
||||
var list: [maximum_bundled]Bundled = undefined;
|
||||
var count: usize = 0;
|
||||
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
|
||||
|
||||
loadByManifest(bs, root, &list, &count) catch {
|
||||
count = 0; // a torn manifest read leaves partial entries; start over
|
||||
};
|
||||
if (count == 0) {
|
||||
const system_directory = try root.open(system_directory_name, .read, .{});
|
||||
defer _ = system_directory.close() catch {};
|
||||
try walkDirectory(bs, system_directory, "/system", 0, &list, &count);
|
||||
}
|
||||
if (count == 0) return error.NoBinaries;
|
||||
|
||||
// Assemble the v2 image: header, entry table, then the blobs.
|
||||
@@ -426,7 +452,72 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat
|
||||
|
||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||
boot_information.initial_ramdisk_len = total;
|
||||
progress("EFI: /system tree loaded\r\n");
|
||||
log("EFI: /system tree loaded, starting the kernel\r\n");
|
||||
}
|
||||
|
||||
/// The boot capsule: the bundled binaries as one v2 initial_ramdisk image.
|
||||
const capsule_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\system.img");
|
||||
|
||||
/// Load boot\system.img whole and hand it to the kernel unmodified — it is
|
||||
/// already the initial_ramdisk wire format. Returns false (capsule absent or
|
||||
/// unreadable or wrong magic) to let the caller fall back to per-file loading.
|
||||
fn loadCapsule(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, boot_information: *BootInformation) bool {
|
||||
const file = root.open(capsule_file_name, .read, .{}) catch return false;
|
||||
defer _ = file.close() catch {};
|
||||
const image = readWholeFile(bs, file) catch return false;
|
||||
if (image.len < @sizeOf(initial_ramdisk.Header) or
|
||||
std.mem.bytesToValue(initial_ramdisk.Header, image[0..@sizeOf(initial_ramdisk.Header)]).magic != initial_ramdisk.magic)
|
||||
{
|
||||
_ = bs.freePool(image.ptr) catch {};
|
||||
return false;
|
||||
}
|
||||
boot_information.initial_ramdisk_base = @intFromPtr(image.ptr);
|
||||
boot_information.initial_ramdisk_len = image.len;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The manifest path, and a scratch limit for its UTF-16 conversion.
|
||||
const manifest_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\manifest");
|
||||
|
||||
/// Load every binary the manifest lists, opening each path by name from the
|
||||
/// volume root. A listed-but-unopenable file is skipped (the kernel reports the
|
||||
/// absence); a missing manifest errors so the caller falls back to the walk.
|
||||
fn loadByManifest(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, list: *[maximum_bundled]Bundled, count: *usize) !void {
|
||||
const manifest_handle = try root.open(manifest_file_name, .read, .{});
|
||||
var manifest_open = true;
|
||||
defer if (manifest_open) {
|
||||
_ = manifest_handle.close() catch {};
|
||||
};
|
||||
const manifest = try readWholeFile(bs, manifest_handle);
|
||||
_ = manifest_handle.close() catch {};
|
||||
manifest_open = false;
|
||||
defer _ = bs.freePool(manifest.ptr) catch {};
|
||||
|
||||
var lines = std.mem.tokenizeAny(u8, manifest, "\r\n");
|
||||
while (lines.next()) |line| {
|
||||
if (line.len < 2 or line[0] != '/') continue;
|
||||
if (line.len >= initial_ramdisk.maximum_name) continue;
|
||||
if (count.* == maximum_bundled) return;
|
||||
|
||||
// "/system/services/init" -> UTF-16 "system\services\init".
|
||||
var name16: [initial_ramdisk.maximum_name]u16 = undefined;
|
||||
var i: usize = 0;
|
||||
for (line[1..]) |c| {
|
||||
name16[i] = if (c == '/') '\\' else c;
|
||||
i += 1;
|
||||
}
|
||||
name16[i] = 0;
|
||||
|
||||
const file = root.open(@ptrCast(name16[0..i :0]), .read, .{}) catch continue;
|
||||
defer _ = file.close() catch {};
|
||||
const data = readWholeFile(bs, file) catch continue;
|
||||
|
||||
var entry: *Bundled = &list[count.*];
|
||||
@memcpy(entry.path[0..line.len], line);
|
||||
entry.path_len = line.len;
|
||||
entry.data = data;
|
||||
count.* += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect the regular files below `directory` into `list`. Top-level
|
||||
|
||||
Reference in New Issue
Block a user