Pass argv to processes on a SysV entry stack; grow the user stack to 32 KiB

Processes now start with C-compatible arguments: the kernel builds the
System V AMD64 entry block (argc, argv, empty envp, auxiliary vector)
at the top of the stack, argv[0] is the path or initial-ramdisk name
the process was spawned as, and system_spawn carries an optional
NUL-separated blob that becomes argv[1..]. The runtime parses the block
(runtime.argumentCount/argument) and its spawn wrappers pass arguments
through. The name is also recorded on the task, so a fault report says
which binary died, not just its id.

The user stack grows from one page to eight (32 KiB,
parameters.user_stack_pages), with the page below left unmapped as a
guard so an overflow faults into a clean process kill rather than
corrupting the image. Task.name_buffer is zero-initialised, not
undefined: an undefined default is materialised as a 0xAA fill that
moved the static task pool out of .bss and made the whole kernel ~7x
slower under QEMU TCG (caught by the affinity test).

Proven end to end by the new args test: args-echo respawns itself with
arguments via the syscall blob, burns more stack than one page could
hold, and echoes its argv intact. Full suite: 44/44.
This commit is contained in:
Daniel Samson
2026-07-11 08:33:12 +01:00
parent 6b3ae0c997
commit a5fe63c1dd
14 changed files with 385 additions and 50 deletions
+48 -8
View File
@@ -120,6 +120,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
userPfTest();
} else if (eql(case, "fault-recovery")) {
faultRecoveryTest(boot_information);
} else if (eql(case, "args")) {
argsTest(boot_information);
} else if (eql(case, "init")) {
initTest(boot_information);
} else if (eql(case, "process")) {
@@ -1162,8 +1164,8 @@ fn processTest(boot_information: *const BootInformation) void {
scheduler.spawn(procWorker, 4); // kernel task at the processes' priority
var spawned: u32 = 0;
if (process.spawnProcess(image, 4)) spawned += 1 else |_| {}
if (process.spawnProcess(image, 4)) spawned += 1 else |_| {}
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
if (process.spawnProcess(image, 4, &.{"/system/services/init"})) spawned += 1 else |_| {}
// Wait (real time) for several heartbeats across the two processes. Each
// process sleeps ~1 s between beats, so a few seconds yields several.
@@ -1218,9 +1220,9 @@ fn spawnFaultingProcess() bool {
architecture.destroyAddressSpace(aspace); // frees code_frame too — it's mapped
return false;
};
architecture.mapUserPageInto(aspace, process.stack_virtual, stack_frame, true, false); // RW + NX
architecture.mapUserPageInto(aspace, process.stack_base_virtual, stack_frame, true, false); // RW + NX
if (!scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_virtual + abi.page_size, 4)) {
if (!scheduler.spawnUserLocked(aspace, process.code_virtual, process.stack_base_virtual + abi.page_size, 4, "fault-probe")) {
architecture.destroyAddressSpace(aspace);
return false;
}
@@ -1243,7 +1245,7 @@ fn faultRecoveryTest(boot_information: *const BootInformation) void {
process.write_count = 0;
process.fault_kill_count = 0;
const spawned = if (process.spawnProcess(image, 4)) true else |_| false;
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |_| false;
check("init spawned as the surviving process", spawned);
// A first heartbeat proves init runs before the fault.
@@ -1287,7 +1289,7 @@ fn initTest(boot_information: *const BootInformation) void {
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
process.write_count = 0;
const spawned = if (process.spawnProcess(image, 4)) true else |err| blk: {
const spawned = if (process.spawnProcess(image, 4, &.{"/system/services/init"})) true else |err| blk: {
log("DANOS-INIT-ERR: {s}\n", .{@errorName(err)});
break :blk false;
};
@@ -1334,7 +1336,7 @@ fn initialRamdiskTest(boot_information: *const BootInformation) void {
var i: u32 = 0;
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (process.spawnProcess(item.blob, 4)) spawned += 1 else |err| {
if (process.spawnProcess(item.blob, 4, &.{item.name})) spawned += 1 else |err| {
log("DANOS-INITRD-ERR: {s}: {s}\n", .{ item.name, @errorName(err) });
}
}
@@ -1394,6 +1396,44 @@ fn vfsTest(boot_information: *const BootInformation) void {
result();
}
/// Process arguments, end to end: spawn args-echo bare (its argv[0] is the
/// initial-ramdisk name). Instance 1 sees argc == 1 and respawns itself through
/// `system_spawn` with the extra arguments "alpha beta-42" — the syscall argument
/// blob. Instance 2 parses the kernel-built System V entry stack via the runtime
/// and echoes its whole argv in one write, which must arrive exactly as sent.
fn argsTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: args\n", .{});
check("bootloader handed over an initial_ramdisk", boot_information.initial_ramdisk_len != 0);
if (boot_information.initial_ramdisk_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.initial_ramdisk_base)))[0..boot_information.initial_ramdisk_len];
const rd = initial_ramdisk.Reader.init(image) orelse {
check("initial_ramdisk image is valid", false);
result();
return;
};
process.setInitialRamdisk(image); // args-echo respawns itself through system_spawn
process.write_count = 0;
process.write_from_user = false;
check("args-echo spawned from the initial_ramdisk", spawnNamed(rd, "args-echo"));
// Wait for the *second* instance's echo (the first writes nothing).
scheduler.setPriority(1);
const deadline = architecture.millis() + 8000;
while (process.write_count < 1 and architecture.millis() < deadline) scheduler.yield();
scheduler.setPriority(4);
const expected = "args: 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);
check("echo came from user mode (CPL 3)", process.write_from_user);
result();
}
/// 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.
fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
@@ -1401,7 +1441,7 @@ fn spawnNamed(rd: initial_ramdisk.Reader, name: []const u8) bool {
while (i < rd.count) : (i += 1) {
const item = rd.entry(i) orelse continue;
if (eql(item.name, name)) {
return if (process.spawnProcess(item.blob, 4)) true else |_| false;
return if (process.spawnProcess(item.blob, 4, &.{item.name})) true else |_| false;
}
}
return false;