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
+23 -3
View File
@@ -70,8 +70,24 @@ pub const Task = struct {
ipc_send_cap: u64 = ~@as(u64, 0), // handle to transfer with this message (abi.no_cap = none)
ipc_received_cap: u64 = ~@as(u64, 0), // client: handle the reply's transferred cap landed at (abi.no_cap = none)
next: ?*Task = null, // ready-queue link (also the endpoint sender-FIFO link)
// The process's name — argv[0] as it was spawned (a boot-volume path for init,
// an initial-ramdisk name for everything else); empty for kernel tasks. Fixed
// storage, so the fault path can name the dead without touching the heap.
// Zero-initialised (not `undefined`): an undefined default is materialised as
// a 0xAA fill, which would move the whole static task pool out of .bss.
name_buffer: [maximum_task_name]u8 = .{0} ** maximum_task_name,
name_length: u8 = 0,
/// The task's name (argv[0] at spawn), or empty for a kernel task.
pub fn name(self: *const Task) []const u8 {
return self.name_buffer[0..self.name_length];
}
};
/// Capacity of `Task.name_buffer` — matches the longest name `system_spawn`
/// accepts, so a spawned name is never truncated.
pub const maximum_task_name = 64;
/// Size of each task's IPC handle table. Kept here (not in ipc_sync.zig) because
/// it dimensions a field of `Task`; ipc_sync.zig re-exports it.
pub const ipc_maximum_handles = 16;
@@ -258,12 +274,13 @@ pub fn spawnOn(entry: *const fn () void, priority: Priority, cpu: u32) bool {
}
/// Spawn a **user** task: a task with its own address space (`aspace`) that starts
/// in user mode at `entry` on `user_sp`. It gets a fresh kernel stack for
/// syscalls/interrupts, and its first switch-in lands in `user_task_trampoline`.
/// in user mode at `entry` on `user_sp`, recorded under `name` (its argv[0]).
/// It gets a fresh kernel stack for syscalls/interrupts, and its first switch-in
/// lands in `user_task_trampoline`.
/// Returns false (creating nothing) if the table is full or out of memory.
/// **Caller must hold the kernel lock** (the loader that builds `aspace` holds it
/// across the whole spawn, so the address space and the task appear atomically).
pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority) bool {
pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority, task_name: []const u8) bool {
const t = freeSlot() orelse return false;
const stack = heap.allocator().alloc(u8, stack_size) catch return false;
t.* = .{
@@ -275,6 +292,9 @@ pub fn spawnUserLocked(aspace: u64, entry: u64, user_sp: u64, priority: Priority
.user_ip = entry,
.user_sp = user_sp,
};
const name_length = @min(task_name.len, maximum_task_name);
@memcpy(t.name_buffer[0..name_length], task_name[0..name_length]);
t.name_length = @intCast(name_length);
next_id += 1;
const top = @intFromPtr(stack.ptr) + stack.len;
t.kstack_top = top;