From 8505807ab6a46aa90be03f78bcd2a91912d52d05 Mon Sep 17 00:00:00 2001 From: theMackabu Date: Sun, 5 Apr 2026 22:16:04 -0700 Subject: [PATCH] prevent OOM on json parsing --- include/gc/roots.h | 30 +++- include/internal.h | 1 + src/gc/roots.c | 67 +++++++- src/modules/json.c | 261 +++++++++++++++++++++++++------ tests/json_temp_roots_stress.cjs | 52 ++++++ 5 files changed, 357 insertions(+), 54 deletions(-) create mode 100644 tests/json_temp_roots_stress.cjs diff --git a/include/gc/roots.h b/include/gc/roots.h index 2804658..4d2d73c 100644 --- a/include/gc/roots.h +++ b/include/gc/roots.h @@ -4,17 +4,41 @@ #include "types.h" #include #include +#include #define GC_MAX_STATIC_ROOTS 128 -typedef void (*gc_root_visitor_t)(ant_t *js, ant_value_t v); +typedef struct gc_temp_root_scope { + ant_t *js; + ant_value_t *items; + size_t len; + size_t cap; + struct gc_temp_root_scope *prev; +} gc_temp_root_scope_t; + +typedef struct { + gc_temp_root_scope_t *scope; + size_t index; +} gc_temp_root_handle_t; + +typedef void (*gc_root_visitor_t)( + ant_t *js, + ant_value_t v +); size_t gc_root_scope(ant_t *js); + bool gc_push_root(ant_t *js, ant_value_t *slot); +bool gc_temp_root_set(gc_temp_root_handle_t handle, ant_value_t value); void gc_register_root(ant_value_t *slot); void gc_pop_roots(ant_t *js, size_t mark); void gc_visit_roots(ant_t *js, gc_root_visitor_t visitor); +void gc_temp_root_scope_begin(ant_t *js, gc_temp_root_scope_t *scope); +void gc_temp_root_scope_end(gc_temp_root_scope_t *scope); + +ant_value_t gc_temp_root_get(gc_temp_root_handle_t handle); +gc_temp_root_handle_t gc_temp_root_add(gc_temp_root_scope_t *scope, ant_value_t value); #define GC_ROOT_PIN(js, slot) do { \ bool _gc_root_ok = gc_push_root((js), &(slot)); \ @@ -25,4 +49,8 @@ void gc_visit_roots(ant_t *js, gc_root_visitor_t visitor); #define GC_ROOT_SAVE(name, js) size_t name = gc_root_scope((js)) #define GC_ROOT_RESTORE(js, mark) gc_pop_roots((js), (mark)) +static inline bool gc_temp_root_handle_valid(gc_temp_root_handle_t handle) { + return handle.scope != NULL; +} + #endif diff --git a/include/internal.h b/include/internal.h index 5783d71..29db05c 100644 --- a/include/internal.h +++ b/include/internal.h @@ -144,6 +144,7 @@ struct ant_isolate_t { ant_value_t **c_roots; size_t c_root_count; size_t c_root_cap; + struct gc_temp_root_scope *temp_roots; const char *code; const char *filename; diff --git a/src/gc/roots.c b/src/gc/roots.c index df97251..b03b342 100644 --- a/src/gc/roots.c +++ b/src/gc/roots.c @@ -1,5 +1,5 @@ #include "gc/roots.h" -#include "internal.h" +#include "internal.h" // IWYU pragma: keep #include #include @@ -36,14 +36,73 @@ void gc_pop_roots(ant_t *js, size_t mark) { js->c_root_count = (mark <= js->c_root_count) ? mark : 0; } -void gc_visit_roots(ant_t *js, gc_root_visitor_t visitor) { - for (size_t i = 0; i < g_root_count; i++) { - if (g_roots[i] && *g_roots[i]) visitor(js, *g_roots[i]); +void gc_temp_root_scope_begin(ant_t *js, gc_temp_root_scope_t *scope) { + if (!scope) return; + scope->js = js; + scope->items = NULL; + scope->len = 0; + scope->cap = 0; + scope->prev = js ? js->temp_roots : NULL; + if (js) js->temp_roots = scope; +} + +void gc_temp_root_scope_end(gc_temp_root_scope_t *scope) { + if (!scope) return; + + ant_t *js = scope->js; + if (js && js->temp_roots == scope) js->temp_roots = scope->prev; + + free(scope->items); + scope->items = NULL; + scope->len = 0; + scope->cap = 0; + scope->prev = NULL; + scope->js = NULL; +} + +gc_temp_root_handle_t gc_temp_root_add(gc_temp_root_scope_t *scope, ant_value_t value) { + gc_temp_root_handle_t invalid = {0}; + if (!scope) return invalid; + + if (scope->len >= scope->cap) { + size_t new_cap = scope->cap ? scope->cap * 2 : 16; + ant_value_t *next = realloc(scope->items, new_cap * sizeof(*next)); + if (!next) return invalid; + scope->items = next; + scope->cap = new_cap; } + size_t index = scope->len++; + scope->items[index] = value; + gc_temp_root_handle_t handle = { + .scope = scope, + .index = index, + }; + + return handle; +} + +bool gc_temp_root_set(gc_temp_root_handle_t handle, ant_value_t value) { + if (!handle.scope || handle.index >= handle.scope->len) return false; + handle.scope->items[handle.index] = value; + return true; +} + +ant_value_t gc_temp_root_get(gc_temp_root_handle_t handle) { + if (!handle.scope || handle.index >= handle.scope->len) return js_mkundef(); + return handle.scope->items[handle.index]; +} + +void gc_visit_roots(ant_t *js, gc_root_visitor_t visitor) { + for (size_t i = 0; i < g_root_count; i++) + if (g_roots[i] && *g_roots[i]) visitor(js, *g_roots[i]); + if (!js) return; for (size_t i = 0; i < js->c_root_count; i++) { ant_value_t *slot = js->c_roots[i]; if (slot && *slot) visitor(js, *slot); } + + for (gc_temp_root_scope_t *scope = js->temp_roots; scope; scope = scope->prev) + for (size_t i = 0; i < scope->len; i++) if (scope->items[i]) visitor(js, scope->items[i]); } diff --git a/src/modules/json.c b/src/modules/json.c index 2268507..06ed2d6 100644 --- a/src/modules/json.c +++ b/src/modules/json.c @@ -5,7 +5,7 @@ #include #include -#include "gc.h" +#include "gc/roots.h" #include "utf8.h" #include "errors.h" #include "runtime.h" @@ -22,13 +22,43 @@ typedef struct { UT_hash_handle hh; } json_key_entry_t; -static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { +static inline bool json_value_needs_temp_root(ant_value_t value) { + if (value <= NANBOX_PREFIX) return false; + + static const uint32_t mask = + (1u << T_STR) | (1u << T_OBJ) | (1u << T_ARR) | (1u << T_FUNC) | + (1u << T_PROMISE) | (1u << T_GENERATOR) | (1u << T_SYMBOL) | (1u << T_BIGINT); + + uint8_t t = vtype(value); + return t < 32 && (mask >> t) & 1; +} + +static inline bool json_temp_pin(gc_temp_root_scope_t *roots, ant_value_t value) { + if (!json_value_needs_temp_root(value)) return true; + return gc_temp_root_handle_valid(gc_temp_root_add(roots, value)); +} + +static inline ant_value_t json_parse_oom(ant_t *js) { + return js_mkerr(js, "JSON.parse() failed: out of memory"); +} + +static inline ant_value_t json_stringify_oom(ant_t *js) { + return js_mkerr(js, "JSON.stringify() failed: out of memory"); +} + +static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val, gc_temp_root_scope_t *roots) { if (!val) return js_mkundef(); switch (yyjson_get_type(val)) { case YYJSON_TYPE_NULL: return js_mknull(); case YYJSON_TYPE_BOOL: return js_bool(yyjson_get_bool(val)); - case YYJSON_TYPE_STR: return js_mkstr(js, yyjson_get_str(val), yyjson_get_len(val)); + + case YYJSON_TYPE_STR: { + ant_value_t str = js_mkstr(js, yyjson_get_str(val), yyjson_get_len(val)); + if (is_err(str)) return str; + if (!json_temp_pin(roots, str)) return json_parse_oom(js); + return str; + } case YYJSON_TYPE_NUM: { if (yyjson_is_sint(val)) return js_mknum((double)yyjson_get_sint(val)); @@ -38,11 +68,14 @@ static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { case YYJSON_TYPE_ARR: { ant_value_t arr = js_mkarr(js); + if (is_err(arr)) return arr; + if (!json_temp_pin(roots, arr)) return json_parse_oom(js); size_t idx, max; yyjson_val *item; yyjson_arr_foreach(val, idx, max, item) { - ant_value_t elem = yyjson_to_jsval(js, item); + ant_value_t elem = yyjson_to_jsval(js, item, roots); + if (is_err(elem)) return elem; js_arr_push(js, arr, elem); } @@ -51,6 +84,8 @@ static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { case YYJSON_TYPE_OBJ: { ant_value_t obj = js_newobj(js); + if (is_err(obj)) return obj; + if (!json_temp_pin(roots, obj)) return json_parse_oom(js); size_t idx, max; yyjson_val *key, *item; json_key_entry_t *hash = NULL, *entry, *tmp; @@ -59,20 +94,34 @@ static ant_value_t yyjson_to_jsval(ant_t *js, yyjson_val *val) { const char *k = yyjson_get_str(key); size_t klen = yyjson_get_len(key); - ant_value_t v = yyjson_to_jsval(js, item); + ant_value_t v = yyjson_to_jsval(js, item, roots); + if (is_err(v)) { + HASH_ITER(hh, hash, entry, tmp) + HASH_DEL(hash, entry); free(entry); + return v; + } HASH_FIND(hh, hash, k, klen, entry); if (entry) js_saveval(js, entry->prop_off, v); else { ant_offset_t off = js_mkprop_fast_off(js, obj, k, klen, v); + if (off == 0) { + HASH_ITER(hh, hash, entry, tmp) + HASH_DEL(hash, entry); free(entry); + return json_parse_oom(js); + } entry = malloc(sizeof(json_key_entry_t)); + if (!entry) { + HASH_ITER(hh, hash, entry, tmp) + HASH_DEL(hash, entry); free(entry); + return json_parse_oom(js); + } entry->key = k; entry->key_len = klen; entry->prop_off = off; HASH_ADD_KEYPTR(hh, hash, entry->key, entry->key_len, entry); }} - HASH_ITER(hh, hash, entry, tmp) { + HASH_ITER(hh, hash, entry, tmp) HASH_DEL(hash, entry); free(entry); - } - + return obj; } @@ -86,6 +135,11 @@ typedef struct { ant_value_t replacer_arr; ant_value_t error; ant_value_t holder; + + gc_temp_root_scope_t temp_roots; + gc_temp_root_handle_t error_handle; + gc_temp_root_handle_t holder_handle; + int stack_size; int stack_cap; int replacer_arr_len; @@ -101,15 +155,31 @@ static inline ant_value_t json_normalize_error(ant_value_t value) { return value; } +static void json_set_error(json_cycle_ctx *ctx, ant_value_t value) { + ctx->error = value; + gc_temp_root_set(ctx->error_handle, value); +} + +static inline bool json_ctx_pin_value(json_cycle_ctx *ctx, ant_value_t value) { + if (json_temp_pin(&ctx->temp_roots, value)) return true; + json_set_error(ctx, json_stringify_oom(ctx->js)); + return false; +} + +static inline void json_set_holder(json_cycle_ctx *ctx, ant_value_t value) { + ctx->holder = value; + gc_temp_root_set(ctx->holder_handle, value); +} + static void json_capture_error(json_cycle_ctx *ctx, ant_value_t value) { if (vtype(ctx->error) != T_UNDEF) return; if (ctx->js->thrown_exists) { - ctx->error = ctx->js->thrown_value; + json_set_error(ctx, ctx->js->thrown_value); ctx->js->thrown_exists = false; ctx->js->thrown_value = js_mkundef(); return; } - ctx->error = json_normalize_error(value); + json_set_error(ctx, json_normalize_error(value)); } static yyjson_mut_val *json_string_to_yyjson(ant_t *js, yyjson_mut_doc *doc, ant_value_t value) { @@ -188,7 +258,8 @@ static yyjson_mut_val *ant_value_to_yyjson_with_key( static ant_value_t apply_reviver( ant_t *js, ant_value_t holder, - const char *key, ant_value_t reviver + const char *key, ant_value_t reviver, + gc_temp_root_scope_t *roots ); static ant_value_t json_apply_tojson( @@ -206,7 +277,14 @@ static ant_value_t json_apply_tojson( } if (!is_callable(toJSON)) return val; - ant_value_t args[1] = { js_mkstr(js, key, strlen(key)) }; + ant_value_t key_arg = js_mkstr(js, key, strlen(key)); + if (is_err(key_arg)) { + json_capture_error(ctx, key_arg); + return js_mkundef(); + } + + if (!json_ctx_pin_value(ctx, key_arg)) return js_mkundef(); + ant_value_t args[1] = { key_arg }; ant_value_t transformed = sv_vm_call( js->vm, js, @@ -218,6 +296,7 @@ static ant_value_t json_apply_tojson( json_capture_error(ctx, transformed); return js_mkundef(); } + if (!json_ctx_pin_value(ctx, transformed)) return js_mkundef(); return transformed; } @@ -229,7 +308,13 @@ static ant_value_t json_apply_replacer( json_cycle_ctx *ctx ) { if (!is_callable(ctx->replacer_func)) return val; - ant_value_t args[2] = { js_mkstr(js, key, strlen(key)), val }; + ant_value_t key_arg = js_mkstr(js, key, strlen(key)); + if (is_err(key_arg)) { + json_capture_error(ctx, key_arg); + return js_mkundef(); + } + if (!json_ctx_pin_value(ctx, key_arg)) return js_mkundef(); + ant_value_t args[2] = { key_arg, val }; ant_value_t transformed = sv_vm_call( js->vm, js, @@ -241,13 +326,16 @@ static ant_value_t json_apply_replacer( json_capture_error(ctx, transformed); return js_mkundef(); } + if (!json_ctx_pin_value(ctx, transformed)) return js_mkundef(); return transformed; } -static inline ant_value_t json_create_root_holder(ant_t *js, ant_value_t value) { +static inline ant_value_t json_create_root_holder(ant_t *js, ant_value_t value, json_cycle_ctx *ctx) { ant_value_t holder = js_mkobj(js); - if (!is_err(holder)) js_set(js, holder, "", value); + if (is_err(holder)) return holder; + if (!json_ctx_pin_value(ctx, holder)) return js_mkundef(); + js_set(js, holder, "", value); return holder; } @@ -258,20 +346,20 @@ static yyjson_mut_val *json_array_to_yyjson( ant_offset_t length = js_arr_len(js, val); ant_value_t saved_holder = ctx->holder; - ctx->holder = val; + json_set_holder(ctx, val); for (ant_offset_t i = 0; i < length; i++) { char idxstr[32]; uint_to_str(idxstr, sizeof(idxstr), (uint64_t)i); ant_value_t elem = js_arr_get(js, val, i); yyjson_mut_val *item = ant_value_to_yyjson_with_key(js, doc, idxstr, elem, ctx, 1); if (json_has_abort(ctx)) { - ctx->holder = saved_holder; + json_set_holder(ctx, saved_holder); return NULL; } yyjson_mut_arr_add_val(arr, item); } - ctx->holder = saved_holder; + json_set_holder(ctx, saved_holder); return arr; } @@ -286,8 +374,9 @@ static yyjson_mut_val *json_object_to_yyjson( json_capture_error(ctx, keys); return NULL; } + if (!json_ctx_pin_value(ctx, keys)) return NULL; - ctx->holder = val; + json_set_holder(ctx, val); ant_offset_t key_count = js_arr_len(js, keys); for (ant_offset_t i = 0; i < key_count; i++) { @@ -301,13 +390,13 @@ static yyjson_mut_val *json_object_to_yyjson( ant_value_t prop = js_get(js, val, key); if (is_err(prop)) { json_capture_error(ctx, prop); - ctx->holder = saved_holder; + json_set_holder(ctx, saved_holder); return NULL; } yyjson_mut_val *jval = ant_value_to_yyjson_with_key(js, doc, key, prop, ctx, 0); if (json_has_abort(ctx)) { - ctx->holder = saved_holder; + json_set_holder(ctx, saved_holder); return NULL; } @@ -315,7 +404,7 @@ static yyjson_mut_val *json_object_to_yyjson( yyjson_mut_obj_add(obj, yyjson_mut_strncpy(doc, key, key_len), jval); } - ctx->holder = saved_holder; + json_set_holder(ctx, saved_holder); return obj; } @@ -379,8 +468,16 @@ static yyjson_mut_val *ant_value_to_yyjson(ant_t *js, yyjson_mut_doc *doc, ant_v return ant_value_to_yyjson_with_key(js, doc, "", val, ctx, 0); } -static ant_value_t apply_reviver_call(ant_t *js, ant_value_t holder, const char *key, ant_value_t reviver) { +static ant_value_t apply_reviver_call( + ant_t *js, + ant_value_t holder, + const char *key, + ant_value_t reviver, + gc_temp_root_scope_t *roots +) { ant_value_t key_str = js_mkstr(js, key, strlen(key)); + if (is_err(key_str)) return key_str; + if (!json_temp_pin(roots, key_str)) return json_parse_oom(js); ant_value_t current_value = js_get(js, holder, key); ant_value_t call_args[2] = { key_str, current_value }; @@ -388,27 +485,41 @@ static ant_value_t apply_reviver_call(ant_t *js, ant_value_t holder, const char js->vm, js, reviver, holder, call_args, 2, NULL, false ); + if (!is_err(result) && !json_temp_pin(roots, result)) return json_parse_oom(js); return result; } -static void apply_reviver_to_array(ant_t *js, ant_value_t value, ant_value_t reviver) { +static void apply_reviver_to_array( + ant_t *js, + ant_value_t value, + ant_value_t reviver, + gc_temp_root_scope_t *roots +) { ant_offset_t length = js_arr_len(js, value); for (ant_offset_t i = 0; i < length; i++) { char idxstr[32]; size_t idx_len = uint_to_str(idxstr, sizeof(idxstr), (uint64_t)i); - ant_value_t new_elem = apply_reviver(js, value, idxstr, reviver); + ant_value_t new_elem = apply_reviver(js, value, idxstr, reviver, roots); if (vtype(new_elem) == T_UNDEF) js_delete_prop(js, value, idxstr, idx_len); else { ant_value_t key_val = js_mkstr(js, idxstr, idx_len); + if (is_err(key_val)) return; + if (!json_temp_pin(roots, key_val)) return; js_setprop(js, value, key_val, new_elem); }} } -static void apply_reviver_to_object(ant_t *js, ant_value_t value, ant_value_t reviver) { +static void apply_reviver_to_object( + ant_t *js, + ant_value_t value, + ant_value_t reviver, + gc_temp_root_scope_t *roots +) { ant_value_t keys = json_snapshot_keys(js, value); if (is_err(keys) || vtype(keys) != T_ARR) return; + if (!json_temp_pin(roots, keys)) return; ant_offset_t key_count = js_arr_len(js, keys); for (ant_offset_t i = 0; i < key_count; i++) { @@ -416,48 +527,70 @@ static void apply_reviver_to_object(ant_t *js, ant_value_t value, ant_value_t re size_t key_len = 0; char *key = js_getstr(js, key_val, &key_len); if (!key) continue; - ant_value_t new_val = apply_reviver(js, value, key, reviver); + ant_value_t new_val = apply_reviver(js, value, key, reviver, roots); if (vtype(new_val) == T_UNDEF) js_delete_prop(js, value, key, key_len); else js_set(js, value, key, new_val); } } -static ant_value_t apply_reviver(ant_t *js, ant_value_t holder, const char *key, ant_value_t reviver) { +static ant_value_t apply_reviver( + ant_t *js, + ant_value_t holder, + const char *key, + ant_value_t reviver, + gc_temp_root_scope_t *roots +) { ant_value_t val = js_get(js, holder, key); - if (json_is_array(val)) apply_reviver_to_array(js, val, reviver); - else if (vtype(val) == T_OBJ) apply_reviver_to_object(js, val, reviver); + if (json_is_array(val)) apply_reviver_to_array(js, val, reviver, roots); + else if (vtype(val) == T_OBJ) apply_reviver_to_object(js, val, reviver, roots); - return apply_reviver_call(js, holder, key, reviver); + return apply_reviver_call(js, holder, key, reviver, roots); } ant_value_t js_json_parse(ant_t *js, ant_value_t *args, int nargs) { if (nargs < 1) return js_mkerr(js, "JSON.parse() requires at least 1 argument"); if (vtype(args[0]) != T_STR) return js_mkerr(js, "JSON.parse() argument must be a string"); - bool saved_gc_disabled = gc_disabled; + gc_temp_root_scope_t temp_roots; + gc_temp_root_scope_begin(js, &temp_roots); size_t len; char *json_str = js_getstr(js, args[0], &len); - gc_disabled = true; yyjson_doc *doc = yyjson_read(json_str, len, 0); if (!doc) { - gc_disabled = saved_gc_disabled; + gc_temp_root_scope_end(&temp_roots); return js_mkerr_typed(js, JS_ERR_SYNTAX, "JSON.parse: unexpected character"); } - ant_value_t result = yyjson_to_jsval(js, yyjson_doc_get_root(doc)); + ant_value_t result = yyjson_to_jsval(js, yyjson_doc_get_root(doc), &temp_roots); yyjson_doc_free(doc); + if (is_err(result)) { + gc_temp_root_scope_end(&temp_roots); + return result; + } if (nargs >= 2 && is_callable(args[1])) { ant_value_t reviver = args[1]; + if (!json_temp_pin(&temp_roots, reviver)) { + gc_temp_root_scope_end(&temp_roots); + return json_parse_oom(js); + } ant_value_t root = js_mkobj(js); + if (is_err(root)) { + gc_temp_root_scope_end(&temp_roots); + return root; + } + if (!json_temp_pin(&temp_roots, root)) { + gc_temp_root_scope_end(&temp_roots); + return json_parse_oom(js); + } js_set(js, root, "", result); - result = apply_reviver(js, root, "", reviver); + result = apply_reviver(js, root, "", reviver, &temp_roots); } - gc_disabled = saved_gc_disabled; + gc_temp_root_scope_end(&temp_roots); return result; } @@ -483,7 +616,6 @@ static yyjson_write_flag get_write_flags(ant_value_t *args, int nargs) { ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { ant_value_t result; yyjson_mut_doc *doc = NULL; - bool saved_gc_disabled = gc_disabled; json_cycle_ctx ctx = { .js = js, @@ -498,6 +630,20 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { ant_value_t root_holder = js_mkundef(); if (nargs < 1) return js_mkerr(js, "JSON.stringify() requires at least 1 argument"); + gc_temp_root_scope_begin(js, &ctx.temp_roots); + ctx.error_handle = gc_temp_root_add(&ctx.temp_roots, ctx.error); + ctx.holder_handle = gc_temp_root_add(&ctx.temp_roots, ctx.holder); + + if (!gc_temp_root_handle_valid(ctx.error_handle) || !gc_temp_root_handle_valid(ctx.holder_handle)) { + gc_temp_root_scope_end(&ctx.temp_roots); + return json_stringify_oom(js); + } + + if (!json_ctx_pin_value(&ctx, args[0])) { + result = ctx.error; + goto cleanup; + } + int top_type = vtype(args[0]); if (nargs < 2 && top_type == T_STR) { @@ -507,18 +653,23 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { char *str = js_getstr(js, args[0], &byte_len); char *raw = utf8_json_quote(str, byte_len, &raw_len); - if (!raw) return js_mkerr(js, "JSON.stringify() failed: out of memory"); + if (!raw) { + result = js_mkerr(js, "JSON.stringify() failed: out of memory"); + goto cleanup; + } result = js_mkstr(js, raw, raw_len); free(raw); - - return result; + goto cleanup; } - - gc_disabled = true; if (nargs >= 2) { ant_value_t replacer = args[1]; - if (is_callable(replacer)) ctx.replacer_func = replacer; + if (is_callable(replacer)) { + ctx.replacer_func = replacer; + if (!json_ctx_pin_value(&ctx, replacer)) { + result = ctx.error; + goto cleanup; + }} else if (is_special_object(replacer)) { ant_value_t len_val = js_get(js, replacer, "length"); @@ -526,18 +677,30 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { if (vtype(len_val) == T_NUM) { ctx.replacer_arr = replacer; ctx.replacer_arr_len = (int)js_getnum(len_val); - }}} + if (!json_ctx_pin_value(&ctx, replacer)) { + result = ctx.error; + goto cleanup; + } + }}} doc = yyjson_mut_doc_new(NULL); - if (!doc) return js_mkerr(js, "JSON.stringify() failed: out of memory"); + if (!doc) { + result = js_mkerr(js, "JSON.stringify() failed: out of memory"); + goto cleanup; + } - root_holder = json_create_root_holder(js, args[0]); + root_holder = json_create_root_holder(js, args[0], &ctx); if (is_err(root_holder)) { result = root_holder; goto cleanup; } - ctx.holder = root_holder; + if (vtype(root_holder) == T_UNDEF && vtype(ctx.error) != T_UNDEF) { + result = ctx.error; + goto cleanup; + } + + json_set_holder(&ctx, root_holder); yyjson_mut_val *root = ant_value_to_yyjson(js, doc, args[0], &ctx); if (vtype(ctx.error) != T_UNDEF) { @@ -567,10 +730,10 @@ ant_value_t js_json_stringify(ant_t *js, ant_value_t *args, int nargs) { result = js_mkstr(js, json_str, len); cleanup: - gc_disabled = saved_gc_disabled; free(json_str); free(ctx.stack); yyjson_mut_doc_free(doc); + gc_temp_root_scope_end(&ctx.temp_roots); return result; } diff --git a/tests/json_temp_roots_stress.cjs b/tests/json_temp_roots_stress.cjs new file mode 100644 index 0000000..dbcbe0b --- /dev/null +++ b/tests/json_temp_roots_stress.cjs @@ -0,0 +1,52 @@ +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function gcPressure() { + const junk = []; + for (let i = 0; i < 256; i++) { + junk.push({ + i, + label: 'x'.repeat(32), + nested: [i, i + 1, i + 2] + }); + } + return junk.length; +} + +const parsed = JSON.parse('{"outer":{"items":[{"name":"a"},{"name":"b"}]}}', function (key, value) { + gcPressure(); + if (key === 'name') return value.toUpperCase(); + return value; +}); + +assert(parsed.outer.items[0].name === 'A', 'reviver should preserve nested values under GC pressure'); +assert(parsed.outer.items[1].name === 'B', 'reviver should preserve sibling values under GC pressure'); + +const source = { + title: 'root', + nested: { + keep: 'ok', + value: 7, + toJSON() { + gcPressure(); + return { + keep: this.keep, + value: this.value + }; + } + } +}; + +const json = JSON.stringify(source, function (key, value) { + gcPressure(); + if (key === 'title') return value + '-done'; + return value; +}); + +const roundTrip = JSON.parse(json); +assert(roundTrip.title === 'root-done', 'replacer should preserve transformed root properties'); +assert(roundTrip.nested.keep === 'ok', 'toJSON result should survive GC pressure'); +assert(roundTrip.nested.value === 7, 'nested numeric values should survive GC pressure'); + +console.log('json temp roots stress: ok'); -- 2.51.2