Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

Array.prototype.find frees an element twice when interrupted between iterations

Aperta Adatta ai principianti
#1,792 1 commento 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 2 giorni

@josefguenther ci sta già lavorando.

Dal 7/10/2026.

  • #1807 di @josefguenther — aperta
  • #1826 di @ikow — aperta

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
73/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
c
Ambito
backend

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) in gc_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

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di quickjs-ng/quickjs

Tutte le issue di quickjs-ng/quickjs

Issue simili

Altre issue su C

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.