reorg: move test fixtures to test/system/services (source + boot volume)

The 11 QEMU-suite fixtures lived mixed into system/services/ with their
binaries bundled at /system/tests/<name>. Now the repo path is the boot
path, like every real service: test/system/services/<name>. 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.
This commit is contained in:
Daniel Samson 2026-07-23 00:11:07 +01:00
parent b9cec7d1be
commit f023f1cfd6
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
24 changed files with 150 additions and 108 deletions

View File

@ -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

View File

@ -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,

View File

@ -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/<name> 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

View File

@ -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/<name>)
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` |

View File

@ -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

View File

@ -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.

View File

@ -297,7 +297,7 @@ test/qemu_test.py <case>`), 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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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);

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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`

View File

@ -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

View File

@ -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,

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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;
}