From c89d153a32d90c91a8d931e3feefa7e7ba37faad Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:47:58 -0700 Subject: [PATCH 1/8] RegExp: fix cleanup of custom exec results Previously, RegExp.prototype[Symbol.replace]() and Symbol.split() used non-NULL exotic slots as proof that an exec() result was an internally allocated match array. A custom exec() could return any exotic object, which was then cast to an array and freed, corrupting unrelated VM state. The fix is to record whether RegExpBuiltinExec produced each result and to release only fresh builtin results that could not escape through user code. The replace issue was introduced in f7813172 (0.8.2), and the split issue was introduced in aa697e3e (0.8.2). This closes #1090, #1091, #1092, #1093, #1096, and #1097 issues on GitHub. --- src/njs_regexp.c | 104 +++++++++++++++++++++++++-------------- src/test/njs_unit_test.c | 16 ++++++ 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/src/njs_regexp.c b/src/njs_regexp.c index e902dc7b3..be3aa5629 100644 --- a/src/njs_regexp.c +++ b/src/njs_regexp.c @@ -27,11 +27,17 @@ static u_char *njs_regexp_match_trace_handler(njs_trace_t *trace, njs_trace_data_t *td, u_char *start); #define NJS_REGEXP_FLAG_TEST 1 static njs_int_t njs_regexp_exec(njs_vm_t *vm, njs_value_t *r, njs_value_t *s, - unsigned flags, njs_value_t *retval); + unsigned flags, njs_value_t *retval, njs_bool_t *builtin); static njs_array_t *njs_regexp_exec_result(njs_vm_t *vm, njs_value_t *r, njs_utf8_t utf8, njs_string_prop_t *string, njs_regex_match_data_t *data); +typedef struct { + njs_value_t value; + njs_bool_t builtin; +} njs_regexp_result_t; + + njs_int_t njs_regexp_init(njs_vm_t *vm) { @@ -830,7 +836,7 @@ njs_regexp_prototype_test(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - ret = njs_regexp_exec(vm, r, string, NJS_REGEXP_FLAG_TEST, &value); + ret = njs_regexp_exec(vm, r, string, NJS_REGEXP_FLAG_TEST, &value, NULL); if (njs_slow_path(ret != NJS_OK)) { return NJS_ERROR; } @@ -1206,11 +1212,17 @@ njs_regexp_prototype_exec(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, static njs_int_t njs_regexp_exec(njs_vm_t *vm, njs_value_t *r, njs_value_t *s, unsigned flags, - njs_value_t *retval) + njs_value_t *retval, njs_bool_t *builtin) { - njs_int_t ret; - njs_value_t exec; - njs_value_t arguments[2]; + njs_int_t ret; + njs_bool_t builtin_exec; + njs_value_t exec; + njs_function_t *function; + njs_value_t arguments[2]; + + if (builtin != NULL) { + *builtin = 0; + } ret = njs_value_property(vm, r, NJS_ATOM_STRING_exec, &exec); if (njs_slow_path(ret == NJS_ERROR)) { @@ -1218,14 +1230,18 @@ njs_regexp_exec(njs_vm_t *vm, njs_value_t *r, njs_value_t *s, unsigned flags, } if (njs_is_function(&exec)) { + function = njs_function(&exec); + builtin_exec = function->native + && function->u.native == njs_regexp_prototype_exec; + njs_value_assign(&arguments[0], s); if (flags) { njs_set_number(&arguments[1], flags); } - ret = njs_function_call(vm, njs_function(&exec), r, arguments, - flags ? 2 : 1, retval); + ret = njs_function_call(vm, function, r, arguments, flags ? 2 : 1, + retval); if (njs_slow_path(ret == NJS_ERROR)) { return NJS_ERROR; } @@ -1247,6 +1263,10 @@ njs_regexp_exec(njs_vm_t *vm, njs_value_t *r, njs_value_t *s, unsigned flags, return NJS_ERROR; } + if (builtin != NULL && builtin_exec && !njs_is_null(retval)) { + *builtin = 1; + } + return NJS_OK; } @@ -1255,7 +1275,13 @@ njs_regexp_exec(njs_vm_t *vm, njs_value_t *r, njs_value_t *s, unsigned flags, return NJS_ERROR; } - return njs_regexp_builtin_exec(vm, r, s, flags, retval); + ret = njs_regexp_builtin_exec(vm, r, s, flags, retval); + + if (ret == NJS_OK && builtin != NULL && !njs_is_null(retval)) { + *builtin = 1; + } + + return ret; } @@ -1263,19 +1289,20 @@ njs_int_t njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { - int64_t n, last_index, ncaptures, pos, length, next, mlength; - const u_char *p, *start; - njs_str_t rep, m, head, tail; - njs_int_t ret; - njs_arr_t results; - njs_chb_t chain; - njs_uint_t i; - njs_bool_t global; - njs_array_t *array; - njs_value_t *arguments, *r, *rx, *string, *replace; - njs_value_t s_lvalue, r_lvalue, value, matched, groups; - njs_function_t *func_replace; - njs_string_prop_t s, ms; + int64_t n, last_index, ncaptures, pos, length, next, mlength; + const u_char *p, *start; + njs_str_t rep, m, head, tail; + njs_int_t ret; + njs_arr_t results; + njs_chb_t chain; + njs_uint_t i; + njs_bool_t global; + njs_array_t *array; + njs_value_t *arguments, *r, *rx, *string, *replace; + njs_value_t s_lvalue, r_lvalue, value, matched, groups; + njs_function_t *func_replace; + njs_regexp_result_t *result; + njs_string_prop_t s, ms; rx = njs_argument(args, 0); @@ -1326,19 +1353,22 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, results.separate = 0; results.pointer = 0; - r = njs_arr_init(vm->mem_pool, &results, NULL, 4, sizeof(njs_value_t)); - if (njs_slow_path(r == NULL)) { + result = njs_arr_init(vm->mem_pool, &results, NULL, 4, + sizeof(njs_regexp_result_t)); + if (njs_slow_path(result == NULL)) { return NJS_ERROR; } for ( ;; ) { - r = njs_arr_add(&results); - if (njs_slow_path(r == NULL)) { + result = njs_arr_add(&results); + if (njs_slow_path(result == NULL)) { ret = NJS_ERROR; goto exception; } - ret = njs_regexp_exec(vm, rx, string, 0, r); + r = &result->value; + + ret = njs_regexp_exec(vm, rx, string, 0, r, &result->builtin); if (njs_slow_path(ret != NJS_OK)) { goto exception; } @@ -1393,7 +1423,8 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, next = 0; while (i < results.items) { - r = njs_arr_item(&results, i++); + result = njs_arr_item(&results, i++); + r = &result->value; if (njs_slow_path(njs_is_null(r))) { break; @@ -1524,7 +1555,7 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, next = pos + mlength; } - if (!func_replace && njs_object_slots(r)) { + if (!func_replace && result->builtin) { /* * Doing free here ONLY for non-function replace, because * otherwise we cannot be certain the result of match @@ -1565,7 +1596,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, ssize_t len; uint32_t limit; njs_int_t ret; - njs_bool_t sticky; + njs_bool_t builtin, sticky; njs_utf8_t utf8; njs_array_t *array; njs_value_t *rx, *string, *value; @@ -1664,7 +1695,8 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, length = njs_string_prop(vm, &s, string); if (njs_slow_path(s.size == 0)) { - ret = njs_regexp_exec(vm, rx, string, NJS_REGEXP_FLAG_TEST, &z); + ret = njs_regexp_exec(vm, rx, string, NJS_REGEXP_FLAG_TEST, &z, + NULL); if (njs_slow_path(ret != NJS_OK)) { return NJS_ERROR; } @@ -1692,7 +1724,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, return NJS_ERROR; } - ret = njs_regexp_exec(vm, rx, string, 0, &z); + ret = njs_regexp_exec(vm, rx, string, 0, &z, &builtin); if (njs_slow_path(ret != NJS_OK)) { return NJS_ERROR; } @@ -1715,7 +1747,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, e = njs_min(e, length); if (e == p) { - if (njs_object_slots(&z)) { + if (builtin) { njs_regexp_exec_result_free(vm, njs_array(&z)); } @@ -1740,7 +1772,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, } if (array->length == limit) { - if (njs_object_slots(&z)) { + if (builtin) { njs_regexp_exec_result_free(vm, njs_array(&z)); } @@ -1768,7 +1800,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, } if (array->length == limit) { - if (njs_object_slots(&z)) { + if (builtin) { njs_regexp_exec_result_free(vm, njs_array(&z)); } @@ -1776,7 +1808,7 @@ njs_regexp_prototype_symbol_split(njs_vm_t *vm, njs_value_t *args, } } - if (njs_object_slots(&z)) { + if (builtin) { njs_regexp_exec_result_free(vm, njs_array(&z)); } diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 4d104d949..baa4cd58d 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -10060,6 +10060,22 @@ static njs_unit_test_t njs_test[] = "r[Symbol.replace]('ABCD', 'b')"), njs_str("b") }, + { njs_str("var r = /./; r.exec = () => globalThis;" + "r[Symbol.replace]('', '') === ''"), + njs_str("true") }, + + { njs_str("var exec = RegExp.prototype.exec, saved;" + "var r = /a/; r.exec = function(s) {" + " saved = exec.call(this, s); return saved; };" + "r[Symbol.replace]('a', 'b'); saved[0]"), + njs_str("a") }, + + { njs_str("var exec = RegExp.prototype.exec, saved;" + "RegExp.prototype.exec = function(s) {" + " saved = exec.call(this, s); return saved; };" + "'a'.split(/a/); saved[0]"), + njs_str("a") }, + { njs_str("'α'.replace(/(h*)/g, '$1βγ')"), njs_str("βγαβγ") }, From 4ba02584a981bb8b5b0e3b9f78247180bbc6d7b5 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:47:58 -0700 Subject: [PATCH 2/8] Fix indexed fast paths for exotic objects Previously, Object.values() and Object.entries() erased the concrete type of an exotic receiver, while indexed property fast paths accepted any object with the fast_array bit. Typed arrays could consequently be interpreted as njs_array_t objects, causing invalid memory reads and writes. The fix is to preserve the receiver type during enumeration and to use njs_is_fast_array() for both indexed get and set fast paths. The issue was introduced in 69072164 (0.8.6); the overbroad fast path dates to 912ab387 (0.3.8). This closes #1098 issue on GitHub. --- src/njs_object.c | 2 +- src/njs_value.c | 8 ++------ src/test/njs_unit_test.c | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/njs_object.c b/src/njs_object.c index fbaf2208c..65964070a 100644 --- a/src/njs_object.c +++ b/src/njs_object.c @@ -1140,7 +1140,7 @@ njs_object_own_enumerate_object(njs_vm_t *vm, const njs_object_t *object, } entry = NULL; - njs_set_object(&value, (njs_object_t *) object); + njs_set_type_object(&value, (njs_object_t *) object, object->type); for (i = 0; i< items_sorted->length; i++) { ret = njs_value_property_val(vm, &value, &items_sorted->start[i], diff --git a/src/njs_value.c b/src/njs_value.c index 2a7f7fd57..7d77e3415 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -1075,9 +1075,7 @@ njs_value_property(njs_vm_t *vm, njs_value_t *value, uint32_t atom_id, return NJS_OK; } - if (njs_slow_path(!(njs_is_object(value) - && njs_object(value)->fast_array))) - { + if (njs_slow_path(!njs_is_fast_array(value))) { goto slow_path; } @@ -1194,9 +1192,7 @@ njs_value_property_set(njs_vm_t *vm, njs_value_t *value, uint32_t atom_id, return NJS_OK; } - if (njs_slow_path(!(njs_is_object(value) - && njs_object(value)->fast_array))) - { + if (njs_slow_path(!njs_is_fast_array(value))) { goto slow_path; } diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index baa4cd58d..8ce6c53f2 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -6996,6 +6996,24 @@ static njs_unit_test_t njs_test[] = " try {a.set(init,Infinity)} catch (e) {return e.name == 'RangeError'};})"), njs_str("true") }, + { njs_str("var a = new Uint8Array([1]);" + "Object.defineProperty(a, 'x', {enumerable: true," + " get() { return this === a && this[0]; }});" + "Object.values(a)"), + njs_str("1,1") }, + + { njs_str("var a = new Uint8Array([1]);" + "Object.defineProperty(a, 'x', {enumerable: true," + " get() { this[0] = 2; return this[0]; }});" + "Object.values(a); a[0]"), + njs_str("2") }, + + { njs_str("var a = Buffer.from([1]);" + "Object.defineProperty(a, 'x', {enumerable: true," + " get() { return this === a && this[0]; }});" + "Object.values(a)"), + njs_str("1,1") }, + { njs_str(NJS_INT_TYPED_ARRAY_LIST ".map(v=>{try { var a = new v(1); $262.detachArrayBuffer(a.buffer); Object.entries(a)} " "catch (e) {return e.name}}).every(v=>v === 'TypeError')"), From 009ff594edee6cb042b936dc31e89d938e23fcf8 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:47:58 -0700 Subject: [PATCH 3/8] Fix Array.prototype.concat() with sparse arrays Previously, the fast concat path represented a missing source property with njs_value_invalid and still created the corresponding destination property. For a slow sparse destination this materialized an internal hole sentinel as a normal property, which later property access could interpret as an invalid accessor. The fix is to advance the destination index without creating a property when the source lookup reports a hole. The issue was introduced in 232fb594 (0.7.2). This closes #1095 issue on GitHub. --- src/njs_array.c | 2 +- src/test/njs_unit_test.c | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/njs_array.c b/src/njs_array.c index 5c30e833d..a9ea5143f 100644 --- a/src/njs_array.c +++ b/src/njs_array.c @@ -1961,7 +1961,7 @@ njs_array_prototype_concat(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_ERROR; } - njs_set_invalid(&value); + continue; } ret = njs_value_property_i64_set(vm, &this, length, diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 8ce6c53f2..3fc869088 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -5193,6 +5193,11 @@ static njs_unit_test_t njs_test[] = "njs.dump(a2)"), njs_str("[3,1,<1111110 empty items>,2,4]") }, + { njs_str("var a = []; a[100000] = 1;" + "var b = [].concat(a, [,, 5]);" + "[b.length, Object.keys(b), b[100001], b.sort().length]"), + njs_str("100004,100000,100003,,100004") }, + { njs_str("var re = /abc/; re[Symbol.isConcatSpreadable] = true;" "re[0] = 1, re[1] = 2, re[2] = 3, re.length = 3;" "[].concat(re)"), From e38b8e7f4a77c7c6ba68e6ce4fe91997ff25645a Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:48:11 -0700 Subject: [PATCH 4/8] Fix Object.defineProperty() with reentrant descriptors Previously, njs_object_prop_define() retained a pointer to a target hash property while descriptor getters executed user code. Reentrant mutation of the target could resize the flat hash and leave the retained pointer dangling. Descriptor getters were also evaluated again after fast-array conversion. The fix is to convert the descriptor before querying the target and to place the retry point after descriptor conversion. The issue was introduced in 2f288a9c (0.9.1). This closes #1089 issue on GitHub. --- src/njs_object_prop.c | 27 ++++++++++++++------------- src/test/njs_unit_test.c | 12 ++++++++++++ 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/njs_object_prop.c b/src/njs_object_prop.c index aeaeb71ba..aaf8b683f 100644 --- a/src/njs_object_prop.c +++ b/src/njs_object_prop.c @@ -139,24 +139,13 @@ njs_object_prop_define(njs_vm_t *vm, njs_value_t *object, unsigned atom_id, njs_array_t *array; njs_value_t key, retval; njs_object_prop_t _prop; - njs_object_prop_t *prop = &_prop, *prev, *obj_prop; + njs_object_prop_t *prop, *prev, *obj_prop; njs_property_query_t pq; -again: - set_enumerable = 1; set_configurable = 1; set_writable = 1; - - njs_property_query_init(&pq, NJS_PROPERTY_QUERY_SET, 1); - - ret = (flags & NJS_OBJECT_PROP_CREATE) - ? NJS_DECLINED - : njs_property_query(vm, &pq, object, atom_id); - - if (njs_slow_path(ret == NJS_ERROR)) { - return ret; - } + prop = &_prop; switch (njs_prop_type(flags)) { case NJS_OBJECT_PROP_DESCRIPTOR: @@ -208,6 +197,18 @@ njs_object_prop_define(njs_vm_t *vm, njs_value_t *object, unsigned atom_id, break; } +again: + + njs_property_query_init(&pq, NJS_PROPERTY_QUERY_SET, 1); + + ret = (flags & NJS_OBJECT_PROP_CREATE) + ? NJS_DECLINED + : njs_property_query(vm, &pq, object, atom_id); + + if (njs_slow_path(ret == NJS_ERROR)) { + return ret; + } + if (njs_fast_path(ret == NJS_DECLINED)) { set_prop: diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 3fc869088..9d5d8b6f4 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -16002,6 +16002,18 @@ static njs_unit_test_t njs_test[] = "njs.dump(arr)"), njs_str("[1,'[Getter]']") }, + { njs_str("var a = [1], n = 0;" + "Object.defineProperty(a, 0, {" + " get get() { n++; a.unshift(0); return function() {}; }});" + "n"), + njs_str("1") }, + + { njs_str("var a = [1], n = 0;" + "Object.defineProperty(a, 'length', {" + " get writable() { n++; return false; }});" + "n"), + njs_str("1") }, + { njs_str("Object.defineProperties()"), njs_str("TypeError: Object.defineProperties is called on non-object") }, From fef2daa9f61d157fc838bc6417f7e104a159a1df Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:48:11 -0700 Subject: [PATCH 5/8] Regex: fix decoding of named capture indexes Previously, PCRE name-table capture indexes were decoded through plain char. On signed-char targets, bytes with the high bit set were sign-extended and produced a wild or incorrect capture index used to read a match array. The fix is to decode unsigned bytes through a checked output parameter and to validate every named capture index against the compiled capture count. The issue was introduced in 9496944a (0.3.2). This closes #1088 issue on GitHub. --- external/njs_regex.c | 12 +++++++----- src/njs_regex.h | 6 +++--- src/njs_regexp.c | 12 ++++++++++-- src/test/njs_unit_test.c | 8 ++++++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/external/njs_regex.c b/external/njs_regex.c index cd45afc05..3b4e33391 100644 --- a/external/njs_regex.c +++ b/external/njs_regex.c @@ -573,9 +573,10 @@ njs_regex_is_valid(njs_regex_t *regex) njs_int_t -njs_regex_named_captures(njs_regex_t *regex, njs_str_t *name, int n) +njs_regex_named_capture(njs_regex_t *regex, njs_str_t *name, int n, + uint32_t *capture) { - char *entry; + u_char *entry; if (name == NULL) { return regex->nentries; @@ -587,10 +588,12 @@ njs_regex_named_captures(njs_regex_t *regex, njs_str_t *name, int n) entry = regex->entries + regex->entry_size * n; - name->start = (u_char *) entry + 2; + name->start = entry + 2; name->length = njs_strlen(name->start); - return (entry[0] << 8) + entry[1]; + *capture = (entry[0] << 8) + entry[1]; + + return NJS_OK; } @@ -744,4 +747,3 @@ njs_pcre_free(void *p) #endif - diff --git a/src/njs_regex.h b/src/njs_regex.h index 5cf09cf28..0039ba1fb 100644 --- a/src/njs_regex.h +++ b/src/njs_regex.h @@ -32,7 +32,7 @@ typedef struct { int backrefmax; int nentries; int entry_size; - char *entries; + u_char *entries; } njs_regex_t; @@ -76,8 +76,8 @@ NJS_EXPORT njs_int_t njs_regex_compile(njs_regex_t *regex, u_char *source, size_t len, njs_regex_flags_t flags, njs_regex_compile_ctx_t *ctx, njs_trace_t *trace); NJS_EXPORT njs_bool_t njs_regex_is_valid(njs_regex_t *regex); -NJS_EXPORT njs_int_t njs_regex_named_captures(njs_regex_t *regex, - njs_str_t *name, int n); +NJS_EXPORT njs_int_t njs_regex_named_capture(njs_regex_t *regex, + njs_str_t *name, int n, uint32_t *capture); NJS_EXPORT njs_regex_match_data_t *njs_regex_match_data(njs_regex_t *regex, njs_regex_generic_ctx_t *ctx); NJS_EXPORT void njs_regex_match_data_free(njs_regex_match_data_t *match_data, diff --git a/src/njs_regexp.c b/src/njs_regexp.c index be3aa5629..3506451cd 100644 --- a/src/njs_regexp.c +++ b/src/njs_regexp.c @@ -383,7 +383,7 @@ njs_regexp_pattern_create(njs_vm_t *vm, u_char *start, size_t length, goto fail; } - pattern->ngroups = njs_regex_named_captures(regex, NULL, 0); + pattern->ngroups = njs_regex_named_capture(regex, NULL, 0, NULL); if (pattern->ngroups != 0) { size = sizeof(njs_regexp_group_t) * pattern->ngroups; @@ -399,7 +399,15 @@ njs_regexp_pattern_create(njs_vm_t *vm, u_char *start, size_t length, do { group = &pattern->groups[n]; - group->capture = njs_regex_named_captures(regex, &group->name, n); + ret = njs_regex_named_capture(regex, &group->name, n, + &group->capture); + if (njs_slow_path(ret != NJS_OK + || group->capture >= pattern->ncaptures)) + { + njs_internal_error(vm, "invalid named capture index"); + goto fail; + } + group->hash = njs_djb_hash(group->name.start, group->name.length); n++; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 9d5d8b6f4..2f4b080ef 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -10188,6 +10188,14 @@ static njs_unit_test_t njs_test[] = { njs_str("'ABC'.replace(/(?B)/, '|$@')"), njs_str("A|@C") }, + { njs_str("var s = '(?:' + '()'.repeat(127) + '){0}(?a)';" + "new RegExp(s).exec('a').groups.x"), + njs_str("a") }, + + { njs_str("var s = '(?:' + '()'.repeat(383) + '){0}(?a)';" + "new RegExp(s).exec('a').groups.x"), + njs_str("a") }, + { njs_str("'ABCB'.replaceAll(/(?B)/g, '|$@')"), njs_str("A|@C|@") }, From 8ac117721eb98d496e02332bc72dda56ec4555bb Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:48:39 -0700 Subject: [PATCH 6/8] TypedArray: fix numeric property definitions Previously, integer-indexed exotic validation was restricted to keys marked as strings. Numeric atoms and the canonical string "NaN" could bypass the check and be installed as ordinary typed-array properties. The fix is to validate numeric atoms and canonical numeric strings after key conversion, and to remove the obsolete source-type flag. The numeric-key bypass was introduced in b28e50b1 (0.9.0); the incomplete canonical-index handling dates to bf208048 (0.3.8). This closes #1098 issue on GitHub. --- src/njs_object.h | 5 ----- src/njs_object_prop.c | 35 +++++++++++++++++++++++++++++------ src/test/njs_unit_test.c | 11 +++++++++++ 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/njs_object.h b/src/njs_object.h index 814964aa9..319de1dbf 100644 --- a/src/njs_object.h +++ b/src/njs_object.h @@ -19,7 +19,6 @@ typedef enum { NJS_OBJECT_PROP_CONFIGURABLE = 16, NJS_OBJECT_PROP_WRITABLE = 32, NJS_OBJECT_PROP_UNSET = 64, - NJS_OBJECT_PROP_IS_STRING = 128, #define NJS_OBJECT_PROP_VALUE_ECW (NJS_OBJECT_PROP_VALUE \ | NJS_OBJECT_PROP_ENUMERABLE \ | NJS_OBJECT_PROP_CONFIGURABLE \ @@ -264,10 +263,6 @@ njs_object_prop_define_val(njs_vm_t *vm, njs_value_t *object, njs_value_t *name, } } - if (njs_is_string(name)) { - flags |= NJS_OBJECT_PROP_IS_STRING; - } - return njs_object_prop_define(vm, object, name->atom_id, value, flags); } diff --git a/src/njs_object_prop.c b/src/njs_object_prop.c index aaf8b683f..6b3181d56 100644 --- a/src/njs_object_prop.c +++ b/src/njs_object_prop.c @@ -136,6 +136,7 @@ njs_object_prop_define(njs_vm_t *vm, njs_value_t *object, unsigned atom_id, uint32_t length, index, set_enumerable, set_configurable, set_writable; njs_int_t ret; + njs_str_t string; njs_array_t *array; njs_value_t key, retval; njs_object_prop_t _prop; @@ -220,22 +221,38 @@ njs_object_prop_define(njs_vm_t *vm, njs_value_t *object, unsigned atom_id, return NJS_ERROR; } - if (njs_slow_path(njs_is_typed_array(object) && - (flags & NJS_OBJECT_PROP_IS_STRING))) - { + if (njs_slow_path(njs_is_typed_array(object))) { /* Integer-Indexed Exotic Objects [[DefineOwnProperty]]. */ + if (njs_atom_is_number(atom_id)) { + index = njs_atom_number(atom_id); + + if (index < njs_typed_array_length(njs_typed_array(object))) { + goto complete; + } + + goto invalid_index; + } + ret = njs_atom_to_value(vm, &key, atom_id); if (njs_slow_path(ret != NJS_OK)) { return ret; } - if (!isnan(njs_string_to_index(&key))) { - njs_type_error(vm, "Invalid typed array index"); - return NJS_ERROR; + if (njs_is_string(&key)) { + njs_string_get(vm, &key, &string); + + if (!isnan(njs_string_to_index(&key)) + || (string.length == 3 + && memcmp(string.start, "NaN", 3) == 0)) + { + goto invalid_index; + } } } +complete: + /* 6.2.5.6 CompletePropertyDescriptor */ if (njs_is_accessor_descriptor(prop)) { @@ -559,6 +576,12 @@ njs_object_prop_define(njs_vm_t *vm, njs_value_t *object, unsigned atom_id, njs_atom_string_get(vm, atom_id, &pq.fhq.key); njs_type_error(vm, "Cannot redefine property: \"%V\"", &pq.fhq.key); + return NJS_ERROR; + +invalid_index: + + njs_type_error(vm, "Invalid typed array index"); + return NJS_ERROR; } diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 2f4b080ef..d19018251 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -7013,6 +7013,17 @@ static njs_unit_test_t njs_test[] = "Object.values(a); a[0]"), njs_str("2") }, + { njs_str("[257, '257', NaN, 'NaN', Infinity, 'Infinity', -0, '-0']" + ".every(k => { try {" + " Object.defineProperty(new Uint8Array(), k, {value: 1});" + "} catch (e) { return e.name == 'TypeError'; } })"), + njs_str("true") }, + + { njs_str("['01', ' 1', '0x10', '1e3'].every(k => {" + "var a = new Uint8Array();" + "Object.defineProperty(a, k, {value: 1}); return a[k] == 1; })"), + njs_str("true") }, + { njs_str("var a = Buffer.from([1]);" "Object.defineProperty(a, 'x', {enumerable: true," " get() { return this === a && this[0]; }});" From bc56f3661f8eeea66649660a7c665f7b4d186540 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:48:39 -0700 Subject: [PATCH 7/8] Fix reentrant object-to-primitive conversion Previously, njs_value_to_primitive() retained a pointer to the caller's value slot while valueOf(), toString(), or property getters executed user code. Reentrant code could overwrite that slot, after which the conversion treated the replacement primitive as the original object. The fix is to snapshot the input value before the first observable property lookup and use the stable copy throughout conversion. The issue was introduced in 64dbf104 (0.3.4). This closes #1094 issue on GitHub. --- src/njs_value.c | 5 ++++- src/test/njs_unit_test.c | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/njs_value.c b/src/njs_value.c index 7d77e3415..ae2f206aa 100644 --- a/src/njs_value.c +++ b/src/njs_value.c @@ -37,7 +37,7 @@ njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value, { njs_int_t ret; njs_uint_t tries, force_ordinary; - njs_value_t method, retval, arguments[2]; + njs_value_t input, method, retval, arguments[2]; njs_flathsh_query_t fhq; static const uint32_t atoms[] = { @@ -56,6 +56,9 @@ njs_value_to_primitive(njs_vm_t *vm, njs_value_t *dst, njs_value_t *value, return NJS_OK; } + input = *value; + value = &input; + tries = 0; fhq.proto = &njs_object_hash_proto; diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index d19018251..d7137c569 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -14367,6 +14367,15 @@ static njs_unit_test_t njs_test[] = { njs_str("false.__proto__ === Boolean.prototype"), njs_str("true") }, + { njs_str("var o = {valueOf() { o = 0; return {}; }," + " toString() { return '7'; }}; +o"), + njs_str("7") }, + + { njs_str("var original = {toString() { return '7'; }}, o = original;" + "Object.defineProperty(original, 'valueOf', {get() {" + " o = 0; return () => ({}); }}); +o"), + njs_str("7") }, + { njs_str("var b = Boolean(1); b.__proto__ === Boolean.prototype"), njs_str("true") }, From 17ecb8b9ae63dbda5d91c7a240058c38959dcc14 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 3 Sep 2026 18:48:39 -0700 Subject: [PATCH 8/8] Fix Array.prototype.sort() with huge sparse arrays Previously, sort deleted every index from the compacted output position to the original length. A sparse array with length near 2^32 therefore caused billions of property-delete operations and unbounded memory growth. The fix is to retain the direct cleanup loop for fast arrays and, for slow arrays, enumerate and delete only existing indexed properties in the range. The issue was introduced in 1a2a65f7 (0.8.0). This closes #1095 issue on GitHub. --- src/njs_array.c | 33 ++++++++++++++++++++++++++++++--- src/test/njs_unit_test.c | 4 ++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/njs_array.c b/src/njs_array.c index a9ea5143f..aea9c32b6 100644 --- a/src/njs_array.c +++ b/src/njs_array.c @@ -2917,8 +2917,11 @@ static njs_int_t njs_array_prototype_sort(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval) { + double idx; int64_t i, nslots, nunds, length; + uint32_t n; njs_int_t ret; + njs_array_t *keys; njs_value_t *this, *comparefn; njs_function_t *compare; njs_array_sort_slot_t *slots; @@ -2982,11 +2985,35 @@ njs_array_prototype_sort(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } } - for (; i < length; i++) { - ret = njs_value_property_i64_delete(vm, this, i, NULL); - if (njs_slow_path(ret == NJS_ERROR)) { + if (njs_is_fast_array(this)) { + for (; i < length; i++) { + ret = njs_value_property_i64_delete(vm, this, i, NULL); + if (njs_slow_path(ret == NJS_ERROR)) { + goto exception; + } + } + + } else { + keys = njs_array_indices(vm, this); + if (njs_slow_path(keys == NULL)) { goto exception; } + + for (n = 0; n < keys->length; n++) { + idx = njs_string_to_index(&keys->start[n]); + + if (idx >= i && idx < length) { + ret = njs_value_property_delete(vm, this, + keys->start[n].atom_id, + NULL, 1); + if (njs_slow_path(ret == NJS_ERROR)) { + njs_array_destroy(vm, keys); + goto exception; + } + } + } + + njs_array_destroy(vm, keys); } done: diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index d7137c569..3c01172d8 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -5193,6 +5193,10 @@ static njs_unit_test_t njs_test[] = "njs.dump(a2)"), njs_str("[3,1,<1111110 empty items>,2,4]") }, + { njs_str("var a = []; a[2**32 - 7] = 2; a[2**32 - 2] = 1;" + "a.sort(); [a.length, a[0], a[1], Object.keys(a)]"), + njs_str("4294967295,1,2,0,1") }, + { njs_str("var a = []; a[100000] = 1;" "var b = [].concat(a, [,, 5]);" "[b.length, Object.keys(b), b[100001], b.sort().length]"),