From efd3d752accd061e24f5bf9a6f2ddbf7325e3024 Mon Sep 17 00:00:00 2001 From: Chay Nabors Date: Wed, 25 Feb 2026 00:59:12 -0500 Subject: [PATCH] fix: use-after-free in cabi_realloc free_list on repeated export calls cabi_realloc tracked all allocations in Runtime.free_list, which post_call freed after each export invocation. When the host calls cabi_realloc during an import to write a return value into guest memory, those allocations may still be referenced by live JS objects across repeated export calls. post_call would free them, causing use-after-free on the next invocation. Fix: remove indiscriminate tracking from cabi_realloc. Only the retptr allocated explicitly in call() is tracked and freed by post_call. Fixes #224 --- embedding/embedding.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/embedding/embedding.cpp b/embedding/embedding.cpp index 68463f8f..d49a2b26 100644 --- a/embedding/embedding.cpp +++ b/embedding/embedding.cpp @@ -141,8 +141,6 @@ cabi_realloc_adapter(void *ptr, size_t orig_size, size_t org_align, __attribute__((export_name("cabi_realloc"))) void * cabi_realloc(void *ptr, size_t orig_size, size_t org_align, size_t new_size) { void *ret = JS_realloc(Runtime.cx, ptr, orig_size, new_size); - // track all allocations during a function "call" for freeing - Runtime.free_list.push_back(ret); if (!ret) { Runtime.engine->abort("(cabi_realloc) Unable to realloc"); } @@ -233,6 +231,7 @@ __attribute__((export_name("call"))) uint32_t call(uint32_t fn_idx, if (fn->retptr) { LOG("(call) setting retptr at arg %d\n", argcnt); retptr = cabi_realloc(nullptr, 0, 4, fn->retsize); + Runtime.free_list.push_back(retptr); args[argcnt].setInt32((uint32_t)retptr); } @@ -295,6 +294,7 @@ __attribute__((export_name("call"))) uint32_t call(uint32_t fn_idx, if (!fn->retptr && fn->ret.has_value()) { LOG("(call) singular return"); retptr = cabi_realloc(0, 0, 4, fn->retsize); + Runtime.free_list.push_back(retptr); switch (fn->ret.value()) { case CoreVal::I32: *((uint32_t *)retptr) = ret.toInt32();