From 81b5b70e408ab94bb6d6780751d2f7f675ea2e4d Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Thu, 13 Mar 2025 16:04:38 +0100 Subject: [PATCH 1/5] JS_SetPropertyInternal(): avoid recursing thru the prototypes if the property is found in a prototype --- quickjs.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/quickjs.c b/quickjs.c index ba4cf1ad0..a93109ee0 100644 --- a/quickjs.c +++ b/quickjs.c @@ -8930,6 +8930,8 @@ static int JS_SetPropertyInternal2(JSContext *ctx, JSValueConst obj, JSAtom prop goto retry2; } else if (!(prs->flags & JS_PROP_WRITABLE)) { goto read_only_prop; + } else { + break; } } } From e20a8d88b31265dcc224c17365c82ef19ecedefa Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Thu, 13 Mar 2025 17:27:38 +0100 Subject: [PATCH 2/5] added missing stack overflow check in JSON.stringify() --- quickjs.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/quickjs.c b/quickjs.c index a93109ee0..c571d52c7 100644 --- a/quickjs.c +++ b/quickjs.c @@ -45373,6 +45373,11 @@ static int js_json_to_str(JSContext *ctx, JSONStringifyContext *jsc, tab = JS_UNDEFINED; prop = JS_UNDEFINED; + if (js_check_stack_overflow(ctx->rt, 0)) { + JS_ThrowStackOverflow(ctx); + goto exception; + } + if (JS_IsObject(val)) { p = JS_VALUE_GET_OBJ(val); cl = p->class_id; From 0fd31d488935946d379b284048d6d02d71b958cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Fri, 21 Mar 2025 22:38:01 +0100 Subject: [PATCH 3/5] Revert "Simplify close_lexical_var (#726)" This reverts commit 0b0b7946052b3e96d0634403733263a243f5440a. This makes our code closer to bellard/quickjs, which makes applying the commit right after this one simpler. --- quickjs.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/quickjs.c b/quickjs.c index c571d52c7..3fb6d47cd 100644 --- a/quickjs.c +++ b/quickjs.c @@ -14705,14 +14705,15 @@ static void close_var_refs(JSRuntime *rt, JSStackFrame *sf) } } -static void close_lexical_var(JSContext *ctx, JSStackFrame *sf, int var_idx) +static void close_lexical_var(JSContext *ctx, JSStackFrame *sf, int idx, int is_arg) { struct list_head *el, *el1; JSVarRef *var_ref; + int var_idx = idx; list_for_each_safe(el, el1, &sf->var_ref_list) { var_ref = list_entry(el, JSVarRef, header.link); - if (var_idx == var_ref->var_idx && !var_ref->is_arg) { + if (var_idx == var_ref->var_idx && var_ref->is_arg == is_arg) { var_ref->value = js_dup(sf->var_buf[var_idx]); var_ref->pvalue = &var_ref->value; list_del(&var_ref->header.link); @@ -15975,7 +15976,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, int idx; idx = get_u16(pc); pc += 2; - close_lexical_var(ctx, sf, idx); + close_lexical_var(ctx, sf, idx, FALSE); } BREAK; From 16e21f4c97121197ab135b077e80df7e2c9582e4 Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Fri, 21 Mar 2025 22:50:30 +0100 Subject: [PATCH 4/5] simplified the handling of closures --- quickjs.c | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/quickjs.c b/quickjs.c index 3fb6d47cd..60a363202 100644 --- a/quickjs.c +++ b/quickjs.c @@ -364,15 +364,7 @@ typedef struct JSVarRef { struct { int __gc_ref_count; /* corresponds to header.ref_count */ uint8_t __gc_mark; /* corresponds to header.mark/gc_obj_type */ - - /* 0 : the JSVarRef is on the stack. header.link is an element - of JSStackFrame.var_ref_list. - 1 : the JSVarRef is detached. header.link has the normal meanning - */ - uint8_t is_detached : 1; - uint8_t is_arg : 1; - uint16_t var_idx; /* index of the corresponding function variable on - the stack */ + bool is_detached; }; }; JSValue *pvalue; /* pointer to the value, either on the stack or @@ -14440,10 +14432,16 @@ static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, { JSVarRef *var_ref; struct list_head *el; + JSValue *pvalue; + + if (is_arg) + pvalue = &sf->arg_buf[var_idx]; + else + pvalue = &sf->var_buf[var_idx]; list_for_each(el, &sf->var_ref_list) { var_ref = list_entry(el, JSVarRef, header.link); - if (var_ref->var_idx == var_idx && var_ref->is_arg == is_arg) { + if (var_ref->pvalue == pvalue) { var_ref->header.ref_count++; return var_ref; } @@ -14454,13 +14452,8 @@ static JSVarRef *get_var_ref(JSContext *ctx, JSStackFrame *sf, return NULL; var_ref->header.ref_count = 1; var_ref->is_detached = false; - var_ref->is_arg = is_arg; - var_ref->var_idx = var_idx; list_add_tail(&var_ref->header.link, &sf->var_ref_list); - if (is_arg) - var_ref->pvalue = &sf->arg_buf[var_idx]; - else - var_ref->pvalue = &sf->var_buf[var_idx]; + var_ref->pvalue = pvalue; var_ref->value = JS_UNDEFINED; return var_ref; } @@ -14689,15 +14682,10 @@ static void close_var_refs(JSRuntime *rt, JSStackFrame *sf) { struct list_head *el, *el1; JSVarRef *var_ref; - int var_idx; list_for_each_safe(el, el1, &sf->var_ref_list) { var_ref = list_entry(el, JSVarRef, header.link); - var_idx = var_ref->var_idx; - if (var_ref->is_arg) - var_ref->value = js_dup(sf->arg_buf[var_idx]); - else - var_ref->value = js_dup(sf->var_buf[var_idx]); + var_ref->value = js_dup(*var_ref->pvalue); var_ref->pvalue = &var_ref->value; /* the reference is no longer to a local variable */ var_ref->is_detached = true; @@ -14705,16 +14693,17 @@ static void close_var_refs(JSRuntime *rt, JSStackFrame *sf) } } -static void close_lexical_var(JSContext *ctx, JSStackFrame *sf, int idx, int is_arg) +static void close_lexical_var(JSContext *ctx, JSStackFrame *sf, int var_idx) { + JSValue *pvalue; struct list_head *el, *el1; JSVarRef *var_ref; - int var_idx = idx; + pvalue = &sf->var_buf[var_idx]; list_for_each_safe(el, el1, &sf->var_ref_list) { var_ref = list_entry(el, JSVarRef, header.link); - if (var_idx == var_ref->var_idx && var_ref->is_arg == is_arg) { - var_ref->value = js_dup(sf->var_buf[var_idx]); + if (var_ref->pvalue == pvalue) { + var_ref->value = js_dup(*var_ref->pvalue); var_ref->pvalue = &var_ref->value; list_del(&var_ref->header.link); /* the reference is no longer to a local variable */ @@ -15976,7 +15965,7 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj, int idx; idx = get_u16(pc); pc += 2; - close_lexical_var(ctx, sf, idx, FALSE); + close_lexical_var(ctx, sf, idx); } BREAK; From a0989dab0b44ad44f19c8e2e1a4d403d1f79bea2 Mon Sep 17 00:00:00 2001 From: Fabrice Bellard Date: Fri, 21 Mar 2025 23:02:14 +0100 Subject: [PATCH 5/5] allow regexp interruption (e.g. with Ctrl-C in the REPL) --- libregexp.c | 36 ++++++++++++++++++++++++++++++------ libregexp.h | 5 +++++ quickjs.c | 30 +++++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 11 deletions(-) diff --git a/libregexp.c b/libregexp.c index 04c2888a8..26d96edcf 100644 --- a/libregexp.c +++ b/libregexp.c @@ -53,6 +53,9 @@ typedef enum { #define CAPTURE_COUNT_MAX 255 #define STACK_SIZE_MAX 255 +/* must be large enough to have a negligible runtime cost and small + enough to call the interrupt callback often. */ +#define INTERRUPT_COUNTER_INIT 10000 /* unicode code points */ #define CP_LS 0x2028 @@ -1992,6 +1995,7 @@ typedef struct { bool multi_line; bool ignore_case; bool is_unicode; + int interrupt_counter; void *opaque; /* used for stack overflow check */ size_t state_size; @@ -2038,7 +2042,17 @@ static int push_state(REExecContext *s, return 0; } -/* return 1 if match, 0 if not match or -1 if error. */ +static int lre_poll_timeout(REExecContext *s) +{ + if (unlikely(--s->interrupt_counter <= 0)) { + s->interrupt_counter = INTERRUPT_COUNTER_INIT; + if (lre_check_timeout(s->opaque)) + return LRE_RET_TIMEOUT; + } + return 0; +} + +/* return 1 if match, 0 if not match or < 0 if error. */ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, StackInt *stack, int stack_len, const uint8_t *pc, const uint8_t *cptr, @@ -2069,6 +2083,8 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, ret = 0; recurse: for(;;) { + if (lre_poll_timeout(s)) + return LRE_RET_TIMEOUT; if (s->state_stack_len == 0) return ret; rs = (REExecState *)(s->state_stack + @@ -2162,7 +2178,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, ret = push_state(s, capture, stack, stack_len, pc1, cptr, RE_EXEC_STATE_SPLIT, 0); if (ret < 0) - return -1; + return LRE_RET_MEMORY_ERROR; break; } case REOP_lookahead: @@ -2174,12 +2190,14 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, RE_EXEC_STATE_LOOKAHEAD + opcode - REOP_lookahead, 0); if (ret < 0) - return -1; + return LRE_RET_MEMORY_ERROR; break; case REOP_goto: val = get_u32(pc); pc += 4 + (int)val; + if (lre_poll_timeout(s)) + return LRE_RET_TIMEOUT; break; case REOP_line_start: if (cptr == s->cbuf) @@ -2244,6 +2262,8 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, pc += 4; if (--stack[stack_len - 1] != 0) { pc += (int)val; + if (lre_poll_timeout(s)) + return LRE_RET_TIMEOUT; } break; case REOP_push_char_pos: @@ -2418,9 +2438,12 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, q = 0; for(;;) { + if (lre_poll_timeout(s)) + return LRE_RET_TIMEOUT; res = lre_exec_backtrack(s, capture, stack, stack_len, pc1, cptr, true); - if (res == -1) + if (res == LRE_RET_MEMORY_ERROR || + res == LRE_RET_TIMEOUT) return res; if (!res) break; @@ -2438,7 +2461,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, RE_EXEC_STATE_GREEDY_QUANT, q - quant_min); if (ret < 0) - return -1; + return LRE_RET_MEMORY_ERROR; } } break; @@ -2448,7 +2471,7 @@ static intptr_t lre_exec_backtrack(REExecContext *s, uint8_t **capture, } } -/* Return 1 if match, 0 if not match or -1 if error. cindex is the +/* Return 1 if match, 0 if not match or < 0 if error (see LRE_RET_x). cindex is the starting position of the match and must be such as 0 <= cindex <= clen. */ int lre_exec(uint8_t **capture, @@ -2470,6 +2493,7 @@ int lre_exec(uint8_t **capture, s->cbuf_type = cbuf_type; if (s->cbuf_type == 1 && s->is_unicode) s->cbuf_type = 2; + s->interrupt_counter = INTERRUPT_COUNTER_INIT; s->opaque = opaque; s->state_size = sizeof(REExecState) + diff --git a/libregexp.h b/libregexp.h index 0b8fec52b..898e9a7a3 100644 --- a/libregexp.h +++ b/libregexp.h @@ -43,6 +43,9 @@ extern "C" { #define LRE_FLAG_NAMED_GROUPS (1 << 7) /* named groups are present in the regexp */ #define LRE_FLAG_UNICODE_SETS (1 << 8) +#define LRE_RET_MEMORY_ERROR (-1) +#define LRE_RET_TIMEOUT (-2) + uint8_t *lre_compile(int *plen, char *error_msg, int error_msg_size, const char *buf, size_t buf_len, int re_flags, void *opaque); @@ -60,6 +63,8 @@ void lre_byte_swap(uint8_t *buf, size_t len, bool is_byte_swapped); /* must be provided by the user */ bool lre_check_stack_overflow(void *opaque, size_t alloca_size); +/* must be provided by the user, return non zero if time out */ +int lre_check_timeout(void *opaque); void *lre_realloc(void *opaque, void *ptr, size_t size); /* JS identifier test */ diff --git a/quickjs.c b/quickjs.c index 60a363202..523237b79 100644 --- a/quickjs.c +++ b/quickjs.c @@ -7111,15 +7111,19 @@ static JSValue JS_ThrowTypeErrorInvalidClass(JSContext *ctx, int class_id) return JS_ThrowTypeErrorAtom(ctx, "%s object expected", name); } +static void JS_ThrowInterrupted(JSContext *ctx) +{ + JS_ThrowInternalError(ctx, "interrupted"); + JS_SetUncatchableError(ctx, ctx->rt->current_exception); +} + static no_inline __exception int __js_poll_interrupts(JSContext *ctx) { JSRuntime *rt = ctx->rt; ctx->interrupt_counter = JS_INTERRUPT_COUNTER_INIT; if (rt->interrupt_handler) { if (rt->interrupt_handler(rt, rt->interrupt_opaque)) { - /* XXX: should set a specific flag to avoid catching */ - JS_ThrowInternalError(ctx, "interrupted"); - js_set_uncatchable_error(ctx, ctx->rt->current_exception, true); + JS_ThrowInterrupted(ctx); return -1; } } @@ -43878,6 +43882,14 @@ bool lre_check_stack_overflow(void *opaque, size_t alloca_size) return js_check_stack_overflow(ctx->rt, alloca_size); } +int lre_check_timeout(void *opaque) +{ + JSContext *ctx = opaque; + JSRuntime *rt = ctx->rt; + return (rt->interrupt_handler && + rt->interrupt_handler(rt, rt->interrupt_opaque)); +} + void *lre_realloc(void *opaque, void *ptr, size_t size) { JSContext *ctx = opaque; @@ -43992,7 +44004,11 @@ static JSValue js_regexp_exec(JSContext *ctx, JSValueConst this_val, goto fail; } } else { - JS_ThrowInternalError(ctx, "out of memory in regexp execution"); + if (rc == LRE_RET_TIMEOUT) { + JS_ThrowInterrupted(ctx); + } else { + JS_ThrowInternalError(ctx, "out of memory in regexp execution"); + } goto fail; } } else { @@ -44187,7 +44203,11 @@ static JSValue JS_RegExpDelete(JSContext *ctx, JSValueConst this_val, JSValue ar goto fail; } } else { - JS_ThrowInternalError(ctx, "out of memory in regexp execution"); + if (ret == LRE_RET_TIMEOUT) { + JS_ThrowInterrupted(ctx); + } else { + JS_ThrowInternalError(ctx, "out of memory in regexp execution"); + } goto fail; } break;