Array.prototype.find frees an element twice when interrupted between iterations
I maintainer di solito rispondono entro 2 giorni
@josefguenther ci sta già lavorando.
Dal 7/10/2026.
Valutazione
- Difficoltà
- 2/5
- Tempo stimato
- 1-3 ore
- Idoneità per principianti
- 73/100
Direzione di ricerca
Inizia in quickjs.c, in corrispondenza di js_array_find, dove l’issue indica il controllo delle interruzioni, la pulizia del ciclo e il percorso delle eccezioni; confronta gli helper di iterazione degli array vicini che controllano le interruzioni. Esegui repro.c e build.sh, quindi verifica la correzione per find e i percorsi correlati findIndex/findLast/findLastIndex, anche con AddressSanitizer; il lavoro è concluso quando repro stampa «ok» senza errori di memoria.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Version: quickjs-ng 0.16.2, and still present on master (js_array_find, quickjs.c). The same code serves find, findIndex, findLast and findLastIndex.
Summary
Each iteration of js_array_find that doesn't match frees the element it read, but val keeps the freed value. The next iteration starts with an interrupt check:
for(; k != end; k += dir) {
if (js_poll_interrupts(ctx))
goto exception; /* <- val still holds the previous element */
index_val = js_int64(k);
val = JS_GetPropertyValue(ctx, obj, index_val);
...
JS_FreeValue(ctx, val); /* element of a non-matching iteration */
JS_FreeValue(ctx, index_val);
}
...
exception:
JS_FreeValue(ctx, index_val);
JS_FreeValue(ctx, val); /* frees it a second time */
JS_FreeValue(ctx, obj);
return JS_EXCEPTION;
When the interrupt handler returns non-zero at that check, the exception path frees the previous element a second time. That drops a reference the array still holds. The element is then freed while it is still reachable from the array. What happens next:
- the next GC fails
assert(JS_REF_COUNT(p) > 0)ingc_decref_child; - or a later access reads freed memory;
- or freeing the array frees the element again.
A predicate that throws doesn't hit this, because val is still owned when its exception is propagated. Only the interrupt check at the top of the loop sees the stale pointer.
The other array iteration helpers that poll for interrupts (every/some/map/filter/forEach, reduce, includes, indexOf, lastIndexOf) either reset val after freeing it or don't free it on the exception path.
Reproduction
repro.c and build.sh are below. The repro arms an interrupt handler that returns 1 on every check, then calls find repeatedly on an array of objects:
eval(ctx, "var items = []; for (var i = 0; i < 20000; i++) items.push({ v: i });");
armed = 1; /* interrupt handler now returns 1 */
for (int i = 0; i < 40; i++)
eval(ctx, "items.find(function (x) { return false; })");
armed = 0;
eval(ctx, "var s = 0; for (var i = 0; i < items.length; i++) s += items[i].v;");
JS_RunGC(rt);
JS_FreeRuntime(rt);
./build.sh /path/to/quickjs-ng && ./repro # SIGSEGV
./build.sh /path/to/quickjs-ng -fsanitize=address -fno-omit-frame-pointer && ./repro
==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008
#0 js_free_value_rt quickjs.c:7161
#1 js_array_find quickjs.c:43991
Each find call is interrupted at whichever check falls due first. Over a few dozen calls, one interruption lands at the check at the top of the loop.
Suggested fix
Reset val after freeing it in the loop:
JS_FreeValue(ctx, val);
val = JS_UNDEFINED;
JS_FreeValue(ctx, index_val);
With this change the repro prints ok, both plain and under ASan. Alternatively, the exception path could free val only where it is still owned.
repro.c
/* Array.prototype.find frees an element twice when the interrupt handler
* fires between two iterations. quickjs-ng 0.16.2.
*
* Build: ./build.sh /path/to/quickjs-ng then ./repro
* Expect: an assertion failure in the GC (or, under ASan, a use-after-free)
* instead of "ok".
*/
#include <stdio.h>
#include <string.h>
#include "quickjs.h"
static int armed;
/* Once armed, interrupt at every check: each find() call below is
* interrupted at whichever check falls due, and over a few dozen calls one
* lands at the check at the top of find's loop. */
static int interrupt(JSRuntime *rt, void *opaque)
{
return armed;
}
static void eval(JSContext *ctx, const char *src)
{
JSValue v = JS_Eval(ctx, src, strlen(src), "<repro>", JS_EVAL_TYPE_GLOBAL);
JS_FreeValue(ctx, v);
JS_FreeValue(ctx, JS_GetException(ctx));
}
int main(void)
{
JSRuntime *rt = JS_NewRuntime();
JSContext *ctx = JS_NewContext(rt);
JS_SetInterruptHandler(rt, interrupt, NULL);
eval(ctx, "var items = []; for (var i = 0; i < 20000; i++) items.push({ v: i });");
armed = 1;
for (int i = 0; i < 40; i++)
eval(ctx, "items.find(function (x) { return false; })");
armed = 0;
/* Every element is referenced only by `items`; one has lost a reference. */
eval(ctx, "var s = 0; for (var i = 0; i < items.length; i++) s += items[i].v;");
JS_RunGC(rt);
JS_FreeContext(ctx);
JS_FreeRuntime(rt);
printf("ok\n");
return 0;
}
build.sh
#!/bin/sh
# usage: ./build.sh <quickjs-ng source dir> [extra cflags, e.g. -fsanitize=address]
Q=${1:?quickjs-ng source dir}; shift
cc -g -O1 "$@" -I"$Q" -o repro repro.c \
"$Q/quickjs.c" "$Q/libregexp.c" "$Q/libunicode.c" "$Q/dtoa.c" -lm -lpthread
- Lingua principale
- C
- Stelle
- 3.9k
- Fork
- 389
- Merge medio
- 2g 17h
- PR unite (30g)
- 21
Preparare l'ambiente
Questo progetto non fornisce container di sviluppo, Dockerfile né guida per i contributori, quindi l'ambiente è a tuo carico: parti dal suo README e consulta la nostra guida al primo contributo per i passaggi generali.
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di quickjs-ng/quickjs
-
Object.prototype.propertyIsEnumerable converts `this` before the property keyForse già presa @SomSamantray l’ha presa 15 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
quickjs-ng/quickjs#1772 ·
I maintainer di solito rispondono entro 2 giorni
-
Deserialization corrupts data or returns `undefined` when a RegExp precedes a shared object referenceForse già presa @Sree24-ui l’ha presa 19 giorni fa. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
quickjs-ng/quickjs#1771 · 2 commenti ·
I maintainer di solito rispondono entro 2 giorni
-
Crash in js_map_finalizer when a WeakMap value is the symbol key of the next entryForse già presa @wleeaf l’ha presa 2 giorni fa. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 72/100
quickjs-ng/quickjs#1835 · 1 reazione ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 45/100
quickjs-ng/quickjs#1833 ·
I maintainer di solito rispondono entro 2 giorni
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 54/100
quickjs-ng/quickjs#1800 ·
I maintainer di solito rispondono entro 2 giorni
Tutte le issue di quickjs-ng/quickjs
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
HexaCluster/credcheck#86 ·
-
Wheels for Python 3.15Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 66/100
mongodb/winkerberos#175 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 63/100
raspberrypi/linux#7718 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
AOMediaCodec/SVT-AV1#3320 ·