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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user