Skip to content

Commit 8b0a934

Browse files
leifericfclaude
andcommitted
async: free worker ctx->last_diag before free(ctx) so throws don't leak
When a future's body threw with no enclosing try, prim_throw_classified installed a diag into the worker's per-thread ctx->last_diag. The diag's cached_map got captured into impl->exception (reachable from the future on the GC heap), but worker_run's free(ctx) at exit discarded the diag struct pointer without diag_free'ing it. Leak was ~160 bytes per throwing future. Pre-existing latent: macOS clang ASan ships without LSan, so it never surfaced locally. ubuntu gcc-asan + LSan default-on caught it when v0.255.18's new async test added a throwing future. worker_run now calls diag_free(ctx->last_diag) before free(ctx). The diag's malloc'd kind/code/message/notes/spans/frames all reclaim; the Mino cached_map stays GC-reachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1949eae commit 8b0a934

3 files changed

Lines changed: 36 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# Changelog
22

3+
## v0.255.19 — Fix: Worker leaks last_diag on uncaught throw
4+
5+
LSan on the gcc-built ubuntu-24.04 runners reported a 160-byte
6+
direct leak from `diag_new` via `set_eval_diag_with_data` after the
7+
v0.255.18 async test added `(future (throw (ex-info ...)))`. The
8+
worker's per-thread ctx allocates a `mino_diag_t` when its body
9+
throws and the throw lands at try_depth == 0 (no enclosing try);
10+
the diag installed itself into `ctx->last_diag`, its cached_map
11+
got captured into `impl->exception` for consumer-side rethrow, and
12+
then `worker_run` called `free(ctx)` without freeing the diag
13+
itself.
14+
15+
The leak predates v0.255.18 — any future that threw leaked its
16+
diag — but macOS clang ASan ships without LSan, so it never showed
17+
locally. v0.255.17's CI run had no async test that threw; v0.255.18
18+
added one and surfaced it.
19+
20+
Fix: `worker_run` now calls `diag_free(ctx->last_diag)` before
21+
`free(ctx)`. The diag struct's malloc'd fields (kind/code/msg
22+
strings, notes, spans, frames) all get reclaimed; the Mino-side
23+
cached_map stays reachable from the future's `impl->exception` on
24+
the GC heap.
25+
326
## v0.255.18 — Fix: Preserve ex-info data through futures + expose `>!` / `<!`
427

528
Two async-surface canon-parity fixes surfaced by an adversarial

src/mino.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
*/
2929
#define MINO_VERSION_MAJOR 0
3030
#define MINO_VERSION_MINOR 255
31-
#define MINO_VERSION_PATCH 18
31+
#define MINO_VERSION_PATCH 19
3232

3333
/*
3434
* Human-readable version string of the *linked* runtime, e.g. "0.48.0".

src/runtime/host_threads.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,18 @@ static void worker_run(mino_future_t *impl, char *stack_anchor)
377377
}
378378
mino_worker_list_lock_release(S);
379379

380+
/* Worker may have set ctx->last_diag if its body threw and the
381+
* throw landed at try_depth == 0 (no enclosing try). The diag's
382+
* cached_map (if any) was already captured into impl->exception
383+
* and is reachable from the future on the GC heap; only the
384+
* unmanaged diag struct itself remains. Reclaim it before free(ctx)
385+
* so a future that throws does not leak a ~160-byte diag per call.
386+
* LSan surfaced this on the gcc-built linux runners where libasan
387+
* runs the leak detector at exit. */
388+
if (ctx->last_diag != NULL) {
389+
diag_free(ctx->last_diag);
390+
ctx->last_diag = NULL;
391+
}
380392
mino_tls_ctx = NULL;
381393
free(ctx);
382394
}

0 commit comments

Comments
 (0)