From f023f1cfd6b174cca188511c19ef35286413f4dd Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:11:07 +0100 Subject: [PATCH] reorg: move test fixtures to test/system/services (source + boot volume) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 11 QEMU-suite fixtures lived mixed into system/services/ with their binaries bundled at /system/tests/. Now the repo path is the boot path, like every real service: test/system/services/. fat-test moves out of the fat server's directory into its own; display-demo stays a boot service. - kernel VFS: setInitialRamdisk derives one read-only initrd mount per top-level tree named by the ramdisk entry paths (/system, /test), registers ancestors generically with self-parented roots, and refuses backend shadowing of any initrd tree - EFI loader: the fallback walk also enumerates \test (optional — a volume without fixtures still boots); manifest and capsule unchanged - path literals: vfs-test self-open + create probe, process-test process_enumerate matches, the args-echo argv[0] expectation; the kvfs case now covers the /test root end to end - docs: DFHS /test rows, tree diagrams, loader prose, and the location convention gain the third home; fixed the input-source link 100/100 QEMU cases pass. --- README.md | 3 +- boot/efi.zig | 36 +++++++----- build.zig | 55 ++++++++++--------- docs/README.md | 8 ++- docs/coding-standards.md | 10 ++-- docs/danos-file-system-hierarchy-FSH.md | 7 ++- docs/display.md | 2 +- docs/efi.md | 6 +- docs/system-image.md | 19 ++++--- docs/system-requirements.md | 5 +- system/kernel/log-ring.zig | 2 +- system/kernel/tests.zig | 31 ++++++++--- system/kernel/vfs.zig | 42 +++++++------- .../system}/services/args-echo/args-echo.zig | 0 .../services/crash-test/crash-test.zig | 0 .../services/device-list/device-list.zig | 0 .../system/services/fat-test}/fat-test.zig | 2 +- .../services/input-source/input-source.zig | 2 +- .../services/input-test/input-test.zig | 2 +- .../services/process-test/process-test.zig | 8 +-- .../shared-memory-client.zig | 2 +- .../shared-memory-server.zig | 2 +- .../services/thread-test/thread-test.zig | 0 .../system}/services/vfs-test/vfs-test.zig | 14 ++--- 24 files changed, 150 insertions(+), 108 deletions(-) rename {system => test/system}/services/args-echo/args-echo.zig (100%) rename {system => test/system}/services/crash-test/crash-test.zig (100%) rename {system => test/system}/services/device-list/device-list.zig (100%) rename {system/services/fat => test/system/services/fat-test}/fat-test.zig (98%) rename {system => test/system}/services/input-source/input-source.zig (96%) rename {system => test/system}/services/input-test/input-test.zig (96%) rename {system => test/system}/services/process-test/process-test.zig (95%) rename {system => test/system}/services/shared-memory-client/shared-memory-client.zig (93%) rename {system => test/system}/services/shared-memory-server/shared-memory-server.zig (92%) rename {system => test/system}/services/thread-test/thread-test.zig (100%) rename {system => test/system}/services/vfs-test/vfs-test.zig (82%) diff --git a/README.md b/README.md index 9c95044..e385e0a 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,8 @@ zig build Produces a FHS-shaped `zig-out/` that *is* the danos filesystem and the boot volume: the UEFI bootloader at `zig-out/EFI/BOOT/BOOTX64.efi`, the kernel at `zig-out/system/kernel`, init at `zig-out/system/services/init`, drivers under -`zig-out/system/drivers/`, and the initial-ramdisk at `zig-out/boot/`. +`zig-out/system/drivers/`, the test fixtures under `zig-out/test/system/services/`, +and the initial-ramdisk at `zig-out/boot/`. ## Release media diff --git a/boot/efi.zig b/boot/efi.zig index 7216bdb..3e09bed 100644 --- a/boot/efi.zig +++ b/boot/efi.zig @@ -16,11 +16,12 @@ const MemoryMapSlice = uefi.tables.MemoryMapSlice; /// The kernel image: /system/kernel. const kernel_file_name = std.unicode.utf8ToUtf16LeStringLiteral("system\\kernel"); -/// 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). +/// The user binaries: everything under /system except the kernel itself, plus +/// the test fixtures under /test. The loader walks both trees and packs them +/// 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"); +const test_directory_name = std.unicode.utf8ToUtf16LeStringLiteral("test"); /// Physical page size, and the sentinel UEFI uses to seek to end-of-file. const page_size = 4096; @@ -364,16 +365,17 @@ fn handoff(cr3: u64, entry: usize, boot_information: *const BootInformation) nor unreachable; } -// --- the /system tree -> initial_ramdisk ------------------------------------ +// --- the /system and /test trees -> 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). +/// How deep the walk goes below a tree root ("/system/services/x" is depth 1, +/// "/test/system/services/x" is depth 2). 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. +/// One binary discovered under a walked tree: 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, @@ -389,10 +391,10 @@ const Bundled = struct { /// 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. +/// 2. No manifest: ENUMERATE the /system and /test trees. 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; @@ -426,6 +428,11 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat const system_directory = try root.open(system_directory_name, .read, .{}); defer _ = system_directory.close() catch {}; try walkDirectory(bs, system_directory, "/system", 0, &list, &count); + // The /test tree is optional: a stick without fixtures still boots. + if (root.open(test_directory_name, .read, .{})) |test_directory| { + defer _ = test_directory.close() catch {}; + try walkDirectory(bs, test_directory, "/test", 0, &list, &count); + } else |_| {} } if (count == 0) return error.NoBinaries; @@ -452,7 +459,7 @@ fn loadSystemTree(bs: *uefi.tables.BootServices, boot_information: *BootInformat boot_information.initial_ramdisk_base = @intFromPtr(image.ptr); boot_information.initial_ramdisk_len = total; - log("EFI: /system tree loaded, starting the kernel\r\n"); + log("EFI: boot tree loaded, starting the kernel\r\n"); } /// The boot capsule: the bundled binaries as one v2 initial_ramdisk image. @@ -522,7 +529,8 @@ fn loadByManifest(bs: *uefi.tables.BootServices, root: *uefi.protocol.File, list /// 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. +/// has already consumed and which is not a spawnable user binary (/test has no +/// top-level files, so the skip is a no-op there). fn walkDirectory( bs: *uefi.tables.BootServices, directory: *uefi.protocol.File, diff --git a/build.zig b/build.zig index 48fc285..2631186 100644 --- a/build.zig +++ b/build.zig @@ -648,11 +648,11 @@ pub fn build(b: *std.Build) void { init_options.addOption(bool, "diagnose", diagnose); programModule(init_exe).addImport("build_options", init_options.createModule()); - // --- the rest of the /system tree: services, drivers, test fixtures --- + // --- the rest of the boot tree: /system services and 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 vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "system/services/vfs-test/vfs-test.zig"); + const vfstest_exe = addUserBinary(b, kernel_target, &default_imports, "vfs-test", "test/system/services/vfs-test/vfs-test.zig"); const ps2_bus_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-bus", "system/drivers/ps2-bus/ps2-bus.zig"); const ps2_keyboard_exe = addUserBinary(b, kernel_target, &default_imports, "ps2-keyboard", "system/drivers/ps2-bus/keyboard.zig"); programModule(ps2_keyboard_exe).addImport("input-protocol", input_protocol_module); @@ -696,9 +696,9 @@ pub fn build(b: *std.Build) void { programModule(virtio_gpu_exe).addImport("pci", pci_module); // library/device/pci — the claimed-function view programModule(virtio_gpu_exe).addImport("display-protocol", display_protocol_module); programModule(virtio_gpu_exe).addImport("scanout-protocol", scanout_protocol_module); - const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "system/services/shared-memory-server/shared-memory-server.zig"); - const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "system/services/shared-memory-client/shared-memory-client.zig"); - const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "system/services/fat/fat-test.zig"); + const shared_memory_server_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-server", "test/system/services/shared-memory-server/shared-memory-server.zig"); + const shared_memory_client_exe = addUserBinary(b, kernel_target, &default_imports, "shared-memory-client", "test/system/services/shared-memory-client/shared-memory-client.zig"); + const fat_test_exe = addUserBinary(b, kernel_target, &default_imports, "fat-test", "test/system/services/fat-test/fat-test.zig"); const pci_bus_exe = addUserBinary(b, kernel_target, &default_imports, "pci-bus", "system/drivers/pci-bus/pci-bus.zig"); programModule(pci_bus_exe).addImport("device-manager-protocol", device_manager_protocol_module); // The PCI bus driver decodes each function's class triple to human names in its @@ -706,9 +706,9 @@ pub fn build(b: *std.Build) void { programModule(pci_bus_exe).addImport("pci-class", pci_class_module); // A test fixture, not a real driver: hellos to the device manager, then faults — // what the driver-restart scenario drives the crash-loop cap with. - const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "system/services/crash-test/crash-test.zig"); + const crash_test_exe = addUserBinary(b, kernel_target, &default_imports, "crash-test", "test/system/services/crash-test/crash-test.zig"); programModule(crash_test_exe).addImport("device-manager-protocol", device_manager_protocol_module); - const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "system/services/device-list/device-list.zig"); + const device_list_exe = addUserBinary(b, kernel_target, &default_imports, "device-list", "test/system/services/device-list/device-list.zig"); programModule(device_list_exe).addImport("device-manager-protocol", device_manager_protocol_module); // The discovery service: one swappable process per firmware // (docs/discovery.md), bundled under the neutral ramdisk name @@ -738,21 +738,22 @@ pub fn build(b: *std.Build) void { // source, and a subscriber that doubles as the `input` test's oracle. See docs/input.md. const input_exe = addUserBinary(b, kernel_target, &default_imports, "input", "system/services/input/input.zig"); programModule(input_exe).addImport("input-protocol", input_protocol_module); - const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "system/services/input-source/input-source.zig"); - const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "system/services/input-test/input-test.zig"); - const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "system/services/args-echo/args-echo.zig"); - const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "system/services/process-test/process-test.zig"); + const input_source_exe = addUserBinary(b, kernel_target, &default_imports, "input-source", "test/system/services/input-source/input-source.zig"); + const input_test_exe = addUserBinary(b, kernel_target, &default_imports, "input-test", "test/system/services/input-test/input-test.zig"); + const args_echo_exe = addUserBinary(b, kernel_target, &default_imports, "args-echo", "test/system/services/args-echo/args-echo.zig"); + const process_test_exe = addUserBinary(b, kernel_target, &default_imports, "process-test", "test/system/services/process-test/process-test.zig"); const logger_exe = addUserBinary(b, kernel_target, &default_imports, "logger", "system/services/logger/logger.zig"); // The first multi-threaded binary: exercises runtime.Thread over the thread ABI // (docs/threading.md). Built threaded so its shared-memory poll is real. - const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "system/services/thread-test/thread-test.zig"); + const thread_test_exe = addThreadedUserBinary(b, kernel_target, &default_imports, "thread-test", "test/system/services/thread-test/thread-test.zig"); // 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. + // path on the image, and the EFI loader walks /system and /test at boot and + // builds the in-RAM initial_ramdisk table from the trees — 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. Test fixtures mirror their + // repo home: test/system/services/ in the source tree IS the boot path. const bundled = [_]BundledBinary{ .{ .path = "system/services/init", .binary = init_exe.getEmittedBin() }, .{ .path = "system/services/fat", .binary = fat_exe.getEmittedBin() }, @@ -771,17 +772,17 @@ pub fn build(b: *std.Build) void { .{ .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/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() }, - .{ .path = "system/tests/shared-memory-client", .binary = shared_memory_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() }, + .{ .path = "test/system/services/vfs-test", .binary = vfstest_exe.getEmittedBin() }, + .{ .path = "test/system/services/fat-test", .binary = fat_test_exe.getEmittedBin() }, + .{ .path = "test/system/services/shared-memory-server", .binary = shared_memory_server_exe.getEmittedBin() }, + .{ .path = "test/system/services/shared-memory-client", .binary = shared_memory_client_exe.getEmittedBin() }, + .{ .path = "test/system/services/crash-test", .binary = crash_test_exe.getEmittedBin() }, + .{ .path = "test/system/services/device-list", .binary = device_list_exe.getEmittedBin() }, + .{ .path = "test/system/services/input-source", .binary = input_source_exe.getEmittedBin() }, + .{ .path = "test/system/services/input-test", .binary = input_test_exe.getEmittedBin() }, + .{ .path = "test/system/services/args-echo", .binary = args_echo_exe.getEmittedBin() }, + .{ .path = "test/system/services/process-test", .binary = process_test_exe.getEmittedBin() }, + .{ .path = "test/system/services/thread-test", .binary = thread_test_exe.getEmittedBin() }, }; // The boot manifest: the FHS path of every bundled binary, one per line. The diff --git a/docs/README.md b/docs/README.md index 5613b00..347187f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -220,6 +220,7 @@ addressed as **`system/services/init`** — the repeated leaf resolves away: |----------------------------------------|--------------------------------------------| | `system/services/init/init.zig` | `system/services/init` → `/system/services/init` | | `system/drivers/ps2-bus/ps2-bus.zig` | `system/drivers/ps2-bus` → `/system/drivers/ps2-bus` | +| `test/system/services/vfs-test/vfs-test.zig` | `test/system/services/vfs-test` → `/test/system/services/vfs-test` | | `library/device/pci/pci.zig` | `library/device/pci` (the `pci` module) | In **source**, a sub-project is a directory so it can hold many files — the entry is @@ -258,7 +259,11 @@ library/ → /lib libraries, one sub-directory each protocol/ driver↔service wire contracts (vfs block display scanout input power device-manager usb-transfer), one module per directory boot/ → /boot the loaders -tools/ test/ host-side build + QEMU test harness +test/ → /test the test tree: the QEMU harness (qemu_test.py, host-side) + system/services/ beside the on-image test fixtures — vfs-test/ thread-test/ + crash-test/ … — whose repo path IS their boot-volume path + (/test/system/services/) +tools/ host-side build scripts ``` **Wire protocols live in `library/protocol/`**, one module per directory @@ -314,5 +319,6 @@ exception in [coding-standards.md](coding-standards.md) applies to that seam. | Service clients (a program's view of a service) and device clients | `library/client/` (display, input), `library/device/driver` | | System services (init, the `fat` filesystem, the device-manager) | `system/services/` | | Device drivers, one sub-project each (`pci-bus`, `ps2-bus`, `usb-xhci-bus` bus drivers) | `system/drivers/` | +| On-image test fixtures for the QEMU cases (`vfs-test`, `crash-test`, `thread-test`, …) → `/test/system/services` | `test/system/services/` | | Build + `run-x86-64` (QEMU/OVMF) + `release-x86-64` (the flashable ISO) | `build.zig` | | QEMU integration test harness | `test/qemu_test.py` | diff --git a/docs/coding-standards.md b/docs/coding-standards.md index 23b3c34..935b89a 100644 --- a/docs/coding-standards.md +++ b/docs/coding-standards.md @@ -97,8 +97,9 @@ Three, and only three. That's all — no Unix-abbreviation exception. The source directories are full words (`system`, `library`, not `src`/`lib`), and there is no daemon `d` suffix: a driver -lives in `system/drivers/` and a service in `system/services/`, so the *location* -already says what it is. Encoding the role in the name too (`busd`, `fatd`) is +lives in `system/drivers/`, a service in `system/services/`, and a test fixture in +`test/system/services/` (the repo path *is* its path on the boot volume), so the +*location* already says what it is. Encoding the role in the name too (`busd`, `fatd`) is redundant — the program is just `ps2-bus`, `fat`. Don't put in a name what its directory already tells you. @@ -142,8 +143,9 @@ conventions above — `snake_case` — because it's an identifier, not a filenam **A sub-project's entry point repeats its directory's name** — `init/init.zig`, `pci/pci.zig`, `ps2-bus/ps2-bus.zig` — and the sub-project is addressed by the -*directory* (`system/services/init`, `library/device/pci`), with the repeated leaf -resolving away. See the repository-layout section of [README.md](README.md). +*directory* (`system/services/init`, `library/device/pci`, +`test/system/services/vfs-test`), with the repeated leaf resolving away. See the +repository-layout section of [README.md](README.md). ## Named values, not magic numbers diff --git a/docs/danos-file-system-hierarchy-FSH.md b/docs/danos-file-system-hierarchy-FSH.md index 7c6e887..2038736 100644 --- a/docs/danos-file-system-hierarchy-FSH.md +++ b/docs/danos-file-system-hierarchy-FSH.md @@ -20,6 +20,8 @@ Most modern Unix and Unix-like operating systems follow the FHS. DanOS has its o | /system/drivers | driver binaries, one sub-project each (e.g. /system/drivers/pci-bus, /system/drivers/ps2-bus) | | /system/services | system-service binaries — init, the FAT server, and other user-mode servers (e.g. /system/services/init, /system/services/fat) | | /system/kernel | the kernel image | +| /test | Test fixtures for the QEMU integration suite. Read-only and initrd-backed like /system, and its layout likewise mirrors the source tree (the repo's test/ directory). Present on development and test images; a volume without it still boots. | +| /test/system/services | test-fixture binaries (e.g. /test/system/services/vfs-test, /test/system/services/thread-test) — the same path in the repo source tree and on the boot volume | | /tmp | Directory for temporary files (see also /var/tmp). Often not preserved between system reboots and may be severely size-restricted. | | /usr | Secondary hierarchy for read-only user data; contains the majority of (multi-)user utilities and applications. Should be shareable and read-only. | | /var | Variable files: files whose content is expected to continually change during normal operation of the system, such as logs, spool files, and temporary e-mail files. | @@ -52,8 +54,9 @@ IPC endpoint, and subsequent reads and writes are calls against it. Resolve-to-e is exactly what the kernel's `fs_resolve` already does for any mounted backend, and `FileStatus.kind` is the field that marks a device node; **what is not implemented today is `/dev` itself** — no service mounts it. (The flat eight-node ramfs this section once -described is retired: the kernel-resident VFS root in `system/kernel/vfs.zig` serves the -read-only `/system` initrd mount with real directories and node kinds, and filesystem +described is retired: the kernel-resident VFS root in `system/kernel/vfs.zig` serves a +read-only initrd mount per top-level tree — `/system`, and `/test` on images that carry +the fixtures — with real directories and node kinds, and filesystem backends such as the FAT server mount the rest.) The three sections below describe the intended shape, and are honest about which parts the kernel can already support. diff --git a/docs/display.md b/docs/display.md index ca7db33..5cf2484 100644 --- a/docs/display.md +++ b/docs/display.md @@ -297,7 +297,7 @@ test/qemu_test.py `), each layering on the last: layer — logging `display: compositor self-check ok`. - **`display-demo`** — the full pipeline from a separate process: the hardware-free [`display-demo`](../system/services/display-demo/) client (the - [`input-source`](../system/services/input-source/) analog) drives layers — a wallpaper and + [`input-source`](../test/system/services/input-source/) analog) drives layers — a wallpaper and a sliding rectangle — through the layer client API and heartbeats `display-demo: ok`, proving a frame travelled client → compositor → screen, exactly as the [input test](input.md) proves an event travels source → service → subscriber. It draws diff --git a/docs/efi.md b/docs/efi.md index 9e49d43..8b2c91f 100644 --- a/docs/efi.md +++ b/docs/efi.md @@ -67,8 +67,8 @@ captures the **ACPI RSDP** from the UEFI configuration table (while boot services are still up), loads the system binaries into an in-RAM **initial ramdisk** (`loadSystemTree` — normally a single read of the pre-packed `boot\system.img` capsule, which already *is* the ramdisk wire format; it falls -back to opening each manifest-listed path, and walks the `/system` tree only as -a last resort for hand-assembled sticks — the capsule's format, builder, and +back to opening each manifest-listed path, and walks the `/system` and `/test` +trees only as a last resort for hand-assembled sticks — the capsule's format, builder, and fallback chain are documented in [system-image.md](system-image.md). Best-effort either way — a kernel-only volume still boots), and builds the **bootstrap page tables** the kernel starts life on (`buildBootstrapTables`), all before the jump: @@ -196,7 +196,7 @@ power on -> queryFramebuffer (via GOP: EDID native res, setMode, describe fb) -> loadKernel (read system/kernel ELF, load PT_LOAD segments low, .text at 0x100000) -> loadSystemTree (read the boot\system.img capsule as the in-RAM initial ramdisk; - fallbacks: manifest-listed paths, then a /system tree walk) + fallbacks: manifest-listed paths, then a /system + /test tree walk) -> buildBootstrapTables (identity + physmap + higher-half kernel mappings) -> exitBootServices (retry until the memory-map key holds) -> handoff: load bootstrap CR3, jump to e_entry, boot_information pointer in RDI diff --git a/docs/system-image.md b/docs/system-image.md index ddc7615..5d31faf 100644 --- a/docs/system-image.md +++ b/docs/system-image.md @@ -10,7 +10,7 @@ v2), written to disk ahead of time. The EFI loader reads it in a single sequential pass and hands the bytes to the kernel unmodified. The capsule is a *performance artifact*, not a source of truth. The boot -volume's `/system` file tree remains the canonical layout (see +volume's `/system` and `/test` file trees remain the canonical layout (see [danos-file-system-hierarchy-FSH.md](danos-file-system-hierarchy-FSH.md)); the capsule is a pre-baked snapshot of the same binaries, derived from the same build graph, so the running system is identical whether the loader read the @@ -58,8 +58,9 @@ blobs... each entry's file bytes, at its offset within the image Three artifacts are derived from that same list, in the same build graph, so they cannot drift apart: -1. **The tree**: each binary installed at its FHS path (`zig-out/system/...`, - mirrored onto the FAT boot volume by `tools/make-fat-image.py`). +1. **The tree**: each binary installed at its FHS path (`zig-out/system/...` + and `zig-out/test/...`, mirrored onto the FAT boot volume by + `tools/make-fat-image.py`). 2. **The manifest** (`system/manifest`): the FHS path of every bundled binary, one per line — the loader's per-file fallback input. 3. **The capsule**: `tools/pack-system-image.py` packs the same binaries into @@ -81,7 +82,8 @@ same in-RAM ramdisk image: 2. **The manifest** — read `system\manifest` and open each listed path *by name*. FAT name lookup is case-insensitive and firmware-portable, unlike directory enumeration. The loader assembles the v2 image in RAM itself. -3. **The tree walk** — enumerate `/system` recursively. Last resort for +3. **The tree walk** — enumerate `/system` and `/test` recursively (`/test` + is optional: a stick without fixtures still boots). Last resort for hand-assembled sticks with neither file: some firmware FAT drivers return bare 8.3 names uppercase from enumeration, which is why this is the fallback and not the primary path. @@ -105,10 +107,11 @@ consumers: pre-path callers — and loads them as fresh ring-3 processes. The stored path becomes the task's name. - **The VFS root** (`vfs.zig`, `setInitialRamdisk`): the image is mounted as - the kernel-backed, read-only `/system` mount. Directory nodes are derived - from the entry paths (the unique parents), so `/system` is listable and its - files readable over the normal VFS protocol — the FHS boot tree every - process sees comes straight out of the capsule bytes. + kernel-backed, read-only mounts — one per top-level tree named by the entry + paths, so `/system` and, when the fixtures are bundled, `/test`. Directory + nodes are derived from the entry paths (the unique parents), so the trees + are listable and their files readable over the normal VFS protocol — the + FHS boot tree every process sees comes straight out of the capsule bytes. The image is never copied after the handoff and never mutated: the initrd is immutable, which is what makes the VFS's node serving lock-free. diff --git a/docs/system-requirements.md b/docs/system-requirements.md index 5aa40bd..a01adf6 100644 --- a/docs/system-requirements.md +++ b/docs/system-requirements.md @@ -119,8 +119,9 @@ hypervisor configured for UEFI firmware and an xHCI USB controller. - The loader reads `/system/kernel` off the FAT boot volume, then loads user space: a prebuilt `boot\system.img` capsule ([system-image.md](system-image.md)) when present, otherwise it walks - the volume's `/system` tree (init included) into the initial ramdisk. The - kernel can boot "kernel-only" without either. (`efi.zig:16`, `efi.zig:68`) + the volume's `/system` and optional `/test` trees (init included) into the + initial ramdisk. The kernel can boot "kernel-only" without either. + (`efi.zig:16`, `efi.zig:68`) ## Interrupt controller diff --git a/system/kernel/log-ring.zig b/system/kernel/log-ring.zig index 2aa2b45..0aa5759 100644 --- a/system/kernel/log-ring.zig +++ b/system/kernel/log-ring.zig @@ -187,7 +187,7 @@ test "wrap reclaims whole records and keeps tail on a boundary" { while (i < 200) : (i += 1) { var message: [64]u8 = undefined; const m = std.fmt.bufPrint(&message, "line {d} padding padding padding", .{i}) catch unreachable; - _ = ring.append(1, "/system/tests/writer", .info, i, m, false); + _ = ring.append(1, "/test/system/services/writer", .info, i, m, false); } try std.testing.expect(ring.head - ring.tail <= 4096); diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 9ef99ae..644e79c 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -3105,7 +3105,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: /system/tests/args-echo alpha beta-42\n"; + const expected = "args: /test/system/services/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); @@ -3116,7 +3116,7 @@ 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. +/// every other binary since the loader packs the whole boot 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]; @@ -3137,7 +3137,7 @@ fn kernelVfsTest(boot_information: *const BootInformation) void { return; } const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len]; - process.setInitialRamdisk(image); // also seeds the kernel VFS /system mount + process.setInitialRamdisk(image); // also seeds the kernel VFS /system and /test mounts // A file resolves to a kernel node token; its status and bytes are served. const resolved = kernel_vfs.resolvePath("/system/services/init", false); @@ -3151,21 +3151,22 @@ fn kernelVfsTest(boot_information: *const BootInformation) void { check("its first bytes are an ELF magic", n == 4 and header[0] == 0x7f and header[1] == 'E' and header[2] == 'L' and header[3] == 'F'); } - // Directories resolve and enumerate: /system lists services/drivers/tests. + // Directories resolve and enumerate: /system lists services/drivers. const root_directory = kernel_vfs.resolvePath("/system", false); check("/system resolves to a directory node", root_directory == .kernel_node); var saw_services = false; var saw_drivers = false; + var saw_stray_in_root = false; var saw_files_in_services = false; if (root_directory == .kernel_node) { var cursor: u64 = 0; var name: [64]u8 = undefined; while (kernel_vfs.nodeReaddir(root_directory.kernel_node, cursor, &name)) |entry| : (cursor += 1) { - if (eql(name[0..entry.name_len], "services")) saw_services = true; - if (eql(name[0..entry.name_len], "drivers")) saw_drivers = true; + if (eql(name[0..entry.name_len], "services")) saw_services = true else if (eql(name[0..entry.name_len], "drivers")) saw_drivers = true else saw_stray_in_root = true; } } check("readdir /system yields services and drivers", saw_services and saw_drivers); + check("readdir /system yields nothing else (no /test leakage)", !saw_stray_in_root); const services = kernel_vfs.resolvePath("/system/services", false); if (services == .kernel_node) { var cursor: u64 = 0; @@ -3176,10 +3177,26 @@ fn kernelVfsTest(boot_information: *const BootInformation) void { } check("readdir /system/services yields init", saw_files_in_services); - // Refusals: unknown paths, and create-intent on the immutable initrd. + // The /test tree is a second initrd root, resolvable and enumerable like + // /system (the fixtures live at /test/system/services, mirroring the repo). + const test_root = kernel_vfs.resolvePath("/test", false); + check("/test resolves to a directory node", test_root == .kernel_node); + check("/test/system/services/args-echo resolves to a kernel node", kernel_vfs.resolvePath("/test/system/services/args-echo", false) == .kernel_node); + var saw_test_subtree = false; + if (test_root == .kernel_node) { + var cursor: u64 = 0; + var name: [64]u8 = undefined; + while (kernel_vfs.nodeReaddir(test_root.kernel_node, cursor, &name)) |entry| : (cursor += 1) { + if (eql(name[0..entry.name_len], "system")) saw_test_subtree = true; + } + } + check("readdir /test yields system", saw_test_subtree); + + // Refusals: unknown paths, and create-intent on the immutable initrd trees. check("an unknown path does not resolve", kernel_vfs.resolvePath("/system/services/no-such", false) == .not_found); check("an unmounted absolute path does not resolve", kernel_vfs.resolvePath("/elsewhere", false) == .not_found); check("create on /system is refused (read-only)", kernel_vfs.resolvePath("/system/services/new-file", true) == .not_found); + check("create on /test is refused (read-only)", kernel_vfs.resolvePath("/test/system/services/new-file", true) == .not_found); result(); } diff --git a/system/kernel/vfs.zig b/system/kernel/vfs.zig index 9ca296c..c0f684d 100644 --- a/system/kernel/vfs.zig +++ b/system/kernel/vfs.zig @@ -4,7 +4,7 @@ //! the mechanism is **resolve + redirect**: //! //! - `fs_resolve(path)` walks the mount table. A path under a KERNEL-backed -//! mount (the initrd at /system, the scratch ram nodes) resolves to a +//! mount (the initrd trees at /system and /test, the scratch ram nodes) resolves to a //! stateless node TOKEN served directly by `fs_node` (read/status/readdir //! with copy-out). A path under a USERSPACE mount (the fat server at //! /mnt/usb and /var) resolves to the backend's ENDPOINT: the kernel @@ -85,7 +85,7 @@ const maximum_directories = 8; const Directory = struct { path: [maximum_prefix]u8 = undefined, path_len: usize = 0, - parent: usize = 0, // index into `directories`; 0 is /system itself + parent: usize = 0, // index into `directories`; a top-level tree root is its own parent fn slice(self: *const Directory) []const u8 { return self.path[0..self.path_len]; @@ -122,37 +122,35 @@ fn parentOf(path: []const u8) []const u8 { // --- boot wiring ------------------------------------------------------------- -/// Publish the initrd as the kernel-backed /system mount and derive its bounded -/// directory table (the unique parents of the entry paths). Called once at boot. +/// Publish the initrd as kernel-backed mounts — one per top-level tree its +/// entry paths name (/system, /test) — and derive the bounded directory table +/// (every ancestor directory of the entry paths). Called once at boot. pub fn setInitialRamdisk(image: []const u8) void { ramdisk_image = image; - installMount("/system", .kernel_initrd, null, ""); - - // Directory 0 is /system itself. - directories[0] = .{ .parent = 0 }; - @memcpy(directories[0].path[0..7], "/system"); - directories[0].path_len = 7; - directory_count = 1; + directory_count = 0; const rd = initial_ramdisk.Reader.init(image) orelse return; var i: u32 = 0; while (i < rd.count) : (i += 1) { const item = rd.entry(i) orelse continue; - // Register every ancestor directory strictly below /system. + // Register every ancestor directory, top-level tree roots included. var parent = parentOf(item.name); - while (parent.len > 7) : (parent = parentOf(parent)) { + while (parent.len > 1) : (parent = parentOf(parent)) { if (directoryIndex(parent) == null and directory_count < maximum_directories) { var d = &directories[directory_count]; @memcpy(d.path[0..parent.len], parent); d.path_len = parent.len; - d.parent = 0; // fixed up below once all exist + d.parent = directory_count; // fixed up below once all exist directory_count += 1; } } } // Parent links (a second pass so out-of-order registration doesn't matter). - for (directories[1..directory_count]) |*d| { - d.parent = directoryIndex(parentOf(d.slice())) orelse 0; + // A top-level root keeps itself as parent and becomes an initrd mount. + for (directories[0..directory_count], 0..) |*d, index| { + const parent = parentOf(d.slice()); + d.parent = directoryIndex(parent) orelse index; + if (parent.len == 1) installMount(d.slice(), .kernel_initrd, null, ""); } } @@ -193,7 +191,7 @@ pub const Resolved = union(enum) { }; /// Longest-prefix match over the mount table, then per-kind resolution. -/// `create`-intent on the immutable /system fails here (EROFS-style). +/// `create`-intent on the immutable initrd trees fails here (EROFS-style). pub fn resolvePath(path: []const u8, wants_create: bool) Resolved { if (!isAbsolute(path)) { return .{ .not_found = {} }; // bare names have no kernel namespace (ramfs retired) @@ -295,11 +293,11 @@ pub fn nodeReaddir(node_token: u64, cursor: u64, name_out: []u8) ?struct { heade const self_path = directories[@intCast(directory_index)].slice(); var index: u64 = 0; - // Subdirectories whose parent is this directory. + // Subdirectories whose parent is this directory (roots are self-parented, + // so they never appear as anyone's child). for (directories[0..directory_count], 0..) |*d, i| { if (i == directory_index) continue; if (d.parent != directory_index) continue; - if (i == 0) continue; if (index == cursor) { const name = d.slice()[self_path.len + 1 ..]; const n = @min(name.len, name_out.len); @@ -330,11 +328,13 @@ pub fn nodeReaddir(node_token: u64, cursor: u64, name_out: []u8) ?struct { heade /// Mount `backend` at `prefix` with an optional backend-side `rewrite` prefix. /// The endpoint reference is taken by the caller (process.zig bumps it); refuses -/// shadowing or replacing /system. +/// shadowing or replacing the initrd trees (/system, /test). pub fn mountBackend(prefix: []const u8, backend: *ipc.Endpoint, rewrite: []const u8) bool { if (!isAbsolute(prefix) or prefix.len < 2 or prefix.len > maximum_prefix) return false; if (rewrite.len > maximum_rewrite) return false; - if (underMount(prefix, "/system") != null) return false; // the initrd is not shadowable + for (&mounts) |*m| { // the initrd trees are not shadowable + if (m.used and m.kind == .kernel_initrd and underMount(prefix, m.prefixSlice()) != null) return false; + } installMount(prefix, .backend, backend, rewrite); return true; } diff --git a/system/services/args-echo/args-echo.zig b/test/system/services/args-echo/args-echo.zig similarity index 100% rename from system/services/args-echo/args-echo.zig rename to test/system/services/args-echo/args-echo.zig diff --git a/system/services/crash-test/crash-test.zig b/test/system/services/crash-test/crash-test.zig similarity index 100% rename from system/services/crash-test/crash-test.zig rename to test/system/services/crash-test/crash-test.zig diff --git a/system/services/device-list/device-list.zig b/test/system/services/device-list/device-list.zig similarity index 100% rename from system/services/device-list/device-list.zig rename to test/system/services/device-list/device-list.zig diff --git a/system/services/fat/fat-test.zig b/test/system/services/fat-test/fat-test.zig similarity index 98% rename from system/services/fat/fat-test.zig rename to test/system/services/fat-test/fat-test.zig index e5126ce..7a4dddd 100644 --- a/system/services/fat/fat-test.zig +++ b/test/system/services/fat-test/fat-test.zig @@ -1,4 +1,4 @@ -//! system/services/fat/fat-test — a client that proves the FAT mount end to end: +//! test/system/services/fat-test — a client that proves the FAT mount end to end: //! it waits for the fat server to mount the USB volume at /mnt/usb, lists the //! root directory through the VFS (which routes /mnt/usb to the fat backend), and //! reads a known file off it. Shipped in the initial_ramdisk; the `fat-mount` diff --git a/system/services/input-source/input-source.zig b/test/system/services/input-source/input-source.zig similarity index 96% rename from system/services/input-source/input-source.zig rename to test/system/services/input-source/input-source.zig index 172f6e9..449020d 100644 --- a/system/services/input-source/input-source.zig +++ b/test/system/services/input-source/input-source.zig @@ -1,4 +1,4 @@ -//! system/services/input-source — a hardware-free synthetic input source, used to exercise +//! test/system/services/input-source — a hardware-free synthetic input source, used to exercise //! the input service end to end without a real PS/2 controller (the `input` test case, and //! any bring-up where there is no hardware). It stands in for a driver: it connects to the //! input service and publishes a rolling stream that cycles through all device classes — diff --git a/system/services/input-test/input-test.zig b/test/system/services/input-test/input-test.zig similarity index 96% rename from system/services/input-test/input-test.zig rename to test/system/services/input-test/input-test.zig index ae48857..93220a7 100644 --- a/system/services/input-test/input-test.zig +++ b/test/system/services/input-test/input-test.zig @@ -1,4 +1,4 @@ -//! system/services/input-test — the input service's client and test oracle, the input +//! test/system/services/input-test — the input service's client and test oracle, the input //! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the //! events a source broadcasts, logging each with its class. It emits the success marker //! `"input-test: ok"` only **after it has received at least one of each class** (keyboard, diff --git a/system/services/process-test/process-test.zig b/test/system/services/process-test/process-test.zig similarity index 95% rename from system/services/process-test/process-test.zig rename to test/system/services/process-test/process-test.zig index acabec1..0f1677d 100644 --- a/system/services/process-test/process-test.zig +++ b/test/system/services/process-test/process-test.zig @@ -151,8 +151,8 @@ pub fn main(init: process.Init) void { const spinner = process.spawnSupervised("process-test", &.{"spinner"}, endpoint) orelse fail("spawn spinner"); time.sleepMillis(100); // let the sleeper block and the spinner get a core - 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"); + if (!listed(sleeper, "/test/system/services/process-test")) fail("sleeper not in process_enumerate"); + if (!listed(spinner, "/test/system/services/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 @@ -171,8 +171,8 @@ pub fn main(init: process.Init) void { if (!process.kill(spinner)) fail("kill spinner"); if (awaitChildExit(endpoint) != spinner) fail("spinner exit notification"); - 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"); + if (listed(sleeper, "/test/system/services/process-test")) fail("sleeper still listed after kill"); + if (listed(spinner, "/test/system/services/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/system/services/shared-memory-client/shared-memory-client.zig b/test/system/services/shared-memory-client/shared-memory-client.zig similarity index 93% rename from system/services/shared-memory-client/shared-memory-client.zig rename to test/system/services/shared-memory-client/shared-memory-client.zig index ca34d73..a6efd47 100644 --- a/system/services/shared-memory-client/shared-memory-client.zig +++ b/test/system/services/shared-memory-client/shared-memory-client.zig @@ -1,4 +1,4 @@ -//! system/services/shared-memory-client — the creating half of the shared-memory test (docs/display-v2.md V2). +//! test/system/services/shared-memory-client — the creating half of the shared-memory test (docs/display-v2.md V2). //! It `shared_memory_create`s a shared region, writes a known pattern into it, and hands the region's //! capability to `shared-memory-server` as an `ipc_call` send_cap. The server maps that capability and //! confirms the pattern is visible — proving cross-process shared memory over the extended diff --git a/system/services/shared-memory-server/shared-memory-server.zig b/test/system/services/shared-memory-server/shared-memory-server.zig similarity index 92% rename from system/services/shared-memory-server/shared-memory-server.zig rename to test/system/services/shared-memory-server/shared-memory-server.zig index a30fd24..57d1b8f 100644 --- a/system/services/shared-memory-server/shared-memory-server.zig +++ b/test/system/services/shared-memory-server/shared-memory-server.zig @@ -1,4 +1,4 @@ -//! system/services/shared-memory-server — the receiving half of the shared-memory test (docs/display-v2.md V2). +//! test/system/services/shared-memory-server — the receiving half of the shared-memory test (docs/display-v2.md V2). //! It registers under `ServiceId.shared_memory_test`; when `shared-memory-client` calls it carrying a //! shared-memory capability, it `shared_memory_map`s that capability and checks the client's pattern //! is visible through the mapping — proving the two processes share the same physical pages diff --git a/system/services/thread-test/thread-test.zig b/test/system/services/thread-test/thread-test.zig similarity index 100% rename from system/services/thread-test/thread-test.zig rename to test/system/services/thread-test/thread-test.zig diff --git a/system/services/vfs-test/vfs-test.zig b/test/system/services/vfs-test/vfs-test.zig similarity index 82% rename from system/services/vfs-test/vfs-test.zig rename to test/system/services/vfs-test/vfs-test.zig index 3001d0f..e31b6b7 100644 --- a/system/services/vfs-test/vfs-test.zig +++ b/test/system/services/vfs-test/vfs-test.zig @@ -1,6 +1,6 @@ -//! /system/tests/vfs-test — a ring-3 client that proves the kernel VFS end to -//! end through the plain `file_system` API: resolve its OWN binary under the -//! kernel-served /system mount, check its metadata, read its ELF magic, and +//! /test/system/services/vfs-test — a ring-3 client that proves the kernel VFS +//! end to end through the plain `file_system` API: resolve its OWN binary under +//! the kernel-served /test mount, check its metadata, read its ELF magic, and //! list /system/services. On success it heartbeats "vfstest: ok" so the kernel //! test can observe it; on failure it reports what went wrong. //! @@ -21,7 +21,7 @@ pub fn main(init: process.Init) void { } // Our own binary, resolved through the kernel mount table. - const self_path = "/system/tests/vfs-test"; + const self_path = "/test/system/services/vfs-test"; var file = fs.open(self_path, .{}) orelse { _ = logging.write("vfstest: open of own binary failed\n"); return; @@ -44,9 +44,9 @@ pub fn main(init: process.Init) void { return; } - // The write refusal: /system is read-only by construction. - if (file.write("x") != null or fs.open("/system/tests/new-file", .{ .create = true }) != null) { - _ = logging.write("vfstest: /system accepted a write\n"); + // The write refusal: the initrd trees are read-only by construction. + if (file.write("x") != null or fs.open("/test/system/services/new-file", .{ .create = true }) != null) { + _ = logging.write("vfstest: the initrd tree accepted a write\n"); return; }