From 2d710143ce84bedbd951ac0336789a45638c48db Mon Sep 17 00:00:00 2001 From: yuhao Date: Wed, 30 Sep 2026 02:36:23 +0800 Subject: [PATCH 1/2] Make configured memory-limit exhaustion optionally terminal Embedders can opt in so crossing JS_SetMemoryLimit is a host-visible termination that JavaScript cannot catch, including when no Error object can be allocated. Co-authored-by: Cursor --- api-test.c | 507 +++++++++++++++++++++++++++++ docs/docs/developer-guide/intro.md | 7 + quickjs.c | 95 +++++- quickjs.h | 15 + 4 files changed, 615 insertions(+), 9 deletions(-) diff --git a/api-test.c b/api-test.c index 8a3ce9eec..a9f1b5374 100644 --- a/api-test.c +++ b/api-test.c @@ -2,6 +2,7 @@ #undef NDEBUG #endif #include +#include #include #include #include @@ -2195,6 +2196,511 @@ void private_symbols(void) JS_FreeRuntime(rt); } +/* Opt-in terminal handling of JS_SetMemoryLimit exhaustion. + The runtime is not reusable after the condition is reached. */ +typedef struct { + int allocator; + size_t headroom; + int caught, continued, success, null_caught; + int armed, refused; + size_t live; +} MemState; + +typedef union { + max_align_t alignment; + size_t size; +} MemBlock; + +static void *mem_term_alloc(void *opaque, size_t size) +{ + MemState *s = opaque; + MemBlock *b; + if (s->armed && size >= 1024 * 1024) { + s->refused++; + return NULL; + } + b = malloc(sizeof(*b) + size); + if (!b) + return NULL; + b->size = size; + s->live++; + return b + 1; +} + +static void mem_term_free(void *opaque, void *ptr) +{ + MemState *s = opaque; + if (!ptr) + return; + s->live--; + free((MemBlock *)ptr - 1); +} + +static void *mem_term_realloc(void *opaque, void *ptr, size_t size) +{ + void *p; + size_t old; + if (!ptr) + return mem_term_alloc(opaque, size); + if (!size) { + mem_term_free(opaque, ptr); + return NULL; + } + p = mem_term_alloc(opaque, size); + if (!p) + return NULL; + old = ((MemBlock *)ptr - 1)->size; + memcpy(p, ptr, old < size ? old : size); + mem_term_free(opaque, ptr); + return p; +} + +static void *mem_term_calloc(void *opaque, size_t count, size_t size) +{ + void *p; + if (size && count > SIZE_MAX / size) + return NULL; + p = mem_term_alloc(opaque, count * size); + if (p) + memset(p, 0, count * size); + return p; +} + +static size_t mem_term_usable(const void *ptr) +{ + return ptr ? ((const MemBlock *)ptr - 1)->size : 0; +} + +static const JSMallocFunctions mem_term_mf = { + mem_term_calloc, + mem_term_alloc, + mem_term_free, + mem_term_realloc, + mem_term_usable, +}; + +static JSValue mem_term_arm(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + MemState *s = JS_GetContextOpaque(ctx); + JSMemoryUsage usage; + if (s->allocator) { + s->armed = 1; + return JS_UNDEFINED; + } + JS_ComputeMemoryUsage(JS_GetRuntime(ctx), &usage); + JS_SetMemoryLimit(JS_GetRuntime(ctx), (size_t)usage.malloc_size + s->headroom); + return JS_UNDEFINED; +} + +static JSValue mem_term_mark(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) +{ + MemState *s = JS_GetContextOpaque(ctx); + int32_t kind = -1; + assert(argc >= 1); + assert(JS_ToInt32(ctx, &kind, argv[0]) == 0); + if (kind == 0) { + s->caught++; + if (argc > 1 && JS_IsNull(argv[1])) + s->null_caught++; + } else if (kind == 1) { + s->continued++; + } else if (kind == 2) { + s->success++; + } + return JS_UNDEFINED; +} + +static void mem_term_install(JSContext *ctx) +{ + JSValue g = JS_GetGlobalObject(ctx); + assert(JS_SetPropertyStr(ctx, g, "arm", + JS_NewCFunction(ctx, mem_term_arm, "arm", 0)) >= 0); + assert(JS_SetPropertyStr(ctx, g, "mark", + JS_NewCFunction(ctx, mem_term_mark, "mark", 2)) >= 0); + JS_FreeValue(ctx, g); +} + +static JSRuntime *mem_term_runtime(MemState *s, int terminal) +{ + JSRuntime *rt = s->allocator ? JS_NewRuntime2(&mem_term_mf, s) : JS_NewRuntime(); + assert(rt); + JS_SetDumpFlags(rt, JS_ABORT_ON_LEAKS); + if (terminal) + JS_SetMemoryLimitTermination(rt, true); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + return rt; +} + +static void mem_term_shutdown(JSRuntime *rt, JSContext *ctx, MemState *s) +{ + JS_SetMemoryLimit(rt, 0); + s->armed = 0; + JS_FreeContext(ctx); + JS_FreeRuntime(rt); + assert(s->live == 0); +} + +static void mem_term_eval_ok(JSContext *ctx, const char *code) +{ + JSValue ret = eval(ctx, code); + assert(!JS_IsException(ret)); + JS_FreeValue(ctx, ret); +} + +/* Returns 1 when evaluation itself failed. */ +static int mem_term_eval(JSContext *ctx, const char *code) +{ + JSValue ret = eval(ctx, code); + int failed = JS_IsException(ret); + JS_FreeValue(ctx, ret); + return failed; +} + +static void mem_term_expect_quiescent(MemState *s, JSRuntime *rt) +{ + assert(s->caught == 0 && s->continued == 0 && s->success == 0); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_MEMORY_LIMIT); +} + +static void mem_term_not_reusable(JSRuntime *rt, JSContext *ctx, MemState *s) +{ + JSValue ret, exc; + JSContext *job_ctx = ctx; + JS_SetMemoryLimit(rt, 0); + ret = eval(ctx, "mark(2)"); + assert(JS_IsException(ret)); + JS_FreeValue(ctx, ret); + assert(JS_HasException(ctx)); + exc = JS_GetException(ctx); + assert(JS_IsNull(exc)); + JS_FreeValue(ctx, exc); + assert(s->success == 0); + if (JS_IsJobPending(rt)) + assert(JS_ExecutePendingJob(rt, &job_ctx) < 0); + assert(s->caught == 0 && s->continued == 0 && s->success == 0); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_MEMORY_LIMIT); +} + +static const char mem_term_prelude[] = + "function trigger() { arm(); new ArrayBuffer(1024 * 1024); }\n"; + +static void memory_limit_termination(void) +{ + static const char sync_code[] = + "try { trigger(); } catch (e) { mark(0, e); } mark(1); mark(2);"; + static const char job_code[] = + "Promise.resolve().then(() => {" + " try { trigger(); } catch (e) { mark(0, e); }" + " mark(1);" + "}).then(() => { mark(2); });"; + static const char await_code[] = + "(async () => {" + " await 0;" + " try { trigger(); } catch (e) { mark(0, e); }" + " mark(1);" + "})().then(() => { mark(2); });"; + static const char executor_code[] = + "try { new Promise(() => { trigger(); }); } catch (e) { mark(0, e); }" + "mark(1); mark(2);"; + static const char thenable_code[] = + "Promise.resolve({ then() {" + " try { trigger(); } catch (e) { mark(0, e); }" + " mark(1);" + "}}).catch((e) => { mark(0, e); }).then(() => { mark(2); });"; + static const char ordinary[] = + "try { throw new Error('e'); } catch (e) { mark(0); }" + "try { throw new RangeError('r'); } catch (e) { mark(0); }" + "try { null.f; } catch (e) { mark(0); }" + "try { eval('}'); } catch (e) { mark(0); }" + "mark(1);" + "Promise.reject(42).catch(() => { mark(0); }).then(() => { mark(2); });"; + MemState s; + JSRuntime *rt; + JSContext *ctx, *job_ctx; + JSValue exc; + int steps, saw; + + /* Existing behavior: exhaustion stays catchable, including a null Error. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 0); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, sync_code)); + assert(s.caught == 1 && s.continued == 1 && s.success == 1); + assert(s.null_caught == 0); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + mem_term_shutdown(rt, ctx, &s); + + memset(&s, 0, sizeof(s)); + s.headroom = 1; + rt = mem_term_runtime(&s, 0); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, sync_code)); + assert(s.caught == 1 && s.null_caught == 1); + assert(s.continued == 1 && s.success == 1); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + mem_term_shutdown(rt, ctx, &s); + + /* The configured-limit check records termination before any exception exists. */ + memset(&s, 0, sizeof(s)); + rt = mem_term_runtime(&s, 0); + ctx = JS_NewContext(rt); + assert(ctx); + { + JSMemoryUsage usage; + JS_ComputeMemoryUsage(rt, &usage); + JS_SetMemoryLimit(rt, (size_t)usage.malloc_size + 1); + } + assert(js_malloc_rt(rt, 1024 * 1024) == NULL); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + assert(!JS_HasException(ctx)); + mem_term_shutdown(rt, ctx, &s); + + memset(&s, 0, sizeof(s)); + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + { + JSMemoryUsage usage; + JS_ComputeMemoryUsage(rt, &usage); + JS_SetMemoryLimit(rt, (size_t)usage.malloc_size + 1); + } + assert(js_malloc_rt(rt, 1024 * 1024) == NULL); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_MEMORY_LIMIT); + assert(!JS_HasException(ctx)); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Terminal synchronous path. The diagnostic Error can still be allocated. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(mem_term_eval(ctx, sync_code)); + mem_term_expect_quiescent(&s, rt); + exc = JS_GetException(ctx); + assert(JS_IsError(exc)); + JS_FreeValue(ctx, exc); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* No spare headroom: the Error allocation fails and the value is null. */ + memset(&s, 0, sizeof(s)); + s.headroom = 1; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(mem_term_eval(ctx, sync_code)); + mem_term_expect_quiescent(&s, rt); + exc = JS_GetException(ctx); + assert(JS_IsNull(exc)); + assert(!JS_IsUncatchableError(exc)); + JS_FreeValue(ctx, exc); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Pending job: catch inside the reaction must not recover. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, job_code)); + assert(s.caught == 0 && s.success == 0); + job_ctx = ctx; + steps = 0; + saw = 0; + while (JS_IsJobPending(rt)) { + int r; + assert(++steps < 20); + r = JS_ExecutePendingJob(rt, &job_ctx); + if (r < 0) { + saw = 1; + break; + } + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + } + assert(saw); + mem_term_expect_quiescent(&s, rt); + assert(JS_HasException(job_ctx)); + exc = JS_GetException(job_ctx); + assert(JS_IsError(exc)); + JS_FreeValue(job_ctx, exc); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Same job route with no room to allocate the diagnostic Error. */ + memset(&s, 0, sizeof(s)); + s.headroom = 1; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, job_code)); + job_ctx = ctx; + steps = 0; + saw = 0; + while (JS_IsJobPending(rt)) { + int r; + assert(++steps < 20); + r = JS_ExecutePendingJob(rt, &job_ctx); + if (r < 0) { + saw = 1; + break; + } + } + assert(saw); + mem_term_expect_quiescent(&s, rt); + exc = JS_GetException(job_ctx); + assert(JS_IsNull(exc)); + assert(!JS_IsUncatchableError(exc)); + JS_FreeValue(job_ctx, exc); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Async resumption after await. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, await_code)); + job_ctx = ctx; + steps = 0; + saw = 0; + while (JS_IsJobPending(rt)) { + int r; + assert(++steps < 20); + r = JS_ExecutePendingJob(rt, &job_ctx); + if (r < 0) { + saw = 1; + break; + } + } + assert(saw); + mem_term_expect_quiescent(&s, rt); + if (JS_HasException(job_ctx)) + JS_FreeValue(job_ctx, JS_GetException(job_ctx)); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Promise constructor must not reject and continue. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(mem_term_eval(ctx, executor_code)); + mem_term_expect_quiescent(&s, rt); + if (JS_HasException(ctx)) + JS_FreeValue(ctx, JS_GetException(ctx)); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Thenable job must not reject into a user catch. */ + memset(&s, 0, sizeof(s)); + s.headroom = 65536; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, thenable_code)); + job_ctx = ctx; + steps = 0; + saw = 0; + while (JS_IsJobPending(rt)) { + int r; + assert(++steps < 20); + r = JS_ExecutePendingJob(rt, &job_ctx); + if (r < 0) { + saw = 1; + break; + } + } + assert(saw); + mem_term_expect_quiescent(&s, rt); + if (JS_HasException(job_ctx)) + JS_FreeValue(job_ctx, JS_GetException(job_ctx)); + mem_term_not_reusable(rt, ctx, &s); + mem_term_shutdown(rt, ctx, &s); + + /* Ordinary exceptions stay catchable while the option is enabled. */ + memset(&s, 0, sizeof(s)); + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + assert(!mem_term_eval(ctx, ordinary)); + assert(s.caught == 4 && s.continued == 1 && s.success == 0); + job_ctx = ctx; + while (JS_IsJobPending(rt)) + assert(JS_ExecutePendingJob(rt, &job_ctx) == 1); + assert(s.caught == 5 && s.success == 1); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + mem_term_shutdown(rt, ctx, &s); + + /* A custom allocator NULL is not a configured memory-limit termination. */ + memset(&s, 0, sizeof(s)); + s.allocator = 1; + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + JS_SetContextOpaque(ctx, &s); + mem_term_install(ctx); + mem_term_eval_ok(ctx, mem_term_prelude); + assert(!mem_term_eval(ctx, sync_code)); + assert(s.refused >= 1); + assert(s.caught == 1 && s.continued == 1 && s.success == 1); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + mem_term_shutdown(rt, ctx, &s); + + /* A fresh runtime is unaffected. */ + memset(&s, 0, sizeof(s)); + rt = mem_term_runtime(&s, 1); + ctx = JS_NewContext(rt); + assert(ctx); + exc = eval(ctx, "40 + 2"); + assert(!JS_IsException(exc)); + { + int32_t n = 0; + assert(JS_ToInt32(ctx, &n, exc) == 0 && n == 42); + } + JS_FreeValue(ctx, exc); + assert(JS_GetTerminationStatus(rt) == JS_TERMINATION_NONE); + mem_term_shutdown(rt, ctx, &s); +} + int main(void) { cfunctions(); @@ -2236,5 +2742,6 @@ int main(void) new_typed_array(); std_eval_interrupt_handler(); private_symbols(); + memory_limit_termination(); return 0; } diff --git a/docs/docs/developer-guide/intro.md b/docs/docs/developer-guide/intro.md index 551bb37b2..71a8d0016 100644 --- a/docs/docs/developer-guide/intro.md +++ b/docs/docs/developer-guide/intro.md @@ -97,6 +97,13 @@ of a native module. Use `JS_SetMemoryLimit()` to set a global memory allocation limit to a given `JSRuntime`. +By default, exceeding that limit throws a catchable out-of-memory +exception. `JS_SetMemoryLimitTermination()` opts into treating that +configured-limit exhaustion as terminal: JavaScript cannot catch it, and +`JS_GetTerminationStatus()` reports `JS_TERMINATION_MEMORY_LIMIT` even if +the exception object cannot be allocated. Free the runtime afterwards. +Arbitrary allocator failures are unchanged. + Custom memory allocation functions can be provided with `JS_NewRuntime2()`. The maximum system stack size can be set with `JS_SetMaxStackSize()`. diff --git a/quickjs.c b/quickjs.c index 30bf6c8b5..12f0e7043 100644 --- a/quickjs.c +++ b/quickjs.c @@ -372,6 +372,10 @@ struct JSRuntime { JSValue current_exception; /* true if inside an out of memory error, to avoid recursing */ bool in_out_of_memory; + /* Opt-in: configured JS_SetMemoryLimit exhaustion is terminal. */ + bool terminate_on_memory_limit; + /* Sticky until JS_FreeRuntime. Independent of any exception object. */ + bool memory_limit_exhausted; /* true if inside build_backtrace, to avoid recursing */ bool in_build_stack_trace; /* true if inside JS_FreeRuntime */ @@ -1976,6 +1980,15 @@ static void js_arena_free_all(JSRuntime *rt) } } +/* Record configured JS_SetMemoryLimit exhaustion. malloc_limit == 0 is + unlimited, so that comparison is not this event. Allocator NULL is not + recorded here. The flag is sticky for the life of the runtime. */ +static inline void js_memory_limit_hit(JSRuntime *rt) +{ + if (rt->terminate_on_memory_limit && rt->malloc_state.malloc_limit != 0) + rt->memory_limit_exhausted = true; +} + void *js_calloc_rt(JSRuntime *rt, size_t count, size_t size) { void *ptr; @@ -1990,8 +2003,10 @@ void *js_calloc_rt(JSRuntime *rt, size_t count, size_t size) s = &rt->malloc_state; /* When malloc_limit is 0 (unlimited), malloc_limit - 1 will be SIZE_MAX. */ - if (unlikely(s->malloc_size + (count * size) > s->malloc_limit - 1)) + if (unlikely(s->malloc_size + (count * size) > s->malloc_limit - 1)) { + js_memory_limit_hit(rt); return NULL; + } ptr = js_arena_calloc(rt, count, size); if (!ptr) @@ -2013,8 +2028,10 @@ void *js_malloc_rt(JSRuntime *rt, size_t size) s = &rt->malloc_state; /* When malloc_limit is 0 (unlimited), malloc_limit - 1 will be SIZE_MAX. */ - if (unlikely(s->malloc_size + size > s->malloc_limit - 1)) + if (unlikely(s->malloc_size + size > s->malloc_limit - 1)) { + js_memory_limit_hit(rt); return NULL; + } ptr = js_arena_malloc(rt, size); if (!ptr) @@ -2060,8 +2077,10 @@ void *js_realloc_rt(JSRuntime *rt, void *ptr, size_t size) old_size = js_arena_usable_size(rt, ptr); s = &rt->malloc_state; /* When malloc_limit is 0 (unlimited), malloc_limit - 1 will be SIZE_MAX. */ - if (s->malloc_size + size - old_size > s->malloc_limit - 1) + if (s->malloc_size + size - old_size > s->malloc_limit - 1) { + js_memory_limit_hit(rt); return NULL; + } ptr = js_arena_realloc(rt, ptr, size); if (!ptr) @@ -2438,6 +2457,18 @@ void JS_SetMemoryLimit(JSRuntime *rt, size_t limit) rt->malloc_state.malloc_limit = limit; } +void JS_SetMemoryLimitTermination(JSRuntime *rt, bool enable) +{ + rt->terminate_on_memory_limit = enable; +} + +JSTerminationStatus JS_GetTerminationStatus(JSRuntime *rt) +{ + if (rt->memory_limit_exhausted) + return JS_TERMINATION_MEMORY_LIMIT; + return JS_TERMINATION_NONE; +} + void JS_SetDumpFlags(JSRuntime *rt, uint64_t flags) { #ifdef ENABLE_DUMPS @@ -2535,6 +2566,13 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) return 0; } + /* Do not run JavaScript continuations after the memory limit terminated + execution. Leave the job queued; the host should free the runtime. */ + if (unlikely(rt->memory_limit_exhausted)) { + *pctx = list_entry(rt->job_list.next, JSJobEntry, link)->ctx; + return -1; + } + /* get the first pending job and execute it */ e = list_entry(rt->job_list.next, JSJobEntry, link); list_del(&e->link); @@ -2542,7 +2580,7 @@ int JS_ExecutePendingJob(JSRuntime *rt, JSContext **pctx) res = e->job_func(e->ctx, e->argc, vc(e->argv)); for(i = 0; i < e->argc; i++) JS_FreeValue(ctx, e->argv[i]); - if (JS_IsException(res)) + if (JS_IsException(res) || unlikely(rt->memory_limit_exhausted)) ret = -1; else ret = 1; @@ -8562,6 +8600,10 @@ JSValue JS_ThrowOutOfMemory(JSContext *ctx) if (!rt->in_out_of_memory) { rt->in_out_of_memory = true; JS_ThrowInternalError(ctx, "out of memory"); + /* Diagnostic only. memory_limit_exhausted is the authority and + remains set when this Error cannot be allocated (exception is null). */ + if (rt->memory_limit_exhausted) + JS_SetUncatchableError(ctx, rt->current_exception); rt->in_out_of_memory = false; } return JS_EXCEPTION; @@ -12060,6 +12102,25 @@ bool JS_IsUncatchableError(JSValueConst val) return p->class_id == JS_CLASS_ERROR && p->is_uncatchable_error; } +/* memory_limit_exhausted is independent of exc. exc may be JS_NULL when the + diagnostic Error could not be allocated. */ +static inline bool js_is_uncatchable_exception(JSRuntime *rt, JSValueConst exc) +{ + return rt->memory_limit_exhausted || JS_IsUncatchableError(exc); +} + +/* Block a new JavaScript entry after configured-limit termination. + Returns 1 and ensures a pending exception. */ +static int js_memory_limit_blocks_entry(JSContext *ctx) +{ + JSRuntime *rt = ctx->rt; + if (likely(!rt->memory_limit_exhausted) || rt->in_out_of_memory || rt->in_free) + return 0; + if (JS_IsUninitialized(rt->current_exception)) + JS_Throw(ctx, JS_NULL); + return 1; +} + static void js_set_uncatchable_error(JSContext *ctx, JSValueConst val, bool flag) { JSObject *p; @@ -18051,6 +18112,9 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, JSVarRef **var_refs; size_t alloca_size; + if (unlikely(js_memory_limit_blocks_entry(caller_ctx))) + return JS_EXCEPTION; + #ifdef ENABLE_DUMPS // JS_DUMP_BYTECODE_STEP #define DUMP_BYTECODE_OR_DONT(pc) \ if (check_dump_flag(ctx->rt, JS_DUMP_BYTECODE_STEP)) dump_single_byte_code(ctx, pc, b, 0); @@ -20926,7 +20990,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, build_backtrace(ctx, rt->current_exception, JS_UNDEFINED, NULL, 0, 0, 0); } - if (!JS_IsUncatchableError(rt->current_exception)) { + if (!js_is_uncatchable_exception(rt, rt->current_exception)) { while (sp > stack_buf) { JSValue val = *--sp; JS_FreeValue(ctx, val); @@ -21507,7 +21571,7 @@ static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s) func_ret = async_func_resume(ctx, &s->func_state); if (JS_IsException(func_ret)) { fail: - if (unlikely(JS_IsUncatchableError(ctx->rt->current_exception))) { + if (unlikely(js_is_uncatchable_exception(ctx->rt, ctx->rt->current_exception))) { is_success = false; } else { JSValue error = JS_GetException(ctx); @@ -21516,7 +21580,7 @@ static bool js_async_function_resume(JSContext *ctx, JSAsyncFunctionData *s) JS_FreeValue(ctx, error); resolved: if (unlikely(JS_IsException(ret2))) { - if (JS_IsUncatchableError(ctx->rt->current_exception)) { + if (js_is_uncatchable_exception(ctx->rt, ctx->rt->current_exception)) { is_success = false; } else { abort(); /* BUG */ @@ -38283,6 +38347,9 @@ static JSValue JS_EvalInternal(JSContext *ctx, JSValueConst this_obj, { JSRuntime *rt = ctx->rt; + if (unlikely(js_memory_limit_blocks_entry(ctx))) + return JS_EXCEPTION; + if (unlikely(!ctx->eval_internal)) { return JS_ThrowTypeError(ctx, "eval is not supported"); } @@ -55626,7 +55693,7 @@ static JSValue promise_reaction_job(JSContext *ctx, int argc, } is_reject = JS_IsException(res); if (is_reject) { - if (unlikely(JS_IsUncatchableError(ctx->rt->current_exception))) + if (unlikely(js_is_uncatchable_exception(ctx->rt, ctx->rt->current_exception))) return JS_EXCEPTION; res = JS_GetException(ctx); } @@ -55728,7 +55795,13 @@ static JSValue js_promise_resolve_thenable_job(JSContext *ctx, rt->promise_hook_opaque); } if (JS_IsException(res)) { - JSValue error = JS_GetException(ctx); + JSValue error; + if (unlikely(ctx->rt->memory_limit_exhausted)) { + JS_FreeValue(ctx, args[0]); + JS_FreeValue(ctx, args[1]); + return JS_EXCEPTION; + } + error = JS_GetException(ctx); res = JS_Call(ctx, args[1], JS_UNDEFINED, 1, vc(&error)); JS_FreeValue(ctx, error); } @@ -55847,6 +55920,8 @@ static JSValue js_promise_resolve_function_call(JSContext *ctx, if (JS_IsException(then)) { JSValue error; fail_reject: + if (unlikely(ctx->rt->memory_limit_exhausted)) + return JS_EXCEPTION; error = JS_GetException(ctx); fulfill_or_reject_promise(ctx, s->promise, error, true); JS_FreeValue(ctx, error); @@ -55958,6 +56033,8 @@ static JSValue js_promise_constructor(JSContext *ctx, JSValueConst new_target, ret = JS_Call(ctx, executor, JS_UNDEFINED, 2, vc(args)); if (JS_IsException(ret)) { JSValue ret2, error; + if (unlikely(ctx->rt->memory_limit_exhausted)) + goto fail; error = JS_GetException(ctx); ret2 = JS_Call(ctx, args[1], JS_UNDEFINED, 1, vc(&error)); JS_FreeValue(ctx, error); diff --git a/quickjs.h b/quickjs.h index 7db40213c..b01ff115a 100644 --- a/quickjs.h +++ b/quickjs.h @@ -511,8 +511,23 @@ typedef struct JSGCObjectHeader JSGCObjectHeader; JS_EXTERN JSRuntime *JS_NewRuntime(void); /* info lifetime must exceed that of rt */ JS_EXTERN void JS_SetRuntimeInfo(JSRuntime *rt, const char *info); +typedef enum JSTerminationStatus { + JS_TERMINATION_NONE = 0, + /* JS_SetMemoryLimit rejected an allocation. Not set for arbitrary + allocator failures, system OOM, or ordinary exceptions. Sticky until + JS_FreeRuntime; the runtime is not reusable after this. */ + JS_TERMINATION_MEMORY_LIMIT, +} JSTerminationStatus; + /* use 0 to disable memory limit */ JS_EXTERN void JS_SetMemoryLimit(JSRuntime *rt, size_t limit); +/* Disabled by default. When enabled, configured memory-limit exhaustion + terminates execution: JavaScript cannot catch it, even if no Error object + can be allocated. Query the result with JS_GetTerminationStatus. Disabling + the option does not clear an existing termination. Free the runtime + afterwards; do not reuse it. */ +JS_EXTERN void JS_SetMemoryLimitTermination(JSRuntime *rt, bool enable); +JS_EXTERN JSTerminationStatus JS_GetTerminationStatus(JSRuntime *rt); JS_EXTERN void JS_SetDumpFlags(JSRuntime *rt, uint64_t flags); JS_EXTERN uint64_t JS_GetDumpFlags(JSRuntime *rt); JS_EXTERN size_t JS_GetGCThreshold(JSRuntime *rt); From 33590ae72c202eaea10e4c0e602b397251c0d914 Mon Sep 17 00:00:00 2001 From: yuhao Date: Wed, 30 Sep 2026 09:22:37 +0800 Subject: [PATCH 2/2] test: make memory-limit allocator portable --- api-test.c | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/api-test.c b/api-test.c index a9f1b5374..4ba076bae 100644 --- a/api-test.c +++ b/api-test.c @@ -2,7 +2,6 @@ #undef NDEBUG #endif #include -#include #include #include #include @@ -2206,8 +2205,13 @@ typedef struct { size_t live; } MemState; +/* Header size is a multiple of the strictest standard alignment, so the + pointer returned to the engine stays aligned. max_align_t is unavailable + on tcc and in some MSVC C modes. */ typedef union { - max_align_t alignment; + long double align_ld; + long long align_ll; + void *align_ptr; size_t size; } MemBlock;