From 967ace7bca4afd289fa3217f7903abef622777a6 Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 00:34:42 -0700 Subject: [PATCH 1/9] max_uint32 substitution is the minimal patch for an introduced fix to bug in 40e197f --- quickjs.c | 2 +- tests/expand_fast_array_maxint_poc.js | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 tests/expand_fast_array_maxint_poc.js diff --git a/quickjs.c b/quickjs.c index 43966b14a..44471994b 100644 --- a/quickjs.c +++ b/quickjs.c @@ -9986,7 +9986,7 @@ static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len) JS_ThrowOutOfMemory(ctx); return -1; } - new_size = max_int(new_len, new_size); + new_size = max_uint32(new_len, new_size); new_array_prop = js_realloc2(ctx, p->u.array.u.values, sizeof(JSValue) * new_size, &slack); if (!new_array_prop) return -1; diff --git a/tests/expand_fast_array_maxint_poc.js b/tests/expand_fast_array_maxint_poc.js new file mode 100644 index 000000000..61939a920 --- /dev/null +++ b/tests/expand_fast_array_maxint_poc.js @@ -0,0 +1,12 @@ +// QuickJS expand_fast_array max_int sign truncation — commit 40e197f +// Bug: expand_fast_array calls max_int(new_len, new_size) where new_len is uint32_t. +// When new_len >= 0x80000000, cast to int makes it negative. +// max_int returns the small grow-by-50% size. Buffer is underallocated. +// add_fast_array_element then writes to values[new_len-1] — OOB write. +// Run: qjs poc.js + +const c = [1, 2, 3]; +c.length = 0x7FFFFFFF; // sets length property; count stays 3 +c.push(4); // new_len = count+1; if > size, expand_fast_array called + // sign truncation: max_int underallocates; OOB write follows +print("done"); From 04e77125d93300d0bf18b83e311fbd6aedef3373 Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 00:54:40 -0700 Subject: [PATCH 2/9] update js_int32 -> js_uint32 --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 44471994b..12e7eea13 100644 --- a/quickjs.c +++ b/quickjs.c @@ -42476,7 +42476,7 @@ static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, p->u.array.u.values[array_len + i] = js_dup(argv[i]); } p->u.array.count = new_len; - p->prop[0].u.value = js_int32(new_len); + p->prop[0].u.value = js_uint32(new_len); return js_int32(new_len); } } From 1dc5ddb62b055a868ac55d859bff7734ffc307db Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 01:09:44 -0700 Subject: [PATCH 3/9] Fixing the ubsan failure in CI. Looks like GC runs js_array_mark as well --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 12e7eea13..73ca1da6e 100644 --- a/quickjs.c +++ b/quickjs.c @@ -6495,7 +6495,7 @@ static void js_array_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { JSObject *p = JS_VALUE_GET_OBJ(val); - int i; + uint32_t i; for(i = 0; i < p->u.array.count; i++) { JS_MarkValue(rt, p->u.array.u.values[i], mark_func); From 77ad5b6a34588ac3857df1ced3f0434f36ce65d3 Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 01:20:31 -0700 Subject: [PATCH 4/9] Fixing the ubsan failure in CI. another missed int -> uint32 --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 73ca1da6e..f2c4e6e51 100644 --- a/quickjs.c +++ b/quickjs.c @@ -6483,7 +6483,7 @@ static void free_var_ref(JSRuntime *rt, JSVarRef *var_ref) static void js_array_finalizer(JSRuntime *rt, JSValueConst val) { JSObject *p = JS_VALUE_GET_OBJ(val); - int i; + uint32_t i; for(i = 0; i < p->u.array.count; i++) { JS_FreeValueRT(rt, p->u.array.u.values[i]); From 9d31914c1f93d77e7b1a80fb356c983320288ce8 Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 02:09:52 -0700 Subject: [PATCH 5/9] fix textcase so CI checks pass --- tests/expand_fast_array_maxint_poc.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/expand_fast_array_maxint_poc.js b/tests/expand_fast_array_maxint_poc.js index 61939a920..b7059e386 100644 --- a/tests/expand_fast_array_maxint_poc.js +++ b/tests/expand_fast_array_maxint_poc.js @@ -3,10 +3,23 @@ // When new_len >= 0x80000000, cast to int makes it negative. // max_int returns the small grow-by-50% size. Buffer is underallocated. // add_fast_array_element then writes to values[new_len-1] — OOB write. -// Run: qjs poc.js +// +// Pre-fix behavior: process crashes with SIGBUS (OOB write to unmapped memory) +// Post-fix behavior: InternalError or RangeError thrown and caught cleanly +// +// Run: qjs poc_test_fixed.js +// Expected output: PASS: got graceful error: ... -const c = [1, 2, 3]; -c.length = 0x7FFFFFFF; // sets length property; count stays 3 -c.push(4); // new_len = count+1; if > size, expand_fast_array called - // sign truncation: max_int underallocates; OOB write follows -print("done"); +const arr = [1, 2, 3]; +arr.length = 0x7FFFFFFF; // sets length property, no memory allocated + +try { + arr.push(4); // triggers expand_fast_array with new_len=0x80000000 + throw new Error("FAIL: expected InternalError or RangeError was not thrown"); +} catch(e) { + if (e instanceof InternalError || e instanceof RangeError) { + print("PASS: got graceful error:", e.message); + } else { + throw e; // unexpected error type — re-throw to fail the test + } +} From 408d9f85a896094d1ed49b890e2e44d44b956fda Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 03:06:25 -0700 Subject: [PATCH 6/9] Better long term fix. Fails fast path and falls to slow path with right guard. Test poc should also pass now --- quickjs.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/quickjs.c b/quickjs.c index f2c4e6e51..2319d55cd 100644 --- a/quickjs.c +++ b/quickjs.c @@ -9980,9 +9980,14 @@ static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len) size_t slack; JSValue *new_array_prop; + if (unlikely(new_len > (uint32_t)INT32_MAX)) { + JS_ThrowOutOfMemory(ctx); + return -1; + } + old_size = p->u.array.u1.size; - new_size = old_size + old_size/2; // grow by 50% - if (new_size < old_size) { // integer overflow + new_size = old_size + old_size/2; + if (new_size < old_size) { { JS_ThrowOutOfMemory(ctx); return -1; } @@ -42467,6 +42472,7 @@ static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, (p->shape->prop->flags & JS_PROP_WRITABLE))) { array_len = JS_VALUE_GET_INT(p->prop[0].u.value); new_len = array_len + argc; + if (likely(new_len >= array_len && new_len <= (uint32_t)INT32_MAX)) { /* no overflow and within fast-array bounds */// if (likely(new_len >= array_len)) { /* no overflow */ if (unlikely(new_len > p->u.array.u1.size)) { if (expand_fast_array(ctx, p, new_len)) From 62cf2a6ad8ba7bde7e6290e78d2b49f01441de2e Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 03:19:55 -0700 Subject: [PATCH 7/9] fixed syntax errors and updated poc test case to pass in CI --- quickjs.c | 5 ++--- tests/expand_fast_array_maxint_poc.js | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/quickjs.c b/quickjs.c index 2319d55cd..8a05f21ba 100644 --- a/quickjs.c +++ b/quickjs.c @@ -9987,7 +9987,7 @@ static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len) old_size = p->u.array.u1.size; new_size = old_size + old_size/2; - if (new_size < old_size) { { + if (new_size < old_size) { JS_ThrowOutOfMemory(ctx); return -1; } @@ -42472,8 +42472,7 @@ static JSValue js_array_push(JSContext *ctx, JSValueConst this_val, (p->shape->prop->flags & JS_PROP_WRITABLE))) { array_len = JS_VALUE_GET_INT(p->prop[0].u.value); new_len = array_len + argc; - if (likely(new_len >= array_len && new_len <= (uint32_t)INT32_MAX)) { /* no overflow and within fast-array bounds */// - if (likely(new_len >= array_len)) { /* no overflow */ + if (likely(new_len >= array_len && new_len <= (uint32_t)INT32_MAX)) { /* no overflow and within fast-array bounds */ if (unlikely(new_len > p->u.array.u1.size)) { if (expand_fast_array(ctx, p, new_len)) return JS_EXCEPTION; diff --git a/tests/expand_fast_array_maxint_poc.js b/tests/expand_fast_array_maxint_poc.js index b7059e386..63e280e08 100644 --- a/tests/expand_fast_array_maxint_poc.js +++ b/tests/expand_fast_array_maxint_poc.js @@ -9,17 +9,18 @@ // // Run: qjs poc_test_fixed.js // Expected output: PASS: got graceful error: ... - const arr = [1, 2, 3]; -arr.length = 0x7FFFFFFF; // sets length property, no memory allocated +arr.length = 0x7FFFFFFF; try { - arr.push(4); // triggers expand_fast_array with new_len=0x80000000 - throw new Error("FAIL: expected InternalError or RangeError was not thrown"); -} catch(e) { - if (e instanceof InternalError || e instanceof RangeError) { + const result = arr.push(4); + // Succeeded via slow array path — no crash, no heap corruption + print("PASS: push completed without crash, result:", result); + +} catch(e) { + if (e instanceof InternalError || e instanceof RangeError || e instanceof TypeError) { print("PASS: got graceful error:", e.message); - } else { - throw e; // unexpected error type — re-throw to fail the test - } + } else { + throw e; // unexpected — re-throw + } } From 2b81053237d8dfce8eb9f6a918c43329adcf725a Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 22:09:39 -0700 Subject: [PATCH 8/9] expand_fast_array max_int sign truncation fix related to incomplete commit 40e197f --- tests/{expand_fast_array_maxint_poc.js => bug1468.js} | 1 - 1 file changed, 1 deletion(-) rename tests/{expand_fast_array_maxint_poc.js => bug1468.js} (93%) diff --git a/tests/expand_fast_array_maxint_poc.js b/tests/bug1468.js similarity index 93% rename from tests/expand_fast_array_maxint_poc.js rename to tests/bug1468.js index 63e280e08..904f2ca9d 100644 --- a/tests/expand_fast_array_maxint_poc.js +++ b/tests/bug1468.js @@ -1,4 +1,3 @@ -// QuickJS expand_fast_array max_int sign truncation — commit 40e197f // Bug: expand_fast_array calls max_int(new_len, new_size) where new_len is uint32_t. // When new_len >= 0x80000000, cast to int makes it negative. // max_int returns the small grow-by-50% size. Buffer is underallocated. From 90fa46278b6629ff74076e2c0b341b65e3121c41 Mon Sep 17 00:00:00 2001 From: Stellersjay Date: Tue, 12 May 2026 22:10:43 -0700 Subject: [PATCH 9/9] update bracket spacing --- quickjs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quickjs.c b/quickjs.c index 8a05f21ba..0a3abac68 100644 --- a/quickjs.c +++ b/quickjs.c @@ -9987,7 +9987,7 @@ static int expand_fast_array(JSContext *ctx, JSObject *p, uint32_t new_len) old_size = p->u.array.u1.size; new_size = old_size + old_size/2; - if (new_size < old_size) { + if (new_size < old_size) { JS_ThrowOutOfMemory(ctx); return -1; }