diff --git a/boot/efi.zig b/boot/efi.zig index c3d72bb..87931fd 100644 --- a/boot/efi.zig +++ b/boot/efi.zig @@ -2,6 +2,7 @@ const std = @import("std"); const uefi = std.os.uefi; const elf = std.elf; const boot_handoff = @import("boot-handoff"); +const initial_ramdisk = @import("initial-ramdisk"); const build_options = @import("build_options"); const BootInformation = boot_handoff.BootInformation; const GraphicsOutput = uefi.protocol.GraphicsOutput; @@ -15,11 +16,11 @@ const MemoryMapSlice = uefi.tables.MemoryMapSlice; /// The kernel image: /system/kernel. const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel"); -/// The init program: /system/services/init. -const init_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\services\\init"); - -/// The initial-ramdisk (the VFS server + drivers), in /boot. -const initial_ramdisk_file_name = std.unicode.utf8ToUtf16LeStringLiteral("boot\\initial-ramdisk.img"); +/// The user binaries: everything under /system except the kernel itself. The +/// loader walks this tree and packs it into the in-RAM initial_ramdisk image — +/// the volume's file structure is the single source of truth (no packed image +/// artifact on disk). +const system_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("system"); /// Physical page size, and the sentinel UEFI uses to seek to end-of-file. const page_size = 4096; @@ -64,20 +65,14 @@ fn boot() !noreturn { const entry = try loadKernel(bs, &boot_information); - // Best effort: a volume without /system/services/init still boots (kernel-only). - loadInit(bs, &boot_information) catch |err| { - log("EFI: no /system/services/init ("); + // Best effort: a volume without a /system tree of user binaries still boots + // (kernel-only). The tree — init included — becomes the initial_ramdisk. + loadSystemTree(bs, &boot_information) catch |err| { + log("EFI: no /system binaries ("); logBytes(@errorName(err)); log(") - booting without user space\r\n"); }; - // Best effort: the initial_ramdisk (VFS server + drivers) is optional too. - loadInitialRamdisk(bs, &boot_information) catch |err| { - log("EFI: no initial_ramdisk ("); - 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 @@ -369,11 +364,28 @@ fn handoff(cr3: u64, entry: usize, boot_information: *const BootInformation) nor unreachable; } -/// 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 { +// --- the /system tree -> initial_ramdisk ------------------------------------ + +/// Cap on bundled binaries. Generous: the tree carries ~30 today. +const maximum_bundled = 64; + +/// How deep the walk goes below /system ("/system/services/x" is depth 1). +const maximum_tree_depth = 3; + +/// One binary discovered under /system: its FHS path (UTF-8, '/'-separated, +/// NUL-free) and its contents in a transient pool buffer. +const Bundled = struct { + path: [initial_ramdisk.maximum_name]u8, + path_len: usize, + 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. +fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void { const loaded = (try bs.handleProtocol(uefi.protocol.LoadedImage, uefi.handle)) orelse return error.NoLoadedImage; const device = loaded.device_handle orelse return error.NoBootDevice; @@ -383,40 +395,111 @@ fn loadFile(bs: *uefi.tables.BootServices, name: [*:0]const u16) ![]u8 { const root = try fs.openVolume(); defer _ = root.close() catch {}; - const file = try root.open(name, .read, .{}); - defer _ = file.close() catch {}; + const system_directory = try root.open(system_directory_name, .read, .{}); + defer _ = system_directory.close() catch {}; + var list: [maximum_bundled]Bundled = undefined; + var count: usize = 0; + 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. + const table_end = @sizeOf(initial_ramdisk.Header) + count * @sizeOf(initial_ramdisk.Entry); + var total: usize = table_end; + for (list[0..count]) |e| total += e.data.len; + + const image = try bs.allocatePool(.loader_data, total); // survives the handoff + std.mem.bytesAsValue(initial_ramdisk.Header, image[0..@sizeOf(initial_ramdisk.Header)]).* = .{ + .magic = initial_ramdisk.magic, + .count = @intCast(count), + }; + var offset: usize = table_end; + for (list[0..count], 0..) |e, i| { + var record = initial_ramdisk.Entry{ .name = @splat(0), .offset = offset, .len = e.data.len }; + @memcpy(record.name[0..e.path_len], e.path[0..e.path_len]); + const slot = image[@sizeOf(initial_ramdisk.Header) + i * @sizeOf(initial_ramdisk.Entry) ..][0..@sizeOf(initial_ramdisk.Entry)]; + std.mem.bytesAsValue(initial_ramdisk.Entry, slot).* = record; + @memcpy(image[offset..][0..e.data.len], e.data); + offset += e.data.len; + _ = bs.freePool(e.data.ptr) catch {}; + } + + boot_information.initial_ramdisk_base = @intFromPtr(image.ptr); + boot_information.initial_ramdisk_len = total; + progress("EFI: /system tree loaded\r\n"); +} + +/// Recursively collect the regular files below `directory` into `list`. Top-level +/// files (depth 0) are skipped: the only one is /system/kernel, which loadKernel +/// has already consumed and which is not a spawnable user binary. +fn walkDirectory( + bs: *uefi.tables.BootServices, + directory: *uefi.protocol.File, + prefix: []const u8, + depth: usize, + list: *[maximum_bundled]Bundled, + count: *usize, +) !void { + // Each read() on a directory yields one EFI_FILE_INFO; zero bytes means done. + var info_buffer: [1024]u8 align(8) = undefined; + while (true) { + const n = try directory.read(&info_buffer); + if (n == 0) return; + const info: *const uefi.protocol.File.Info.File = @ptrCast(@alignCast(&info_buffer)); + const name16 = info.getFileName(); + + // Convert the (ASCII in practice) UTF-16 name; skip "." and "..". + var name_buffer: [initial_ramdisk.maximum_name]u8 = undefined; + var name_length: usize = 0; + while (name16[name_length] != 0) : (name_length += 1) { + if (name_length == name_buffer.len) return error.NameTooLong; + const c = name16[name_length]; + if (c > 0x7F) return error.UnsupportedName; + name_buffer[name_length] = @intCast(c); + } + const name = name_buffer[0..name_length]; + if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) continue; + + if (info.attribute.directory) { + if (depth == maximum_tree_depth) continue; + var child_prefix: [initial_ramdisk.maximum_name]u8 = undefined; + const child = try std.fmt.bufPrint(&child_prefix, "{s}/{s}", .{ prefix, name }); + const child_directory = try directory.open(name16, .read, .{}); + defer _ = child_directory.close() catch {}; + try walkDirectory(bs, child_directory, child, depth + 1, list, count); + continue; + } + + if (depth == 0) continue; // /system/kernel — already loaded, not bundled + if (count.* == maximum_bundled) return error.TooManyBinaries; + + var entry: *Bundled = &list[count.*]; + const path = try std.fmt.bufPrint(&entry.path, "{s}/{s}", .{ prefix, name }); + entry.path_len = path.len; + + const file = try directory.open(name16, .read, .{}); + defer _ = file.close() catch {}; + entry.data = try readWholeFile(bs, file); + count.* += 1; + } +} + +/// Read an open file completely into a fresh pool buffer that survives the +/// handoff (LoaderData is classified reserved, so the kernel identity-maps it). +fn readWholeFile(bs: *uefi.tables.BootServices, file: *uefi.protocol.File) ![]align(8) u8 { try file.setPosition(seek_end); const size: usize = @intCast(try file.getPosition()); try file.setPosition(0); if (size == 0) return error.EmptyFile; - const image = try bs.allocatePool(.loader_data, size); // survives the handoff - + const buffer = try bs.allocatePool(.loader_data, size); var read_total: usize = 0; while (read_total < size) { - const n = try file.read(image[read_total..]); + const n = try file.read(buffer[read_total..]); if (n == 0) return error.UnexpectedEof; read_total += n; } - return image[0..size]; -} - -/// Ferry the init program (/system/services/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_information: *BootInformation) !void { - const image = try loadFile(bs, init_file_name); - boot_information.init_base = @intFromPtr(image.ptr); - boot_information.init_len = image.len; - progress("EFI: /system/services/init loaded\r\n"); -} - -/// Ferry the initial_ramdisk (the VFS server + drivers) to the kernel, same as init. -fn loadInitialRamdisk(bs: *uefi.tables.BootServices, boot_information: *BootInformation) !void { - const image = try loadFile(bs, initial_ramdisk_file_name); - boot_information.initial_ramdisk_base = @intFromPtr(image.ptr); - boot_information.initial_ramdisk_len = image.len; - progress("EFI: initial_ramdisk loaded\r\n"); + return buffer[0..size]; } /// Validate the ELF, copy every PT_LOAD segment to its physical address, and diff --git a/build.zig b/build.zig index b9fa826..2b389d2 100644 --- a/build.zig +++ b/build.zig @@ -221,18 +221,21 @@ fn addKernel( return exe; } -/// Assemble the bootable FAT32 image (the in-repo Python builder) holding what -/// the firmware and loader need off the ESP: the EFI stub, `kernel`, `init`, and -/// the initial-ramdisk. Factored so the serial-enabled `run-x86-64` variant can -/// bundle its own serial kernel while sharing the loader, init, and ramdisk — all -/// built once per invocation (the loader's boot breadcrumbs and init's heartbeat -/// both follow the top-level -Dserial). Returns the image's LazyPath. +/// One user binary and its FHS home on the boot volume (and in zig-out). +const BundledBinary = struct { path: []const u8, binary: std.Build.LazyPath }; + +/// Assemble the bootable FAT32 image (the in-repo Python builder) holding the +/// EFI stub, the kernel, and every user binary at its FHS path — the volume's +/// /system tree IS the system image; the EFI loader walks it at boot and builds +/// the in-RAM initial_ramdisk from it. Factored so the serial-enabled +/// `run-x86-64` variant can bundle its own serial kernel while sharing the +/// loader and user tree (the loader's boot breadcrumbs and init's heartbeat both +/// follow the top-level -Dserial). Returns the image's LazyPath. fn addBootImage( b: *std.Build, kernel_bin: std.Build.LazyPath, efi_bin: std.Build.LazyPath, - init_bin: std.Build.LazyPath, - initial_ramdisk_img: std.Build.LazyPath, + bundled: []const BundledBinary, ) std.Build.LazyPath { const mk_fat = b.addSystemCommand(&.{"python3"}); mk_fat.addFileArg(b.path("tools/make-fat-image.py")); @@ -242,10 +245,10 @@ fn addBootImage( mk_fat.addFileArg(efi_bin); mk_fat.addArg("system/kernel"); mk_fat.addFileArg(kernel_bin); - mk_fat.addArg("system/services/init"); - mk_fat.addFileArg(init_bin); - mk_fat.addArg("boot/initial-ramdisk.img"); - mk_fat.addFileArg(initial_ramdisk_img); + for (bundled) |item| { + mk_fat.addArg(item.path); + mk_fat.addFileArg(item.binary); + } return fat_image; } @@ -443,8 +446,9 @@ pub fn build(b: *std.Build) void { }, }); - // The initial_ramdisk container format, shared by the kernel (unpacks it) and the - // build-time packer tools/make-initial-ramdisk.py (produces it). No dependencies. + // The initial_ramdisk container format, shared by the kernel (unpacks it) and + // the EFI loader (packs it in RAM from the boot volume's /system tree). No + // dependencies. const initial_ramdisk_module = b.addModule("initial-ramdisk", .{ .root_source_file = b.path("system/initial-ramdisk.zig"), }); @@ -502,13 +506,11 @@ pub fn build(b: *std.Build) void { const init_options = b.addOptions(); init_options.addOption(bool, "serial", serial); programModule(init_exe).addImport("build_options", init_options.createModule()); - const init_install = b.addInstallArtifact(init_exe, .{ .dest_dir = .{ .override = .{ .custom = "system/services" } } }); - b.getInstallStep().dependOn(&init_install.step); - // --- initial_ramdisk: a bundle of extra user binaries (VFS server + drivers) --- - // Each is built by the same user-binary recipe, then packed into one image by - // the host-side make-initial-ramdisk tool. The bootloader ferries the image to the kernel, - // which unpacks it and spawns each program (system/initial-ramdisk.zig). + // --- the rest of the /system tree: services, drivers, test fixtures --- + // Each is built by the same user-binary recipe and laid out at its FHS path on + // the boot volume (see `bundled` below). The EFI loader walks the tree at boot + // and hands the kernel an in-RAM initial_ramdisk of it (system/initial-ramdisk.zig). const vfs_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs", "system/services/vfs/vfs.zig"); const vfstest_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "vfs-test", "system/services/vfs/vfs-test.zig"); const ps2_bus_exe = addUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); @@ -584,94 +586,51 @@ pub fn build(b: *std.Build) void { // (docs/threading.md). Built threaded so its shared-memory poll is real. const thread_test_exe = addThreadedUserBinary(b, kernel_target, runtime_module, mmio_module, xkeyboard_config_module, acpi_ids_module, "thread-test", "system/services/thread-test/thread-test.zig"); - // Pack the user binaries into the initial_ramdisk image with the host-side Python tool - // (the container format is trivial, and Python sidesteps std API churn). Args: - // make-initial-ramdisk.py [ ]... — one name/file pair per binary. - const mk_run = b.addSystemCommand(&.{"python3"}); - mk_run.addFileArg(b.path("tools/make-initial-ramdisk.py")); - const initial_ramdisk_img = mk_run.addOutputFileArg("initial-ramdisk.img"); - mk_run.addArg("vfs"); - mk_run.addFileArg(vfs_exe.getEmittedBin()); - mk_run.addArg("vfs-test"); - mk_run.addFileArg(vfstest_exe.getEmittedBin()); - mk_run.addArg("ps2-bus"); - mk_run.addFileArg(ps2_bus_exe.getEmittedBin()); - mk_run.addArg("ps2-keyboard"); - mk_run.addFileArg(ps2_keyboard_exe.getEmittedBin()); - mk_run.addArg("ps2-mouse"); - mk_run.addFileArg(ps2_mouse_exe.getEmittedBin()); - mk_run.addArg("usb-xhci-bus"); - mk_run.addFileArg(usb_xhci_bus_exe.getEmittedBin()); - mk_run.addArg("usb-hid-keyboard"); - mk_run.addFileArg(usb_hid_keyboard_exe.getEmittedBin()); - mk_run.addArg("usb-hid-mouse"); - mk_run.addFileArg(usb_hid_mouse_exe.getEmittedBin()); - mk_run.addArg("usb-storage"); - mk_run.addFileArg(usb_storage_exe.getEmittedBin()); - mk_run.addArg("fat"); - mk_run.addFileArg(fat_exe.getEmittedBin()); - mk_run.addArg("fat-test"); - mk_run.addFileArg(fat_test_exe.getEmittedBin()); - mk_run.addArg("display"); - mk_run.addFileArg(display_exe.getEmittedBin()); - mk_run.addArg("display-demo"); - mk_run.addFileArg(display_demo_exe.getEmittedBin()); - mk_run.addArg("virtio-gpu"); - mk_run.addFileArg(virtio_gpu_exe.getEmittedBin()); - mk_run.addArg("shm-server"); - mk_run.addFileArg(shm_server_exe.getEmittedBin()); - mk_run.addArg("shm-client"); - mk_run.addFileArg(shm_client_exe.getEmittedBin()); - mk_run.addArg("pci-bus"); - mk_run.addFileArg(pci_bus_exe.getEmittedBin()); - mk_run.addArg("crash-test"); - mk_run.addFileArg(crash_test_exe.getEmittedBin()); - mk_run.addArg("thread-test"); - mk_run.addFileArg(thread_test_exe.getEmittedBin()); - mk_run.addArg("device-list"); - mk_run.addFileArg(device_list_exe.getEmittedBin()); - mk_run.addArg("discovery"); - mk_run.addFileArg(discovery_exe.getEmittedBin()); - mk_run.addArg("device-manager"); - mk_run.addFileArg(device_manager_exe.getEmittedBin()); - mk_run.addArg("input"); - mk_run.addFileArg(input_exe.getEmittedBin()); - mk_run.addArg("input-source"); - mk_run.addFileArg(input_source_exe.getEmittedBin()); - mk_run.addArg("input-test"); - mk_run.addFileArg(input_test_exe.getEmittedBin()); - mk_run.addArg("args-echo"); - mk_run.addFileArg(args_echo_exe.getEmittedBin()); - mk_run.addArg("process-test"); - mk_run.addFileArg(process_test_exe.getEmittedBin()); - mk_run.addArg("log-flush"); - mk_run.addFileArg(log_flush_exe.getEmittedBin()); + // Every user binary and its FHS home on the boot volume. There is no packed + // ramdisk artifact any more: make-fat-image.py lays each binary out at this + // path on the image, and the EFI loader walks /system at boot and builds the + // in-RAM initial_ramdisk table from the tree — the volume's file structure is + // the single source of truth. Entry names (and hence argv[0] and task names) + // are these paths with a leading slash. + const bundled = [_]BundledBinary{ + .{ .path = "system/services/init", .binary = init_exe.getEmittedBin() }, + .{ .path = "system/services/vfs", .binary = vfs_exe.getEmittedBin() }, + .{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() }, + .{ .path = "system/services/display", .binary = display_exe.getEmittedBin() }, + .{ .path = "system/services/display-demo", .binary = display_demo_exe.getEmittedBin() }, + .{ .path = "system/services/device-manager", .binary = device_manager_exe.getEmittedBin() }, + .{ .path = "system/services/input", .binary = input_exe.getEmittedBin() }, + .{ .path = "system/services/discovery", .binary = discovery_exe.getEmittedBin() }, + .{ .path = "system/services/log-flush", .binary = log_flush_exe.getEmittedBin() }, + .{ .path = "system/drivers/ps2-bus", .binary = ps2_bus_exe.getEmittedBin() }, + .{ .path = "system/drivers/ps2-keyboard", .binary = ps2_keyboard_exe.getEmittedBin() }, + .{ .path = "system/drivers/ps2-mouse", .binary = ps2_mouse_exe.getEmittedBin() }, + .{ .path = "system/drivers/usb-xhci-bus", .binary = usb_xhci_bus_exe.getEmittedBin() }, + .{ .path = "system/drivers/usb-hid-keyboard", .binary = usb_hid_keyboard_exe.getEmittedBin() }, + .{ .path = "system/drivers/usb-hid-mouse", .binary = usb_hid_mouse_exe.getEmittedBin() }, + .{ .path = "system/drivers/usb-storage", .binary = usb_storage_exe.getEmittedBin() }, + .{ .path = "system/drivers/virtio-gpu", .binary = virtio_gpu_exe.getEmittedBin() }, + .{ .path = "system/drivers/pci-bus", .binary = pci_bus_exe.getEmittedBin() }, + .{ .path = "system/tests/vfs-test", .binary = vfstest_exe.getEmittedBin() }, + .{ .path = "system/tests/fat-test", .binary = fat_test_exe.getEmittedBin() }, + .{ .path = "system/tests/shm-server", .binary = shm_server_exe.getEmittedBin() }, + .{ .path = "system/tests/shm-client", .binary = shm_client_exe.getEmittedBin() }, + .{ .path = "system/tests/crash-test", .binary = crash_test_exe.getEmittedBin() }, + .{ .path = "system/tests/device-list", .binary = device_list_exe.getEmittedBin() }, + .{ .path = "system/tests/input-source", .binary = input_source_exe.getEmittedBin() }, + .{ .path = "system/tests/input-test", .binary = input_test_exe.getEmittedBin() }, + .{ .path = "system/tests/args-echo", .binary = args_echo_exe.getEmittedBin() }, + .{ .path = "system/tests/process-test", .binary = process_test_exe.getEmittedBin() }, + .{ .path = "system/tests/thread-test", .binary = thread_test_exe.getEmittedBin() }, + }; - // Also install the packed binaries to their FHS homes, so zig-out is a true image - // of the filesystem — even though at boot they arrive inside the initial-ramdisk. - for ([_]struct { *std.Build.Step.Compile, []const u8 }{ - .{ vfs_exe, "system/services" }, - .{ device_manager_exe, "system/services" }, - .{ input_exe, "system/services" }, - .{ ps2_bus_exe, "system/drivers" }, - .{ ps2_keyboard_exe, "system/drivers" }, - .{ ps2_mouse_exe, "system/drivers" }, - .{ usb_xhci_bus_exe, "system/drivers" }, - .{ usb_hid_keyboard_exe, "system/drivers" }, - .{ usb_hid_mouse_exe, "system/drivers" }, - .{ usb_storage_exe, "system/drivers" }, - .{ fat_exe, "system/services" }, - .{ display_exe, "system/services" }, - .{ log_flush_exe, "system/services" }, - }) |entry| { - const step = b.addInstallArtifact(entry[0], .{ .dest_dir = .{ .override = .{ .custom = entry[1] } } }); - b.getInstallStep().dependOn(&step.step); + // Install every bundled binary to its FHS home, so zig-out is a true image of + // the filesystem — the same tree make-fat-image.py lays out on the boot volume. + for (bundled) |item| { + const install = b.addInstallFileWithDir(item.binary, .prefix, item.path); + b.getInstallStep().dependOn(&install.step); } - // The initial-ramdisk itself installs to /boot (with the loaders). - const initial_ramdisk_install = b.addInstallFile(initial_ramdisk_img, "boot/initial-ramdisk.img"); - b.getInstallStep().dependOn(&initial_ramdisk_install.step); - // Boot methods live in boot/, one per way of getting the kernel running. // Each is its own binary/entry (a loader is built for its own target); today // that's UEFI for x86-64, with room for e.g. a device-tree path for the Pis. @@ -692,8 +651,10 @@ pub fn build(b: *std.Build) void { }), .optimize = optimize, .imports = &.{ - // The bootloader speaks only the handoff contract — never the user ABI. + // The bootloader speaks the handoff contract and the ramdisk + // container it packs the /system tree into — never the user ABI. .{ .name = "boot-handoff", .module = boot_handoff_module }, + .{ .name = "initial-ramdisk", .module = initial_ramdisk_module }, .{ .name = "build_options", .module = loader_options_module }, }, }), @@ -706,11 +667,11 @@ pub fn build(b: *std.Build) void { // --- danos-usb.img: the bootable FAT32 USB image --- // Format a real FAT32 image (the in-repo Python builder, no external tools) - // holding exactly what the firmware and bootloader need off the ESP: the EFI - // stub, the kernel, init, and the initial-ramdisk. QEMU presents this image as - // a USB mass-storage device the guest boots from (see run-x86-64 and the test - // harness), and the danos fat driver mounts the same image at /mnt/usb. - const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img); + // holding the EFI stub, the kernel, and the whole /system tree of user + // binaries at their FHS paths. QEMU presents this image as a USB mass-storage + // device the guest boots from (see run-x86-64 and the test harness), and the + // danos fat driver mounts the same image at /mnt/usb. + const fat_image = addBootImage(b, exe.getEmittedBin(), efiexe.getEmittedBin(), &bundled); const fat_image_install = b.addInstallFile(fat_image, "danos-usb.img"); b.getInstallStep().dependOn(&fat_image_install.step); @@ -719,7 +680,7 @@ pub fn build(b: *std.Build) void { // log captured to serial0 — without baking serial into the image users flash. // Built lazily (only when `run-x86-64` is requested), and never installed. const exe_serial = addKernel(b, kernel_target, optimize, kernel_modules, test_case, true); - const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), init_exe.getEmittedBin(), initial_ramdisk_img); + const fat_image_serial = addBootImage(b, exe_serial.getEmittedBin(), efiexe.getEmittedBin(), &bundled); // `zig build check-fat-image` — validate the produced image is a real FAT32 // with the EFI stub present (the builder's own --verify, no external tools). diff --git a/system/boot-handoff.zig b/system/boot-handoff.zig index 2c10024..5480b79 100644 --- a/system/boot-handoff.zig +++ b/system/boot-handoff.zig @@ -147,15 +147,11 @@ pub const BootInformation = extern struct { /// A device-tree boot path leaves this 0 and (later) fills a `device_tree_blob` /// field instead, so the kernel discovers devices without knowing what booted it. acpi_rsdp: u64 = 0, - /// The raw `/system/services/init` ELF image, read off the boot volume by the loader - /// into memory that survives the handoff (classified reserved, so the kernel - /// identity-maps it and never allocates over it). 0/0 = no init found — the - /// kernel boots without user space. Grows into a full initial_ramdisk handoff later. - init_base: u64 = 0, - init_len: u64 = 0, - /// The initial_ramdisk 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 initial_ramdisk. See system/initial-ramdisk.zig. + /// The initial_ramdisk image: every user binary from the boot volume's /system + /// tree (init included), packed by the loader into memory that survives the + /// handoff (classified reserved, so the kernel identity-maps it and never + /// allocates over it). Entries are named by full FHS path. 0/0 = no binaries + /// found — the kernel boots without user space. See system/initial-ramdisk.zig. initial_ramdisk_base: u64 = 0, initial_ramdisk_len: u64 = 0, }; diff --git a/system/initial-ramdisk.zig b/system/initial-ramdisk.zig index 487c280..e74f208 100644 --- a/system/initial-ramdisk.zig +++ b/system/initial-ramdisk.zig @@ -1,7 +1,13 @@ -//! The initial_ramdisk (initial ramdisk) container format — shared by the build-time -//! packer (tools/make-initial-ramdisk.py) 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. +//! The initial_ramdisk (initial ramdisk) container format — built in RAM by the +//! bootloader (boot/efi.zig walks the boot volume's /system tree) and unpacked by +//! the kernel. 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. +//! +//! v2: entry names are full FHS paths ("/system/services/init"), 64 bytes — the +//! same limit as a task name (abi.maximum_process_name), so a path-named task is +//! never truncated. The boot volume's file tree is the single source of truth; +//! this image is only the loader→kernel handoff snapshot of it. //! //! Layout: //! Header (magic, count) @@ -10,8 +16,14 @@ const std = @import("std"); -/// "DNRD" — identifies a danos initial_ramdisk image. -pub const magic: u32 = 0x444E5244; +/// "DNR2" — identifies a danos initial_ramdisk image, format v2 (path names). +/// The v1 magic ("DNRD", basename entries) is rejected: a stale image should +/// fail loudly at Reader.init, not misparse names. +pub const magic: u32 = 0x32524E44; + +/// Entry name capacity. Matches abi.maximum_process_name so a spawned task can +/// always carry its full binary path as its name. +pub const maximum_name = 64; pub const Header = extern struct { magic: u32, @@ -19,11 +31,17 @@ pub const Header = extern struct { }; pub const Entry = extern struct { - name: [32]u8, // NUL-padded file name (basename) + name: [maximum_name]u8, // NUL-padded FHS path, e.g. "/system/services/init" offset: u64, // byte offset of the blob within the image len: u64, // blob length in bytes }; +/// The basename of a path: the final component after the last '/'. +pub fn basename(path: []const u8) []const u8 { + const i = std.mem.lastIndexOfScalar(u8, path, '/') orelse return path; + return path[i + 1 ..]; +} + /// A validated view over an initial_ramdisk image. `init` checks the magic and that the /// entry table fits; `entry` bounds-checks each blob against the image. pub const Reader = struct { @@ -48,11 +66,74 @@ pub const Reader = struct { 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 name_field = self.image[off .. off + maximum_name]; 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)], }; } + + /// Look a binary up by name: an exact path match wins; otherwise a unique + /// basename match ("fat" finds "/system/services/fat") keeps pre-path callers + /// working. The returned Item's name is always the stored full path. + pub fn find(self: Reader, name: []const u8) ?Item { + var i: u32 = 0; + while (i < self.count) : (i += 1) { + const item = self.entry(i) orelse continue; + if (std.mem.eql(u8, item.name, name)) return item; + } + i = 0; + while (i < self.count) : (i += 1) { + const item = self.entry(i) orelse continue; + if (std.mem.eql(u8, basename(item.name), name)) return item; + } + return null; + } }; + +// --- tests (host) ----------------------------------------------------------- + +fn testImage(buffer: []u8, entries: []const struct { name: []const u8, blob: []const u8 }) []const u8 { + const table_end = @sizeOf(Header) + entries.len * @sizeOf(Entry); + var offset: usize = table_end; + std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = magic, .count = @intCast(entries.len) }; + for (entries, 0..) |e, i| { + var record = Entry{ .name = @splat(0), .offset = offset, .len = e.blob.len }; + @memcpy(record.name[0..e.name.len], e.name); + std.mem.bytesAsValue(Entry, buffer[@sizeOf(Header) + i * @sizeOf(Entry) ..][0..@sizeOf(Entry)]).* = record; + @memcpy(buffer[offset..][0..e.blob.len], e.blob); + offset += e.blob.len; + } + return buffer[0..offset]; +} + +test "find matches exact path, then unique basename; name is the stored path" { + var buffer: [1024]u8 = undefined; + const image = testImage(&buffer, &.{ + .{ .name = "/system/services/init", .blob = "INIT" }, + .{ .name = "/system/drivers/ps2-bus", .blob = "PS2" }, + }); + const rd = Reader.init(image).?; + + const by_path = rd.find("/system/services/init").?; + try std.testing.expectEqualStrings("/system/services/init", by_path.name); + try std.testing.expectEqualStrings("INIT", by_path.blob); + + const by_base = rd.find("ps2-bus").?; + try std.testing.expectEqualStrings("/system/drivers/ps2-bus", by_base.name); + try std.testing.expectEqualStrings("PS2", by_base.blob); + + try std.testing.expect(rd.find("no-such-binary") == null); +} + +test "v1 magic is rejected" { + var buffer: [64]u8 = @splat(0); + std.mem.bytesAsValue(Header, buffer[0..@sizeOf(Header)]).* = .{ .magic = 0x444E5244, .count = 0 }; + try std.testing.expect(Reader.init(&buffer) == null); +} + +test "basename" { + try std.testing.expectEqualStrings("fat", basename("/system/services/fat")); + try std.testing.expectEqualStrings("fat", basename("fat")); +} diff --git a/system/kernel/kernel.zig b/system/kernel/kernel.zig index fcc7aec..09b9179 100644 --- a/system/kernel/kernel.zig +++ b/system/kernel/kernel.zig @@ -329,20 +329,16 @@ fn kmain(boot_information: *const BootInformation) noreturn { // service supervisor and the device manager spawns the drivers it discovers. publishInitialRamdisk(boot_information); - // Hand over to user space: load /system/services/init (read off the boot volume by - // the loader) and spawn it as a real ring-3 process, PID 1. As the supervisor it - // brings up the system services (the VFS server, the device manager); the device - // manager then discovers the hardware and spawns each driver. init runs on its own - // address space, preemptively — this boot context becomes the BSP's idle loop. - if (boot_information.init_len != 0) { - status("/system/kernel: starting /system/services/init...\n"); - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; - process.spawnProcess(image, 4, &.{"/system/services/init"}) catch |err| { - statusPrint("/system/kernel: /system/services/init failed to load: {s}\n", .{@errorName(err)}); - }; - } else { - status("no /system/services/init on the boot volume.\n"); - } + // Hand over to user space: spawn /system/services/init out of the ramdisk as a + // real ring-3 process, PID 1 — it rides the same table as every other binary. + // As the supervisor it brings up the system services (the VFS server, the device + // manager); the device manager then discovers the hardware and spawns each + // driver. init runs on its own address space, preemptively — this boot context + // becomes the BSP's idle loop. + status("/system/kernel: starting /system/services/init...\n"); + process.spawnBundled("/system/services/init") catch |err| { + statusPrint("/system/kernel: /system/services/init failed to start: {s}\n", .{@errorName(err)}); + }; // Become the idle task: drop below every real task and halt until an // interrupt. The timer keeps preempting into init and any other work. diff --git a/system/kernel/process.zig b/system/kernel/process.zig index 6b17970..9696667 100644 --- a/system/kernel/process.zig +++ b/system/kernel/process.zig @@ -140,6 +140,16 @@ pub fn setInitialRamdisk(image: []const u8) void { ramdisk_image = image; } +/// Spawn a bundled binary from the kernel by path. Used exactly once, to start +/// /system/services/init (PID 1) — every other spawn goes through the +/// `system_spawn` syscall. +pub fn spawnBundled(name: []const u8) !void { + const image = ramdisk_image orelse return error.NoInitialRamdisk; + const rd = initial_ramdisk.Reader.init(image) orelse return error.BadInitialRamdisk; + const item = rd.find(name) orelse return error.NotBundled; + try spawnProcess(item.blob, 4, &.{item.name}); +} + /// The system_call surface, dispatched on the saved system_call number (`abi.SystemCall`). /// This is the microkernel-minimal set — memory + scheduling only; file/device /// I/O will arrive as IPC to user-space servers (docs/syscall.md). The result is @@ -656,8 +666,11 @@ fn systemSpawn(state: *architecture.CpuState) void { const rd = initial_ramdisk.Reader.init(image) orelse return fail(state); const name = @as([*]const u8, @ptrFromInt(ptr))[0..len]; + // Exact path first, basename fallback second; either way argv[0] (and hence + // the task name, and the log ring's attribution) is the stored full path. + const item = rd.find(name) orelse return fail(state); // no bundled binary by that name var argv: [maximum_arguments][]const u8 = undefined; - argv[0] = name; + argv[0] = item.name; var argc: usize = 1; if (arguments_len != 0) { const blob = @as([*]const u8, @ptrFromInt(arguments_ptr))[0..arguments_len]; @@ -669,15 +682,8 @@ fn systemSpawn(state: *architecture.CpuState) void { } } - var i: u32 = 0; - while (i < rd.count) : (i += 1) { - const item = rd.entry(i) orelse continue; - if (!std.mem.eql(u8, item.name, name)) continue; - const child = spawnProcessSupervised(item.blob, 4, argv[0..argc], t.id, exit_endpoint) catch return fail(state); - architecture.setSystemCallResult(state, child); - return; - } - fail(state); // no bundled binary by that name + const child = spawnProcessSupervised(item.blob, 4, argv[0..argc], t.id, exit_endpoint) catch return fail(state); + architecture.setSystemCallResult(state, child); } /// thread_spawn(entry, stack_top, arg) -> tid: start a task that shares the **caller's** diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index c27f154..519e0c8 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -1327,12 +1327,12 @@ fn procWorker() void { /// strongest cheap proof of address-space isolation. fn processTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: process\n", .{}); - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0) { + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); process.write_count = 0; process.write_from_user = false; @@ -1414,12 +1414,12 @@ fn spawnFaultingProcess() ?u32 { /// time the harness out. fn faultRecoveryTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: fault-recovery\n", .{}); - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0) { + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); process.write_count = 0; process.fault_kill_count = 0; @@ -1550,7 +1550,7 @@ fn threadJoinTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "join" })) true else |_| false; break; } @@ -1594,7 +1594,7 @@ fn threadFutexTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "futex" })) true else |_| false; break; } @@ -1642,7 +1642,7 @@ fn threadMutexTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "mutex" })) true else |_| false; break; } @@ -1683,7 +1683,7 @@ fn threadIdTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "id" })) true else |_| false; break; } @@ -1726,7 +1726,7 @@ fn threadAllocTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "alloc" })) true else |_| false; break; } @@ -1769,7 +1769,7 @@ fn threadTlsTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "tls" })) true else |_| false; break; } @@ -1811,7 +1811,7 @@ fn threadRwlockTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "thread-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "thread-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "thread-test", "rwlock" })) true else |_| false; break; } @@ -1876,12 +1876,12 @@ fn taskReapTest(boot_information: *const BootInformation) void { /// (write + sleep), and stays alive rather than exiting. fn initTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: init\n", .{}); - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0) { + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); process.write_count = 0; const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |err| blk: { log("DANOS-INIT-ERR: {s}\n", .{@errorName(err)}); @@ -1912,12 +1912,12 @@ fn initTest(boot_information: *const BootInformation) void { /// learns how big a buffer to bring). fn processListTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: process-list\n", .{}); - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0) { + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); var spawned: u32 = 0; if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {} @@ -1963,13 +1963,12 @@ fn processListTest(boot_information: *const BootInformation) void { /// harness out rather than passing vacuously. fn processKillTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: process-kill\n", .{}); - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { - check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0); + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; const rd = initial_ramdisk.Reader.init(ramdisk) orelse { check("initial_ramdisk image is valid", false); @@ -2024,7 +2023,7 @@ fn processKillTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "process-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue; spinner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "spinner" }, me, endpoint) catch 0; break; } @@ -2041,7 +2040,7 @@ fn processKillTest(boot_information: *const BootInformation) void { i = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "args-echo")) continue; + if (!eql(initial_ramdisk.basename(item.name), "args-echo")) continue; clean = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "clean-exit" }, me, endpoint) catch 0; break; } @@ -2089,12 +2088,12 @@ fn claimReleaseTest(boot_information: *const BootInformation) void { check("cleanup released owner 222", devices_broker.ownerOf(1) == null); // The death-path wiring: a real process dies holding a claim. - check("bootloader handed over /system/services/init", boot_information.init_len != 0); - if (boot_information.init_len == 0) { + const image = bundledInit(boot_information) orelse { + check("initial_ramdisk carries /system/services/init", false); result(); return; - } - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; + }; + check("initial_ramdisk carries /system/services/init", true); const me = scheduler.currentId(); const endpoint = ipcsync.createIpcEndpoint() orelse { check("exit endpoint allocated", false); @@ -2148,7 +2147,7 @@ fn vfsClientDeathTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "vfs-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "vfs-test")) continue; client = process.spawnProcessSupervised(item.blob, 4, &.{ "vfs-test", "park" }, me, endpoint) catch 0; break; } @@ -2209,7 +2208,7 @@ fn signalsTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "process-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue; runner = process.spawnProcessSupervised(item.blob, 4, &.{ "process-test", "signal-run" }, scheduler.currentId(), null) catch 0; break; } @@ -2257,7 +2256,7 @@ fn driverRestartTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-restart" }, scheduler.currentId(), null) catch 0; break; } @@ -2295,7 +2294,7 @@ fn usbReportTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0; break; } @@ -2327,7 +2326,7 @@ fn deviceListTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-usb-restart" }, scheduler.currentId(), null) catch 0; break; } @@ -2368,7 +2367,7 @@ fn pciScanTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-pci-restart" }, scheduler.currentId(), null) catch 0; break; } @@ -2452,8 +2451,8 @@ fn usbStorageTest(boot_information: *const BootInformation) void { /// the fat mount and the client's success. fn fatMountTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: fat-mount\n", .{}); - if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { - check("bootloader handed over init and the initial_ramdisk", false); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over the initial_ramdisk", false); result(); return; } @@ -2464,8 +2463,7 @@ fn fatMountTest(boot_information: *const BootInformation) void { return; }; process.setInitialRamdisk(ramdisk); - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; - const init_ok = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false; + const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false; check("init spawned (boots the tree, incl. the fat server)", init_ok); check("fat-test client spawned", spawnNamed(rd, "fat-test")); result(); @@ -2473,30 +2471,28 @@ fn fatMountTest(boot_information: *const BootInformation) void { fn bootServiceTreeTest(boot_information: *const BootInformation, comptime label: []const u8) void { log("DANOS-TEST-BEGIN: " ++ label ++ "\n", .{}); - if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { - check("bootloader handed over init and the initial_ramdisk", false); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over the initial_ramdisk", false); result(); return; } const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; process.setInitialRamdisk(ramdisk); - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; - const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false; + const spawned = if (process.spawnBundled("/system/services/init")) true else |_| false; check("init spawned (boots vfs, input, device-manager, and the USB chain)", spawned); result(); } fn orderlyShutdownTest(boot_information: *const BootInformation) void { log("DANOS-TEST-BEGIN: orderly-shutdown\n", .{}); - if (boot_information.init_len == 0 or boot_information.initial_ramdisk_len == 0) { - check("bootloader handed over init and the initial_ramdisk", false); + if (boot_information.initial_ramdisk_len == 0) { + check("bootloader handed over the initial_ramdisk", false); result(); return; } const ramdisk = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; process.setInitialRamdisk(ramdisk); - const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len]; - const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false; + const spawned = if (process.spawnBundled("/system/services/init")) true else |_| false; check("init spawned as PID root of user space", spawned); result(); } @@ -2524,7 +2520,7 @@ fn acpiReportTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; _ = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; spawned = true; break; @@ -2562,7 +2558,7 @@ fn acpiParseTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "discovery")) continue; + if (!eql(initial_ramdisk.basename(item.name), "discovery")) continue; _ = process.spawnProcessSupervised(item.blob, 4, &.{ "discovery", "1" }, scheduler.currentId(), null) catch 0; spawned = true; break; @@ -2597,7 +2593,7 @@ fn supervisionTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "process-test")) continue; + if (!eql(initial_ramdisk.basename(item.name), "process-test")) continue; started = if (process.spawnProcess(item.blob, 4, &.{ "process-test", "run" })) true else |_| false; break; } @@ -2921,7 +2917,7 @@ fn virtioGpuTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; break; } @@ -2962,7 +2958,7 @@ fn displayNativeTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{"device-manager"}, scheduler.currentId(), null) catch 0; break; } @@ -3006,7 +3002,7 @@ fn displayReattachTest(boot_information: *const BootInformation) void { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (!eql(item.name, "device-manager")) continue; + if (!eql(initial_ramdisk.basename(item.name), "device-manager")) continue; manager = process.spawnProcessSupervised(item.blob, 4, &.{ "device-manager", "test-scanout-restart" }, scheduler.currentId(), null) catch 0; break; } @@ -3055,7 +3051,7 @@ fn argsTest(boot_information: *const BootInformation) void { while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield(); scheduler.setPriority(4); - const expected = "args: args-echo alpha beta-42\n"; + const expected = "args: /system/tests/args-echo alpha beta-42\n"; const echoed = process.write_len == expected.len and eql(process.write_buffer[0..process.write_len], expected); if (!echoed and process.write_len > 0) log("DANOS-ARGS: got \"{s}\"\n", .{process.write_buffer[0..process.write_len]}); check("argv arrived intact (argv[0] = name, argv[1..] = spawn arguments)", echoed); @@ -3065,11 +3061,21 @@ fn argsTest(boot_information: *const BootInformation) void { /// Spawn the initial_ramdisk binary named `name` as a ring-3 process. Returns false if it /// isn't in the image or fails to load. +/// The init ELF image out of the initial_ramdisk — init rides the table like +/// every other binary since the loader packs the whole /system tree. +fn bundledInit(boot_information: *const BootInformation) ?[]const u8 { + if (boot_information.initial_ramdisk_len == 0) return null; + const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; + const rd = initial_ramdisk.Reader.init(image) orelse return null; + const item = rd.find("/system/services/init") orelse return null; + return item.blob; +} + fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool { var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (eql(item.name, name)) { + if (eql(initial_ramdisk.basename(item.name), name)) { return if (process.spawnProcess(item.blob, 4, &.{item.name})) true else |_| false; } } @@ -3082,7 +3088,7 @@ fn spawnNamedWithArg(rd: initial_ramdisk.Reader, name: []const u8, arg: []const var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - if (eql(item.name, name)) { + if (eql(initial_ramdisk.basename(item.name), name)) { return if (process.spawnProcess(item.blob, 4, &.{ item.name, arg })) true else |_| false; } } diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index b2bae5b..f138ddd 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -56,8 +56,8 @@ const virtio_gpu_pci_class: u64 = pci_class.ClassCode.pack(.{ /// its registered id as argv[1]. fn pciDriverForIdentity(identity: u64) ?[]const u8 { return switch (identity) { - xhci_pci_class => "usb-xhci-bus", - virtio_gpu_pci_class => "virtio-gpu", + xhci_pci_class => "/system/drivers/usb-xhci-bus", + virtio_gpu_pci_class => "/system/drivers/virtio-gpu", else => null, }; } @@ -67,8 +67,8 @@ fn pciDriverForIdentity(identity: u64) ?[]const u8 { /// nodes the kernel used to build). ps2-bus is a singleton that finds both its /// devices by hid once spawned, so keyboard and mouse map to the same name. fn hidDriverFor(hid: []const u8) ?[]const u8 { - if (std.mem.eql(u8, hid, "PNP0303")) return "ps2-bus"; // PS/2 keyboard - if (std.mem.eql(u8, hid, "PNP0F13")) return "ps2-bus"; // PS/2 mouse + if (std.mem.eql(u8, hid, "PNP0303")) return "/system/drivers/ps2-bus"; // PS/2 keyboard + if (std.mem.eql(u8, hid, "PNP0F13")) return "/system/drivers/ps2-bus"; // PS/2 mouse return null; } @@ -95,9 +95,9 @@ fn usbDriverForIdentity(identity: u64) ?[]const u8 { @intFromEnum(usb_ids.mass_storage.Protocol.bulk_only), ); return switch (identity) { - keyboard => "usb-hid-keyboard", - mouse => "usb-hid-mouse", - storage => "usb-storage", + keyboard => "/system/drivers/usb-hid-keyboard", + mouse => "/system/drivers/usb-hid-mouse", + storage => "/system/drivers/usb-storage", else => null, }; } @@ -429,7 +429,7 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, capability: ?runtime writeLine("/system/services/device-manager: hello from {s} (device {d})\n", .{ driver.name(), hello.device_id }); // Resilience drill (V6): once, kill the virtio-gpu driver a moment after it hellos, so // the normal restart policy respawns it — the compositor must survive and re-attach. - if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "virtio-gpu")) { + if (test_scanout_restart_mode and !test_scanout_killed and std.mem.eql(u8, driver.name(), "/system/drivers/virtio-gpu")) { test_scanout_killed = true; test_kill_pid = sender; test_kill_due_ns = system.clock() + 1_500_000_000; @@ -498,7 +498,7 @@ fn onChildAdded(message: []const u8, reply: []u8, sender: u32) usize { // Only the xHCI reporter is the drill's victim — pci-bus also reports // now, and whichever finishes second must not trigger the kill. if (driverByProcess(sender)) |driver| { - if (std.mem.eql(u8, driver.name(), "usb-xhci-bus")) { + if (std.mem.eql(u8, driver.name(), "/system/drivers/usb-xhci-bus")) { // Delayed, not immediate: the device-list scenario's subscriber // needs a window to enumerate and subscribe before the events. test_usb_killed = true; diff --git a/system/services/init/init.zig b/system/services/init/init.zig index 5bad503..51d9d98 100644 --- a/system/services/init/init.zig +++ b/system/services/init/init.zig @@ -27,11 +27,19 @@ const build_options = @import("build_options"); /// the log-flush one-shot writes it once at boot. const log_path = "/mnt/usb/DANOS.LOG"; -/// The system services init brings up at boot, in order. This is init's policy — the -/// microkernel keeps such choices in user space, not the kernel. Drivers are absent -/// on purpose: the device manager owns those. (A future init reads this from a -/// manifest under /system/services instead of a hardcoded list.) -const boot_services = [_][]const u8{ "vfs", "input", "device-manager", "fat", "display", "display-demo" }; +/// The system services init brings up at boot, in order, by binary path. This is +/// init's policy — the microkernel keeps such choices in user space, not the +/// kernel. Drivers are absent on purpose: the device manager owns those. (A +/// future init reads this from a manifest under /system/services instead of a +/// hardcoded list.) +const boot_services = [_][]const u8{ + "/system/services/vfs", + "/system/services/input", + "/system/services/device-manager", + "/system/services/fat", + "/system/services/display", + "/system/services/display-demo", +}; /// The live process id of each boot service (0 = not running), indexed by its position /// in `boot_services`, plus how many times init has restarted it. init supervises these: @@ -84,7 +92,7 @@ pub fn main() void { // and forget: it polls for the mount itself, and is deliberately NOT one of // init's supervised children (a transient one-shot must not be stopped-and- // waited-for during shutdown). - _ = runtime.system.spawn("log-flush"); + _ = runtime.system.spawn("/system/services/log-flush"); // Subscribe to power events (retry: the power service registers well after // init starts). Best-effort — without it, a `terminate` signal still diff --git a/system/services/process-test/process-test.zig b/system/services/process-test/process-test.zig index 6dd0ce8..3b1a80e 100644 --- a/system/services/process-test/process-test.zig +++ b/system/services/process-test/process-test.zig @@ -147,8 +147,8 @@ pub fn main(init: runtime.process.Init) void { const spinner = runtime.system.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner"); runtime.system.sleep(100); // let the sleeper block and the spinner get a core - if (!listed(sleeper, "process-test")) fail("sleeper not in process_enumerate"); - if (!listed(spinner, "process-test")) fail("spinner not in process_enumerate"); + if (!listed(sleeper, "/system/tests/process-test")) fail("sleeper not in process_enumerate"); + if (!listed(spinner, "/system/tests/process-test")) fail("spinner not in process_enumerate"); // Kills that must be refused: a kernel task (id 0), and an id that was never // issued — both -ESRCH. (-EPERM needs a second supervisor; the kernel-level @@ -167,8 +167,8 @@ pub fn main(init: runtime.process.Init) void { if (!runtime.system.kill(spinner)) fail("kill spinner"); if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification"); - if (listed(sleeper, "process-test")) fail("sleeper still listed after kill"); - if (listed(spinner, "process-test")) fail("spinner still listed after kill"); + if (listed(sleeper, "/system/tests/process-test")) fail("sleeper still listed after kill"); + if (listed(spinner, "/system/tests/process-test")) fail("spinner still listed after kill"); // M17.2: both children were killed by us, and the reason says so — the whole // restart-policy input, read through the runtime like a real supervisor would. diff --git a/tools/make-initial-ramdisk.py b/tools/make-initial-ramdisk.py deleted file mode 100644 index 88a0194..0000000 --- a/tools/make-initial-ramdisk.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/env python3 -"""Build-time initial_ramdisk packer. Concatenates user binaries into one image the -bootloader ferries to the kernel. - -Usage: make-initial-ramdisk.py [ ]... - -Image layout (little-endian), mirroring src/user/proto/initial-ramdisk.zig: - Header : magic u32 ("DNRD"=0x444E5244), count u32 - Entry*N : name [32]u8 (NUL-padded), offset u64, len u64 - blobs : each entry's file bytes at its offset -""" -import struct -import sys - -MAGIC = 0x444E5244 -HEADER = struct.Struct(" int: - out_path = sys.argv[1] - rest = sys.argv[2:] - if len(rest) % 2 != 0: - sys.stderr.write("usage: make-initial-ramdisk.py [ ]...\n") - return 2 - items = [(rest[i], rest[i + 1]) for i in range(0, len(rest), 2)] - - table_end = HEADER.size + len(items) * ENTRY.size - entries = b"" - blobs = [] - off = table_end - for name, path in items: - with open(path, "rb") as f: - data = f.read() - entries += ENTRY.pack(name.encode()[:31], off, len(data)) - blobs.append(data) - off += len(data) - - with open(out_path, "wb") as f: - f.write(HEADER.pack(MAGIC, len(items))) - f.write(entries) - for b in blobs: - f.write(b) - return 0 - - -if __name__ == "__main__": - sys.exit(main())