From 027906dd461a0b63a7bb0069db0c40d757f2d8d9 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Sat, 25 Jul 2026 23:16:38 +0530 Subject: [PATCH] Implement AddToKeptObjects and add JS_ClearKeptObjects A WeakRef target must stay alive until the end of the current job (AddToKeptObjects in the WeakRef constructor and in deref, cleared by ClearKeptObjects when a job completes). Because quickjs reclaims by reference counting, the violation is observable in plain JS: let o = { x: 1 }; const w = new WeakRef(o); o = null; w.deref(); // undefined, spec says the object Keep a list of retained targets on the runtime. JS_ExecutePendingJob clears it on entry: the host calling it signals that the previous script or job finished, and the usual drain loop's final call clears the last job's set. Embedders that do not drain jobs can call the new JS_ClearKeptObjects directly. V8 exposes the same operation as v8::Isolate::ClearKeptObjects. tests/bug652.js asserted the old behavior (it exists to check deref does not throw); its expectation is updated. --- api-test.c | 21 ++++++++++++++++++++ quickjs.c | 37 +++++++++++++++++++++++++++++++++++ quickjs.h | 1 + tests/bug652.js | 2 +- tests/weakref-kept-objects.js | 20 +++++++++++++++++++ 5 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/weakref-kept-objects.js diff --git a/api-test.c b/api-test.c index 85aa27e4c..d3788ab1a 100644 --- a/api-test.c +++ b/api-test.c @@ -473,6 +473,26 @@ static void promise_mark_as_handled(void) JS_FreeRuntime(rt); } +static void clear_kept_objects(void) +{ + JSRuntime *rt = new_runtime(); + JSContext *ctx = JS_NewContext(rt); + + JSValue w = eval(ctx, "let o = {x:1}; const w = new WeakRef(o); o = null; w"); + assert(JS_IsObject(w)); + JSValue got = eval(ctx, "w.deref()"); + assert(JS_IsObject(got)); + JS_FreeValue(ctx, got); + + JS_ClearKeptObjects(rt); + got = eval(ctx, "w.deref()"); + assert(JS_IsUndefined(got)); + + JS_FreeValue(ctx, w); + JS_FreeContext(ctx); + JS_FreeRuntime(rt); +} + static void runtime_cstring_free(void) { JSRuntime *rt = new_runtime(); @@ -1294,6 +1314,7 @@ int main(void) module_serde(); module_unhandled_rejection(); promise_mark_as_handled(); + clear_kept_objects(); runtime_cstring_free(); utf16_string(); weak_map_gc_check(); diff --git a/quickjs.c b/quickjs.c index d51b96867..64657feee 100644 --- a/quickjs.c +++ b/quickjs.c @@ -378,6 +378,9 @@ struct JSRuntime { // to js_promise_constructor JSValueLink *parent_promise; + // AddToKeptObjects list, dropped at job boundaries + JSValueLink *kept_objects; + JSHostPromiseRejectionTracker *host_promise_rejection_tracker; void *host_promise_rejection_tracker_opaque; @@ -2527,6 +2530,10 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) JSValue res; int i, ret; + /* job boundary: the host calling us means the previous script or job + finished */ + JS_ClearKeptObjects(rt); + if (list_empty(&rt->job_list)) { *pctx = NULL; return 0; @@ -2641,6 +2648,7 @@ void JS_FreeRuntime(JSRuntime *rt) rt->in_free = true; JS_FreeValueRT(rt, rt->current_exception); + JS_ClearKeptObjects(rt); list_for_each_safe(el, el1, &rt->job_list) { JSJobEntry *e = list_entry(el, JSJobEntry, link); @@ -62499,6 +62507,29 @@ typedef struct JSWeakRefData { static JSWeakRefData js_weakref_sentinel; +static int js_add_to_kept_objects(JSContext *ctx, JSValueConst value) +{ + JSValueLink *link = js_malloc(ctx, sizeof(*link)); + if (!link) + return -1; + link->value = js_dup(value); + link->next = ctx->rt->kept_objects; + ctx->rt->kept_objects = link; + return 0; +} + +void JS_ClearKeptObjects(JSRuntime *rt) +{ + JSValueLink *link = rt->kept_objects; + rt->kept_objects = NULL; + while (link) { + JSValueLink *next = link->next; + JS_FreeValueRT(rt, unsafe_unconst(link->value)); + js_free_rt(rt, link); + link = next; + } +} + static void js_weakref_finalizer(JSRuntime *rt, JSValueConst val) { JSWeakRefData *wrd = JS_GetOpaque(val, JS_CLASS_WEAK_REF); @@ -62551,6 +62582,10 @@ static JSValue js_weakref_constructor(JSContext *ctx, JSValueConst new_target, insert_weakref_record(arg, wr); JS_SetOpaqueInternal(obj, wrd); + if (js_add_to_kept_objects(ctx, arg)) { + JS_FreeValue(ctx, obj); + return JS_EXCEPTION; + } return obj; } @@ -62561,6 +62596,8 @@ static JSValue js_weakref_deref(JSContext *ctx, JSValueConst this_val, int argc, return JS_EXCEPTION; if (wrd == &js_weakref_sentinel) return JS_UNDEFINED; + if (js_add_to_kept_objects(ctx, wrd->target)) + return JS_EXCEPTION; return js_dup(wrd->target); } diff --git a/quickjs.h b/quickjs.h index e950144c6..2c712b00f 100644 --- a/quickjs.h +++ b/quickjs.h @@ -557,6 +557,7 @@ JS_EXTERN int JS_AddIntrinsicTypedArrays(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicPromise(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicBigInt(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicWeakRef(JSContext *ctx); +JS_EXTERN void JS_ClearKeptObjects(JSRuntime *rt); JS_EXTERN int JS_AddPerformance(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicDOMException(JSContext *ctx); JS_EXTERN int JS_AddIntrinsicAToB(JSContext *ctx); diff --git a/tests/bug652.js b/tests/bug652.js index 27dea3bdd..00efb63bd 100644 --- a/tests/bug652.js +++ b/tests/bug652.js @@ -1,4 +1,4 @@ import { assert } from "./assert.js" const ref = new WeakRef({}) const val = ref.deref() // should not throw -assert(val, undefined) +assert(typeof val, "object") // kept alive until the end of the job diff --git a/tests/weakref-kept-objects.js b/tests/weakref-kept-objects.js new file mode 100644 index 000000000..bb83a4d75 --- /dev/null +++ b/tests/weakref-kept-objects.js @@ -0,0 +1,20 @@ +import { assert } from "./assert.js"; + +/* the constructor keeps the target alive until the end of the job */ +{ + let o = { x: 1 }; + const w = new WeakRef(o); + o = null; + assert(typeof w.deref(), "object"); +} + +/* deref() keeps it too, and the kept set is cleared at job boundaries */ +{ + let o = { x: 2 }; + const w = new WeakRef(o); + assert(w.deref().x, 2); + o = null; + assert(typeof w.deref(), "object"); + await Promise.resolve(); + assert(w.deref(), undefined); +}