kernel+tests: M4 shared-fate — nine group-death test cases, per-space arena cursors

Eight new QEMU cases (thread-fault-group, kill-threaded-group,
kill-via-worker-tid, racing-triggers, exit-group, leader-thread-exit,
thread-exit-solo, shm-mapping-ref) driving seven new thread-test modes; a
shared checkGroupDead asserts the contract everywhere: one notification,
badged with the leader, reason on the leader's record, no member listed,
claims released first.

The shm-mapping-ref case flushed out the per-task DMA/shared-memory arena
cursor bug directly (a sibling's regions mapped over the worker's), so both
cursors moved to the AddressSpaceRef like the mmap/MMIO cursors before them
(threading M7 pattern). Runtime gains Thread.tryExitCurrent for the leader
-EPERM refusal path. Docs updated: threading.md's shared-fate gap is closed,
process-management.md and process-lifecycle.md describe the leader re-key,
plan status = implemented. Full suite: 100/100.
This commit is contained in:
Daniel Samson
2026-07-22 10:49:46 +01:00
parent 1882161cb4
commit 2bc2a0d70d
10 changed files with 565 additions and 43 deletions
+165
View File
@@ -469,6 +469,157 @@ fn runRwlockMode() void {
}
}
// --- shared-fate modes (docs/shared-fate-plan.md M4) -------------------------
fn faultNullPage() void {
@as(*volatile u32, @ptrFromInt(0x10)).* = 1; // unmapped null page: #PF, group death
}
fn faultingWorker() void {
write("thread-test: worker faulting\n");
faultNullPage();
}
/// fault-worker: a worker faults; shared fate must end the whole group, so the
/// main thread parks forever and never prints anything more.
fn runFaultWorkerMode() void {
write("thread-test: spawning faulting worker\n");
_ = runtime.Thread.spawn(.{}, faultingWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
while (true) runtime.system.yield();
}
fn spinningWorker() void {
while (true) {} // no system calls: only a tick can deliver a deferred kill
}
/// spin-forever: a kill target. The worker spins without syscalls (the condemned
/// path); the main thread yields (the parked path).
fn runSpinForeverMode() void {
_ = runtime.Thread.spawn(.{}, spinningWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
write("thread-test: spinning\n");
while (true) runtime.system.yield();
}
fn exitingWorker() void {
write("thread-test: worker exiting the process\n");
runtime.system.exit(3); // exit from ANY thread is group death (.aborted)
}
/// exit-worker: a WORKER calls exit(3); the group must die with the leader's
/// reason reading .aborted.
fn runExitWorkerMode() void {
_ = runtime.Thread.spawn(.{}, exitingWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
while (true) runtime.system.yield();
}
var race_go = std.atomic.Value(u32).init(0);
fn racingWorker() void {
while (race_go.load(.acquire) == 0) {}
faultNullPage();
}
/// race: two members fault as near-simultaneously as user space can arrange —
/// the group-dying latch must make the two triggers count as one death.
fn runRaceMode() void {
_ = runtime.Thread.spawn(.{}, racingWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
write("thread-test: racing\n");
race_go.store(1, .release);
faultNullPage();
}
var leader_exit_done = std.atomic.Value(u32).init(0);
fn patientWorker() void {
while (leader_exit_done.load(.acquire) == 0) runtime.system.yield();
}
/// leader-exit: the MAIN thread asks for thread_exit; the kernel must refuse
/// (-EPERM) and the worker must be entirely unaffected.
fn runLeaderExitMode() void {
const worker = runtime.Thread.spawn(.{}, patientWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
runtime.Thread.tryExitCurrent(); // refused: we are the leader
write("thread-test: leader thread_exit refused ok\n");
leader_exit_done.store(1, .release);
worker.join();
}
fn promptWorker() void {}
/// solo: regression — a WORKER's thread_exit stays per-thread; the sibling
/// (main) survives it.
fn runSoloMode() void {
const worker = runtime.Thread.spawn(.{}, promptWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
worker.join();
write("thread-test: solo sibling survived ok\n");
}
const shm_pattern_length: usize = 4096;
var shm_region_base = std.atomic.Value(usize).init(0);
fn patternByte(i: usize) u8 {
return @truncate(i *% 31 +% 7);
}
fn shmWorker() void {
const region = runtime.shared_memory.create(shm_pattern_length) orelse {
write("thread-shm: FAIL create refused\n");
return;
};
for (0..shm_pattern_length) |i| region.ptr[i] = patternByte(i);
shm_region_base.store(@intFromPtr(region.ptr), .release);
// Returning thread_exits this worker: its handle table — holding the
// region's ONLY handle — closes. The sibling's view must survive on the
// mapping reference (docs/shared-fate-plan.md M3).
}
/// shm-worker: the M3 use-after-free regression. The worker creates and fills a
/// shared-memory region and dies; the main thread churns the frame allocator and
/// then checks the mapping is intact — freed frames would have been reused and
/// scribbled on.
fn runShmWorkerMode() void {
const worker = runtime.Thread.spawn(.{}, shmWorker, .{}) catch {
write("thread-test: FAIL spawn refused\n");
return;
};
worker.join();
const base = shm_region_base.load(.acquire);
if (base == 0) return; // the worker already printed the failure
var churn: usize = 0;
while (churn < 8) : (churn += 1) {
const noise = runtime.shared_memory.create(shm_pattern_length) orelse break;
@memset(noise.ptr[0..shm_pattern_length], 0xFF);
}
const view: [*]const u8 = @ptrFromInt(base);
var intact = true;
for (0..shm_pattern_length) |i| {
if (view[i] != patternByte(i)) intact = false;
}
if (intact) {
write("thread-shm: mapping survives creator ok\n");
} else {
write("thread-shm: FAIL mapping corrupted after creator death\n");
}
}
pub fn main(init: runtime.process.Init) void {
const mode = init.arguments.get(1) orelse "spawn";
if (std.mem.eql(u8, mode, "join")) {
@@ -485,6 +636,20 @@ pub fn main(init: runtime.process.Init) void {
runTlsMode();
} else if (std.mem.eql(u8, mode, "rwlock")) {
runRwlockMode();
} else if (std.mem.eql(u8, mode, "fault-worker")) {
runFaultWorkerMode();
} else if (std.mem.eql(u8, mode, "spin-forever")) {
runSpinForeverMode();
} else if (std.mem.eql(u8, mode, "exit-worker")) {
runExitWorkerMode();
} else if (std.mem.eql(u8, mode, "race")) {
runRaceMode();
} else if (std.mem.eql(u8, mode, "leader-exit")) {
runLeaderExitMode();
} else if (std.mem.eql(u8, mode, "solo")) {
runSoloMode();
} else if (std.mem.eql(u8, mode, "shm-worker")) {
runShmWorkerMode();
} else {
runSpawnMode();
}