From a0f0a7956558a5124563c77d8e11de07a90e9198 Mon Sep 17 00:00:00 2001 From: Jonathon McReynolds Date: Wed, 17 Jun 2026 02:01:11 -0500 Subject: [PATCH] vm: heap-force builtin-arg wrapper to plug arena-window ref leak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Multi-arg builtin calls inside an arena_mark/arena_reset window leaked one ref per heap argument. The CALL path packs args into a wrapper make_list(argc); with g_arena.active=1 that wrapper was arena-allocated, list_append incref'd heap items (e.g. weights/grad), but the matching val_decref(arg) is a no-op on arena values — arena reclaim doesn't walk children, so the items' incref was permanently stranded. Add make_list_heap() that forces heap regardless of arena state; use it for the two builtin-arg wrapper sites (jit_helper_call, CASE(CALL)). val_decref(arg) now actually releases items as designed. Retires test_arena_ownership.eigs and test_optimize.eigs from the standalone-leakers list (19 -> 17). Suite: 1882/1882 release + ASan. Co-Authored-By: Claude Opus 4.7 --- src/eigenscript.c | 16 ++++++++++++++++ src/eigenscript.h | 1 + src/vm.c | 8 ++++++-- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/eigenscript.c b/src/eigenscript.c index eb1077be..cafa394d 100644 --- a/src/eigenscript.c +++ b/src/eigenscript.c @@ -625,6 +625,22 @@ Value* make_list(int capacity) { return v; } +/* Heap-forced list — for VM-internal wrappers that must outlive an arena + * window. An arena list freezes val_decref into a no-op, so any heap items + * it incref'd via list_append are leaked when the arena is reclaimed. + * Used by the builtin-arg packing path: the wrapper holds incref'd args, + * and val_decref(arg) must actually walk and release them on return. */ +Value* make_list_heap(int capacity) { + Value *v = xcalloc(1, sizeof(Value)); + v->type = VAL_LIST; + v->data.list.capacity = capacity < 8 ? 8 : capacity; + v->data.list.items = xcalloc(v->data.list.capacity, sizeof(Value*)); + v->data.list.count = 0; + v->refcount = 1; + v->arena = 0; + return v; +} + Value* make_text_builder(void) { Value *v = xcalloc(1, sizeof(Value)); v->type = VAL_TEXT_BUILDER; diff --git a/src/eigenscript.h b/src/eigenscript.h index f92f9b4e..adfba9ee 100644 --- a/src/eigenscript.h +++ b/src/eigenscript.h @@ -584,6 +584,7 @@ Value* make_str(const char *s); Value* make_str_owned(char *s); Value* make_null(void); Value* make_list(int capacity); +Value* make_list_heap(int capacity); Value* make_text_builder(void); Value* make_fn(const char *name, char **params, int param_count, Env *closure); Value* make_builtin(BuiltinFn fn); diff --git a/src/vm.c b/src/vm.c index 8743397d..027b1e9c 100644 --- a/src/vm.c +++ b/src/vm.c @@ -1525,7 +1525,10 @@ int jit_helper_call(EigsChunk *caller_chunk, int argc, int resume_off) { arg = STK_AS_VAL(g_vm.sp - 1); val_incref(arg); } else { - arg = make_list(argc); + /* Wrapper must be heap: val_decref(arg) below has to actually + * release the list_append increfs on heap items, which an arena + * list silently swallows. */ + arg = make_list_heap(argc); for (int i = 0; i < argc; i++) { list_append(arg, STK_AS_VAL(g_vm.sp - argc + i)); } @@ -2646,7 +2649,8 @@ static Value *vm_run(EigsChunk *chunk, Env *env) { arg = STK_AS_VAL(g_vm.sp - 1); val_incref(arg); } else { - arg = make_list(argc); + /* Heap-forced: see jit_helper_call wrapper rationale. */ + arg = make_list_heap(argc); for (int i = 0; i < argc; i++) { list_append(arg, STK_AS_VAL(g_vm.sp - argc + i)); }