kernel: M3 shared-fate — shared-memory frames live while any mapping does

Each address space that maps a shared-memory region now holds its own
reference, recorded on the AddressSpaceRef and dropped when the space is
destroyed — so 'last reference' means no handles AND no mappings, and a
region's frames can no longer be freed out from under a sibling thread (or
any other live mapper) when the handle-holding task dies. The group-death
notification still posts after every mapping release. (docs/shared-fate-plan.md M3)
This commit is contained in:
Daniel Samson
2026-07-22 10:34:03 +01:00
parent b09a62bc36
commit 08e139ebba
3 changed files with 94 additions and 5 deletions
+38 -1
View File
@@ -168,6 +168,7 @@ pub fn init() void {
scheduler.reap_task_hook = reapTaskLocked;
scheduler.timer_tick_hook = timerSweepLocked;
scheduler.group_exit_hook = groupExitLocked;
scheduler.space_mapping_release_hook = dropSpaceMappingHook;
}
/// Return -1 (as an unsigned bit pattern) in the system_call result register.
@@ -570,9 +571,26 @@ fn systemSharedMemoryCreate(state: *architecture.CpuState) void {
for (0..pages) |i| pmm.free(phys + i * page_size);
return fail(state);
};
// The creator's mapping holds its own reference, recorded on the space
// (docs/shared-fate-plan.md M3): frames must outlive every MAPPING, not just
// every handle — a sibling thread keeps using the region after the
// handle-holding thread dies.
{
const flags = sync.enter();
if (!scheduler.recordSpaceMappingLocked(t.address_space, @ptrCast(shared_memory))) {
sync.leave(flags);
ipc.dropSharedMemoryReference(shared_memory); // last ref: frees the object AND its frames
return fail(state);
}
ipc.retainSharedMemory(shared_memory);
sync.leave(flags);
}
const handle = ipc.installSharedMemoryHandle(t, shared_memory);
if (handle < 0) {
ipc.dropSharedMemoryReference(shared_memory); // last ref: frees the object and its frames
// The mapping record keeps one reference; drop only the creator's. The
// object (and frames) now live until this space is destroyed — the
// region was never named, so nothing else can reach it.
ipc.dropSharedMemoryReference(shared_memory);
return fail(state);
}
@@ -597,6 +615,18 @@ fn systemSharedMemoryMap(state: *architecture.CpuState) void {
const size = shared_memory.pages * page_size;
if (base_v + size > shared_memory_arena_end) return fail(state);
// This mapping holds its own reference, recorded on the space and dropped at
// its destruction (docs/shared-fate-plan.md M3) — the handle's reference is
// separate and may be closed while the mapping lives on.
{
const flags = sync.enter();
if (!scheduler.recordSpaceMappingLocked(t.address_space, @ptrCast(shared_memory))) {
sync.leave(flags);
return fail(state); // mapping table full: refuse rather than map unrecorded
}
ipc.retainSharedMemory(shared_memory);
sync.leave(flags);
}
architecture.mapUserSharedInto(t.address_space, base_v, shared_memory.phys, size);
t.shared_memory_map_next = base_v + size;
architecture.setSystemCallResult(state, base_v);
@@ -1126,6 +1156,13 @@ fn groupExitLocked(leader: u32, supervisor: u32, reason: abi.ExitReason, exit_en
}
}
/// scheduler.space_mapping_release_hook: drop one shared-memory MAPPING
/// reference when the space that held it is destroyed (docs/shared-fate-plan.md
/// M3). Lock held by the release path.
fn dropSpaceMappingHook(object: *anyopaque) void {
ipc.dropSharedMemoryReference(@ptrCast(@alignCast(object)));
}
/// Overwrite dead task `id`'s exit record with the group reason, or re-append it
/// if the group's death burst already evicted it — a supervisor must always be
/// able to read the reason for a notification it just received. Lock held.