feat: reclaim fiber + async heap (close the closure-env / Future leaks)
Closes the documented per-spawn closure-env leak and most of the async leak,
using only the existing closure.env / closure.fn_ptr field accessors — no compiler
change. Also names the fat-pointer ABI in core.sx (ClosureRaw / SliceRaw) so the
underlying {fn_ptr, env} / {ptr, len} layout is discoverable in one place.
- Fiber body env: Scheduler.reap_fiber frees f.body.env via f.dctx.allocator (the
spawn-time allocator snapshotted in dctx) at all three reap sites (run/poll/
deinit). 1820's 'live after deinit' 3 -> 0.
- Async box + closure envs: sx_run_boxed_closure frees the ThunkBox, the
completion-closure env, and the worker's env (new ThunkBox.worker_env) the
instant the worker completes.
- Async Future: two-flag ownership — Future.worker_done (set at the end of the
completion closure) + consumed (set at the end of await); fut_release frees the
heap Future (via the captured Future.alloc) when BOTH are set, so the LAST of
{worker, await} reclaims it. await now CONSUMES the future (single-use; touching
it afterward is a use-after-free — documented). Residual for an AWAITED future
is 0 (lock: examples/concurrency/1827); a never-awaited future (fire-and-forget /
race loser) keeps only its Future struct — the structured-concurrency remainder.
Self-reviewed across orderings (await-after/before-complete, cancel-then-await,
cancel-while-parked, double-free via await+deinit, race residual, blocking impl,
cross-allocator reap) — all deterministic, no UAF/double-free. Suite 855/0;
byte-identical on aarch64-macOS + aarch64-linux; .ir churn is the core.sx +
Future/ThunkBox field additions.
This commit is contained in:
@@ -433,10 +433,8 @@ Scheduler :: struct {
|
||||
self.current = null;
|
||||
if f.state == .done {
|
||||
// We've switched OFF f's stack already (the final swap landed
|
||||
// here), so the stack is free to unmap. Free the Fiber struct
|
||||
// AFTER munmap.
|
||||
munmap(f.stack_region, f.stack_len);
|
||||
self.own_allocator.dealloc_bytes(xx f);
|
||||
// here), so the stack is free to unmap and the body is dead.
|
||||
reap_fiber(self, f);
|
||||
} else if f.state == .ready {
|
||||
enqueue(self, f);
|
||||
}
|
||||
@@ -561,12 +559,11 @@ Scheduler :: struct {
|
||||
// `ThunkBox`es likewise leak (they are not scheduler-tracked) — freeing both
|
||||
// needs join-point / closure-env ownership affordances.
|
||||
deinit :: (self: *Scheduler) {
|
||||
// (1) Reap leftover ready fibers: unmap the stack, free the Fiber.
|
||||
// (1) Reap leftover ready fibers: free the body env, unmap, free the Fiber.
|
||||
f := self.ready_head;
|
||||
while f != null {
|
||||
nxt := f.next;
|
||||
munmap(f.stack_region, f.stack_len);
|
||||
self.own_allocator.dealloc_bytes(xx f);
|
||||
reap_fiber(self, f);
|
||||
f = nxt;
|
||||
}
|
||||
self.ready_head = null;
|
||||
@@ -708,8 +705,7 @@ impl Io for Scheduler {
|
||||
swap_context(@self.sched_ctx, @f.ctx);
|
||||
self.current = null;
|
||||
if f.state == .done {
|
||||
munmap(f.stack_region, f.stack_len);
|
||||
self.own_allocator.dealloc_bytes(xx f);
|
||||
reap_fiber(self, f);
|
||||
} else if f.state == .ready {
|
||||
enqueue(self, f);
|
||||
}
|
||||
@@ -885,6 +881,25 @@ boot_stack :: (f: *Fiber, size: i64) -> u64 {
|
||||
return top - (top % 16); // 16-byte aligned stack top (AAPCS)
|
||||
}
|
||||
|
||||
// --- fiber reap -------------------------------------------------------------
|
||||
|
||||
// Reclaim a finished (`.done`) or leftover fiber. Frees, in order: the body
|
||||
// closure's heap ENV (`body.env` — the captured environment, allocated at the
|
||||
// closure literal via the SPAWN-time `context.allocator`, which `dctx` snapshots;
|
||||
// `null` for a capture-free body, so the free is an unconditional no-op then),
|
||||
// then the guarded stack (munmap), then the `Fiber` struct itself. This closes
|
||||
// the per-spawn env leak. MUST be the LAST use of `f` — `f` is dangling after.
|
||||
// (The body's env outlives the body's execution but dies WITH the fiber: the
|
||||
// body has returned by the time a `.done` fiber is reaped, so nothing reads the
|
||||
// captures again.)
|
||||
reap_fiber :: (self: *Scheduler, f: *Fiber) {
|
||||
if f.body.env != null {
|
||||
f.dctx.allocator.dealloc_bytes(f.body.env);
|
||||
}
|
||||
munmap(f.stack_region, f.stack_len);
|
||||
self.own_allocator.dealloc_bytes(xx f);
|
||||
}
|
||||
|
||||
// --- intrusive FIFO ready-queue -------------------------------------------
|
||||
|
||||
enqueue :: (self: *Scheduler, f: *Fiber) {
|
||||
|
||||
Reference in New Issue
Block a user