M8: initrd handoff + multi-binary shipping
Ship more than one user binary: the bootloader now ferries an initrd bundle
(the VFS server + future drivers) alongside sbin/init, and the kernel unpacks
and spawns each program.
- src/user/proto/initrd.zig: the container format (Header{magic,count} +
Entry{name[32],offset,len} + blobs) with a validating Reader, shared by the
kernel and the packer.
- tools/mkinitrd.py: host-side packer (Python — trivial format, and sidesteps
the reworked Zig 0.16 std fs/args API). build.zig runs it on the built user
binaries via addSystemCommand and installs initrd.img to zig-out/bin + the
ESP root.
- BootInfo gains initrd_base/initrd_len; efi.zig loadInit refactored into a
shared loadFile, and loadInitrd ferries \initrd.img into surviving
LoaderData like init.
- main.zig startInitrdBinaries(): parse the image, spawn every entry (kernel-
spawns-all for now; init takes over via sys_spawn later).
- sbin/vfs.zig: a heartbeat stub (the real VFS server is M9), packed into the
initrd to prove the pipeline.
- New `initrd` test: parse + spawn + confirm the vfs stub reaches ring 3 and
heartbeats. Harness ships initrd.img on the ESP. Suite 30/30.
This commit is contained in:
+31
-8
@@ -15,6 +15,9 @@ const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("kernel");
|
||||
/// the FAT driver walks the components itself, so no directory dance needed).
|
||||
const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("sbin\\init");
|
||||
|
||||
/// Path of the initrd image on the boot volume (the VFS server + drivers).
|
||||
const initrd_file_name = std.unicode.utf8ToUtf16LeStringLiteral("initrd.img");
|
||||
|
||||
/// Physical page size, and the sentinel UEFI uses to seek to end-of-file.
|
||||
const page_size = 4096;
|
||||
const seek_end = 0xffff_ffff_ffff_ffff;
|
||||
@@ -65,6 +68,13 @@ fn boot() !noreturn {
|
||||
log(") - booting without user space\r\n");
|
||||
};
|
||||
|
||||
// Best effort: the initrd (VFS server + drivers) is optional too.
|
||||
loadInitrd(bs, &boot_info) catch |err| {
|
||||
log("danos: no initrd (");
|
||||
logBytes(@errorName(err));
|
||||
log(")\r\n");
|
||||
};
|
||||
|
||||
// Build the page tables the kernel starts life on: identity + a physmap of
|
||||
// low RAM, plus the higher-half kernel image once it links high. Allocated
|
||||
// now, while boot services (and the memory map) are still stable — nothing
|
||||
@@ -344,12 +354,11 @@ fn handoff(cr3: u64, entry: usize, boot_info: *const BootInfo) noreturn {
|
||||
unreachable;
|
||||
}
|
||||
|
||||
/// Read the init program (sbin/init) into memory that outlives the loader and
|
||||
/// record it in the handoff. The pool buffer is deliberately NOT freed: it's
|
||||
/// LoaderData, which the memory-map conversion classifies as reserved, so the
|
||||
/// kernel identity-maps it and reads the ELF from there. The kernel does the
|
||||
/// loading itself (into ring-3 mappings) — the loader just ferries the bytes.
|
||||
fn loadInit(bs: *uefi.tables.BootServices, boot_info: *BootInfo) !void {
|
||||
/// Read a whole file off the boot volume into a pool buffer that outlives the
|
||||
/// loader. The buffer is deliberately NOT freed: it's LoaderData, which the
|
||||
/// memory-map conversion classifies as reserved, so the kernel identity-maps it
|
||||
/// and reads from there. Returns the buffer (pointer + length).
|
||||
fn loadFile(bs: *uefi.tables.BootServices, name: [*:0]const u16) ![]u8 {
|
||||
const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse
|
||||
return error.NoLoadedImage;
|
||||
const device = loaded.device_handle orelse return error.NoBootDevice;
|
||||
@@ -359,7 +368,7 @@ fn loadInit(bs: *uefi.tables.BootServices, boot_info: *BootInfo) !void {
|
||||
const root = try fs.openVolume();
|
||||
defer _ = root.close() catch {};
|
||||
|
||||
const file = try root.open(init_file_name, .read, .{});
|
||||
const file = try root.open(name, .read, .{});
|
||||
defer _ = file.close() catch {};
|
||||
|
||||
try file.setPosition(seek_end);
|
||||
@@ -375,12 +384,26 @@ fn loadInit(bs: *uefi.tables.BootServices, boot_info: *BootInfo) !void {
|
||||
if (n == 0) return error.UnexpectedEof;
|
||||
read_total += n;
|
||||
}
|
||||
return image[0..size];
|
||||
}
|
||||
|
||||
/// Ferry the init program (sbin/init) to the kernel. The kernel does the ELF
|
||||
/// loading itself (into ring-3 mappings) — the loader just carries the bytes.
|
||||
fn loadInit(bs: *uefi.tables.BootServices, boot_info: *BootInfo) !void {
|
||||
const image = try loadFile(bs, init_file_name);
|
||||
boot_info.init_base = @intFromPtr(image.ptr);
|
||||
boot_info.init_len = size;
|
||||
boot_info.init_len = image.len;
|
||||
log("danos: sbin/init loaded\r\n");
|
||||
}
|
||||
|
||||
/// Ferry the initrd (the VFS server + drivers) to the kernel, same as init.
|
||||
fn loadInitrd(bs: *uefi.tables.BootServices, boot_info: *BootInfo) !void {
|
||||
const image = try loadFile(bs, initrd_file_name);
|
||||
boot_info.initrd_base = @intFromPtr(image.ptr);
|
||||
boot_info.initrd_len = image.len;
|
||||
log("danos: initrd loaded\r\n");
|
||||
}
|
||||
|
||||
/// Validate the ELF, copy every PT_LOAD segment to its physical address, and
|
||||
/// record each segment's layout so the kernel can re-map itself with the right
|
||||
/// permissions.
|
||||
|
||||
@@ -8,6 +8,7 @@ const pmm = @import("pmm.zig");
|
||||
const heap = @import("heap.zig");
|
||||
const scheduler = @import("scheduler.zig");
|
||||
const process = @import("process.zig");
|
||||
const initrd = @import("initrd");
|
||||
const platform = @import("platform");
|
||||
const tests = @import("tests.zig");
|
||||
const build_options = @import("build_options");
|
||||
@@ -270,6 +271,11 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
status("no /sbin/init on the boot volume.\n");
|
||||
}
|
||||
|
||||
// Spawn the extra user binaries the loader ferried in the initrd (the VFS
|
||||
// server, and later device drivers). For now the kernel launches them all;
|
||||
// once init is a real service supervisor it will spawn them itself (sys_spawn).
|
||||
startInitrdBinaries(boot_info);
|
||||
|
||||
// Become the idle task: drop below every real task and halt until an
|
||||
// interrupt. The timer keeps preempting into init and any other work.
|
||||
scheduler.setPriority(0);
|
||||
@@ -277,6 +283,26 @@ fn kmain(boot_info: *const BootInfo) noreturn {
|
||||
arch.halt();
|
||||
}
|
||||
|
||||
/// Spawn every program bundled in the initrd as its own ring-3 process. A bad
|
||||
/// image or a program that fails to load is logged and skipped — the rest of the
|
||||
/// system still runs.
|
||||
fn startInitrdBinaries(boot_info: *const danos.BootInfo) void {
|
||||
if (boot_info.initrd_len == 0) return;
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.initrd_base)))[0..boot_info.initrd_len];
|
||||
const rd = initrd.Reader.init(image) orelse {
|
||||
status("initrd: bad image, skipping\n");
|
||||
return;
|
||||
};
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
statusPrint("starting /sbin/{s} (from initrd)...\n", .{item.name});
|
||||
process.spawnProcess(item.blob, 4) catch |err| {
|
||||
statusPrint("initrd: {s} failed to load: {s}\n", .{ item.name, @errorName(err) });
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Wake the application processors the firmware left parked. Allocates the low
|
||||
/// trampoline page (and makes it executable), then wakes each non-boot core in turn,
|
||||
/// handing it a fresh kernel stack and its per-CPU slot. Cores that don't report in
|
||||
|
||||
@@ -19,6 +19,7 @@ const sched = @import("scheduler.zig");
|
||||
const ipc = @import("ipc.zig");
|
||||
const ipcsync = @import("ipc_sync.zig");
|
||||
const process = @import("process.zig");
|
||||
const initrd = @import("initrd");
|
||||
|
||||
/// Formatted write straight to serial, independent of the framebuffer console.
|
||||
fn log(comptime fmt: []const u8, args: anytype) void {
|
||||
@@ -104,6 +105,8 @@ pub fn run(case: []const u8, boot_info: *const BootInfo) void {
|
||||
initTest(boot_info);
|
||||
} else if (eql(case, "process")) {
|
||||
processTest(boot_info);
|
||||
} else if (eql(case, "initrd")) {
|
||||
initrdTest(boot_info);
|
||||
} else if (eql(case, "poweroff")) {
|
||||
powerTest(.off);
|
||||
} else if (eql(case, "reboot")) {
|
||||
@@ -936,6 +939,51 @@ fn initTest(boot_info: *const BootInfo) void {
|
||||
result();
|
||||
}
|
||||
|
||||
/// The initrd path: the bootloader handed over an image bundling extra user
|
||||
/// binaries; parse it, spawn every program, and confirm one (the vfs stub)
|
||||
/// reaches ring 3 and heartbeats — proving the whole ferry-parse-spawn pipeline.
|
||||
fn initrdTest(boot_info: *const BootInfo) void {
|
||||
log("DANOS-TEST-BEGIN: initrd\n", .{});
|
||||
check("bootloader handed over an initrd", boot_info.initrd_len != 0);
|
||||
if (boot_info.initrd_len == 0) {
|
||||
result();
|
||||
return;
|
||||
}
|
||||
const image = @as([*]const u8, @ptrFromInt(danos.physToVirt(boot_info.initrd_base)))[0..boot_info.initrd_len];
|
||||
const rd = initrd.Reader.init(image) orelse {
|
||||
check("initrd image is valid", false);
|
||||
result();
|
||||
return;
|
||||
};
|
||||
check("initrd image is valid", true);
|
||||
check("initrd contains at least one binary", rd.count >= 1);
|
||||
|
||||
process.write_count = 0;
|
||||
process.write_from_user = false;
|
||||
var spawned: u32 = 0;
|
||||
var i: u32 = 0;
|
||||
while (i < rd.count) : (i += 1) {
|
||||
const item = rd.entry(i) orelse continue;
|
||||
if (process.spawnProcess(item.blob, 4)) spawned += 1 else |err| {
|
||||
log("DANOS-INITRD-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
|
||||
}
|
||||
}
|
||||
check("every initrd binary spawned", spawned == rd.count);
|
||||
|
||||
// Wait for the spawned program(s) to heartbeat (the vfs stub sleeps ~1 s).
|
||||
sched.setPriority(1);
|
||||
const deadline = arch.millis() + 8000;
|
||||
while (process.write_count < 2 and arch.millis() < deadline) sched.yield();
|
||||
sched.setPriority(4);
|
||||
|
||||
const prefix = "vfs: alive";
|
||||
const beat_ok = process.write_len >= prefix.len and eql(process.write_buf[0..prefix.len], prefix);
|
||||
check("an initrd process heartbeats (>=2)", process.write_count >= 2);
|
||||
check("heartbeat text arrived intact", beat_ok);
|
||||
check("heartbeats came from user mode (CPL 3)", process.write_from_user);
|
||||
result();
|
||||
}
|
||||
|
||||
fn faultInvalidOpcode() void {
|
||||
log("DANOS-TEST-BEGIN: fault-ud\n", .{});
|
||||
asm volatile ("ud2");
|
||||
|
||||
@@ -182,4 +182,9 @@ pub const BootInfo = extern struct {
|
||||
/// kernel boots without user space. Grows into a full initrd handoff later.
|
||||
init_base: u64 = 0,
|
||||
init_len: u64 = 0,
|
||||
/// The initrd image (a bundle of extra user binaries — the VFS server and
|
||||
/// device drivers), read off the boot volume into memory that survives the
|
||||
/// handoff, same as `init` above. 0/0 = no initrd. See src/user/proto/initrd.zig.
|
||||
initrd_base: u64 = 0,
|
||||
initrd_len: u64 = 0,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//! The initrd (initial ramdisk) container format — shared by the build-time
|
||||
//! packer (tools/mkinitrd.zig) and the kernel that unpacks it. Deliberately
|
||||
//! trivial: a header, a table of fixed-size entries, then the concatenated file
|
||||
//! blobs. We own both producer and consumer, so it need be no fancier.
|
||||
//!
|
||||
//! Layout:
|
||||
//! Header (magic, count)
|
||||
//! Entry * count (name, offset, len) — offset/len into the image
|
||||
//! blob bytes... (each entry's file, at its offset)
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
/// "DNRD" — identifies a danos initrd image.
|
||||
pub const magic: u32 = 0x444E5244;
|
||||
|
||||
pub const Header = extern struct {
|
||||
magic: u32,
|
||||
count: u32,
|
||||
};
|
||||
|
||||
pub const Entry = extern struct {
|
||||
name: [32]u8, // NUL-padded file name (basename)
|
||||
offset: u64, // byte offset of the blob within the image
|
||||
len: u64, // blob length in bytes
|
||||
};
|
||||
|
||||
/// A validated view over an initrd image. `init` checks the magic and that the
|
||||
/// entry table fits; `entry` bounds-checks each blob against the image.
|
||||
pub const Reader = struct {
|
||||
image: []const u8,
|
||||
count: u32,
|
||||
|
||||
pub fn init(image: []const u8) ?Reader {
|
||||
if (image.len < @sizeOf(Header)) return null;
|
||||
const h = std.mem.bytesToValue(Header, image[0..@sizeOf(Header)]);
|
||||
if (h.magic != magic) return null;
|
||||
const table_end = @sizeOf(Header) + @as(usize, h.count) * @sizeOf(Entry);
|
||||
if (table_end > image.len) return null;
|
||||
return .{ .image = image, .count = h.count };
|
||||
}
|
||||
|
||||
pub const Item = struct { name: []const u8, blob: []const u8 };
|
||||
|
||||
pub fn entry(self: Reader, i: u32) ?Item {
|
||||
if (i >= self.count) return null;
|
||||
const off = @sizeOf(Header) + @as(usize, i) * @sizeOf(Entry);
|
||||
const e = std.mem.bytesToValue(Entry, self.image[off..][0..@sizeOf(Entry)]);
|
||||
if (e.offset > self.image.len or e.len > self.image.len - e.offset) return null;
|
||||
// The name is stored in the entry's fixed field; return a stable slice
|
||||
// into the image (not the value copy) up to the NUL terminator.
|
||||
const name_field = self.image[off .. off + 32];
|
||||
const nlen = std.mem.indexOfScalar(u8, name_field, 0) orelse name_field.len;
|
||||
return .{
|
||||
.name = name_field[0..nlen],
|
||||
.blob = self.image[@intCast(e.offset)..][0..@intCast(e.len)],
|
||||
};
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user