From 3e1d9c80065405b9a9a9300b8a8c524d1b0bbe80 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 2 Sep 2026 16:47:11 -0700 Subject: [PATCH 1/3] Version bump --- src/njs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/njs.h b/src/njs.h index b883905a9..b33a85680 100644 --- a/src/njs.h +++ b/src/njs.h @@ -11,8 +11,8 @@ #include -#define NJS_VERSION "1.0.1" -#define NJS_VERSION_NUMBER 0x010001 +#define NJS_VERSION "1.0.2" +#define NJS_VERSION_NUMBER 0x010002 #include From 0bc05e75feb55e928bbfcd8ad35a1096b58e1351 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 2 Sep 2026 16:06:22 -0700 Subject: [PATCH 2/3] RegExp: fix Symbol.replace() with an inconsistent match object Previously, the matched string returned by exec() was assumed to be a substring of the subject at the reported index. njs_string_get_substitution() converted "index + matched length" to a byte offset without a bounds check, and the replace loop advanced the end of the last match by the byte size of the matched string instead of its character length. As a result the $' expansion read outside the subject, and on a multibyte subject the result could start in the middle of a UTF-8 sequence. The fix is to pass the head and tail slices of the subject to njs_string_get_substitution() and to track the end of the last match as a character index. The function no longer converts indexes to offsets, so the out of range conversion is prevented by construction. This is an extended fix for #1116 pull request on Github. Reported by Basavaraj S m (basavaraj@digiscrypt.com). --- src/njs_regexp.c | 47 +++++++++++++++++++------------ src/njs_string.c | 61 ++++++++++++++++++++++------------------ src/njs_string.h | 4 +-- src/test/njs_unit_test.c | 51 +++++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+), 48 deletions(-) diff --git a/src/njs_regexp.c b/src/njs_regexp.c index 6ab211239..e902dc7b3 100644 --- a/src/njs_regexp.c +++ b/src/njs_regexp.c @@ -1263,9 +1263,9 @@ 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; - const u_char *p, *next; - njs_str_t rep, m; + 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; @@ -1275,7 +1275,7 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, 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; + njs_string_prop_t s, ms; rx = njs_argument(args, 0); @@ -1390,8 +1390,7 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, } i = 0; - p = s.start; - next = p; + next = 0; while (i < results.items) { r = njs_arr_item(&results, i++); @@ -1422,6 +1421,10 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, pos = njs_max(njs_min(pos, (int64_t) length), 0); + mlength = njs_string_prop(vm, &ms, &matched); + + p = njs_string_offset(&s, pos); + ret = njs_object_length(vm, r, &ncaptures); if (njs_slow_path(ret != NJS_OK)) { goto exception; @@ -1466,9 +1469,19 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, } } - ret = njs_string_get_substitution(vm, &matched, string, pos, - arguments, ncaptures, &groups, - replace, retval); + m.start = ms.start; + m.length = ms.size; + + head.start = s.start; + head.length = p - s.start; + + start = njs_string_offset(&s, njs_min(pos + mlength, length)); + tail.start = (u_char *) start; + tail.length = (s.start + s.size) - start; + + ret = njs_string_get_substitution(vm, &m, &head, &tail, arguments, + ncaptures, &groups, replace, + retval); } else { ret = njs_array_expand(vm, array, 0, @@ -1501,17 +1514,14 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, goto exception; } - p = njs_string_offset(&s, pos); - - if (p >= next) { - njs_chb_append(&chain, next, p - next); + if (pos >= next) { + start = njs_string_offset(&s, next); + njs_chb_append(&chain, start, p - start); njs_string_get(vm, retval, &rep); njs_chb_append_str(&chain, &rep); - njs_string_get(vm, &matched, &m); - - next = p + m.length; + next = pos + mlength; } if (!func_replace && njs_object_slots(r)) { @@ -1524,8 +1534,9 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, } } - if (next < s.start + s.size) { - njs_chb_append(&chain, next, s.start + s.size - next); + if (next < length) { + start = njs_string_offset(&s, next); + njs_chb_append(&chain, start, s.start + s.size - start); } ret = njs_string_create_chb(vm, retval, &chain); diff --git a/src/njs_string.c b/src/njs_string.c index 125ec967d..272ac4e2d 100644 --- a/src/njs_string.c +++ b/src/njs_string.c @@ -2941,18 +2941,16 @@ njs_string_prototype_split(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_int_t -njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched, - njs_value_t *string, int64_t pos, njs_value_t *captures, int64_t ncaptures, +njs_string_get_substitution(njs_vm_t *vm, njs_str_t *matched, njs_str_t *head, + njs_str_t *tail, njs_value_t *captures, int64_t ncaptures, njs_value_t *groups, njs_value_t *replacement, njs_value_t *retval) { - u_char c, c2, *p, *r, *end; - size_t length; - int64_t tail, n; - njs_str_t rep, str, cap; - njs_int_t ret; - njs_chb_t chain; - njs_value_t name, value; - njs_string_prop_t s, m; + u_char c, c2, *p, *r, *end; + int64_t n; + njs_str_t rep, str, cap; + njs_int_t ret; + njs_chb_t chain; + njs_value_t name, value; njs_string_get(vm, replacement, &rep); p = rep.start; @@ -2984,26 +2982,17 @@ njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched, break; case '&': - (void) njs_string_prop(vm, &m, matched); - njs_chb_append(&chain, m.start, m.size); + njs_chb_append_str(&chain, matched); p += 2; break; case '`': - (void) njs_string_prop(vm, &s, string); - n = njs_string_offset(&s, pos) - s.start; - njs_chb_append(&chain, s.start, n); + njs_chb_append_str(&chain, head); p += 2; break; case '\'': - length = njs_string_prop(vm, &m, matched); - (void) njs_string_prop(vm, &s, string); - - tail = njs_string_offset(&s, pos + length) - s.start; - - njs_chb_append(&chain, &s.start[tail], - njs_max((int64_t) s.size - tail, 0)); + njs_chb_append_str(&chain, tail); p += 2; break; @@ -3104,7 +3093,7 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, size_t length, size, end_of_last_match; int64_t pos; njs_int_t ret; - njs_str_t str; + njs_str_t str, m, head, tail; njs_chb_t chain; njs_value_t *this, *search, *replace; njs_value_t search_lvalue, replace_lvalue, replacer, value, @@ -3198,9 +3187,20 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_OK; } + m.start = s.start; + m.length = s.size; + if (!replaceAll) { + end = njs_string_offset(&string, pos); + if (func_replace == NULL) { - ret = njs_string_get_substitution(vm, search, this, pos, NULL, 0, + head.start = string.start; + head.length = end - string.start; + + tail.start = (u_char *) end + s.size; + tail.length = (string.start + string.size) - (end + s.size); + + ret = njs_string_get_substitution(vm, &m, &head, &tail, NULL, 0, NULL, replace, &value); if (njs_slow_path(ret != NJS_OK)) { return ret; @@ -3223,8 +3223,6 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } } - end = njs_string_offset(&string, pos); - (void) njs_string_prop(vm, &ret_string, &value); size = string.size + ret_string.size - s.size; @@ -3247,8 +3245,16 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, start = string.start; do { + end = njs_string_offset(&string, pos); + if (func_replace == NULL) { - ret = njs_string_get_substitution(vm, search, this, pos, NULL, 0, + head.start = string.start; + head.length = end - string.start; + + tail.start = (u_char *) end + s.size; + tail.length = (string.start + string.size) - (end + s.size); + + ret = njs_string_get_substitution(vm, &m, &head, &tail, NULL, 0, NULL, replace, &value); if (njs_slow_path(ret != NJS_OK)) { return ret; @@ -3271,7 +3277,6 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, } } - end = njs_string_offset(&string, pos); (void) njs_string_prop(vm, &ret_string, &value); njs_chb_append(&chain, start, end - start); diff --git a/src/njs_string.h b/src/njs_string.h index ac3fd5e16..33722c6f5 100644 --- a/src/njs_string.h +++ b/src/njs_string.h @@ -174,8 +174,8 @@ njs_int_t njs_string_atob(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_int_t njs_string_prototype_concat(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, njs_index_t unused, njs_value_t *retval); -njs_int_t njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched, - njs_value_t *string, int64_t pos, njs_value_t *captures, int64_t ncaptures, +njs_int_t njs_string_get_substitution(njs_vm_t *vm, njs_str_t *matched, + njs_str_t *head, njs_str_t *tail, njs_value_t *captures, int64_t ncaptures, njs_value_t *groups, njs_value_t *replacement, njs_value_t *retval); diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 547eeef92..8ffd3a97b 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -10138,6 +10138,57 @@ static njs_unit_test_t njs_test[] = "'abc'.replaceAll(re, '@$1|$2|$3|$4|$99|$100|@')"), njs_str("@|X||Y|Z|0|@") }, + { njs_str("var n = 0, s = 'αβγ';" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'y'.repeat(100), index: 0}};" + "RegExp.prototype[Symbol.replace].call(re, s, \"[$']\")"), + njs_str("[]") }, + + { njs_str("var n = 0, s = 'αβγδε';" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'y'.repeat(1000), index: 2}};" + "RegExp.prototype[Symbol.replace].call(re, s, \"<$'>\")"), + njs_str("αβ<>") }, + + { njs_str("var n = 0, s = 'αβγ';" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'y', index: 0}};" + "RegExp.prototype[Symbol.replace].call(re, s, '')"), + njs_str("βγ") }, + + { njs_str("var n = 0, s = 'αβγ';" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'y', index: 1e9}};" + "RegExp.prototype[Symbol.replace].call(re, s, '[$`]')"), + njs_str("αβγ[αβγ]") }, + + { njs_str("var n = 0, s = 'αβγδ';" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'αβ', index: 1}};" + "RegExp.prototype[Symbol.replace].call(re, s, \"[$&|$`|$']\")"), + njs_str("α[αβ|α|δ]δ") }, + + { njs_str("var n = 0, s = 'αβγδεζ';" + "var a = [{0: 'y', index: 5}, {0: 'y', index: 0}];" + "var re = {global: true, flags: 'g'," + " exec: () => n < a.length ? a[n++] : null};" + "RegExp.prototype[Symbol.replace].call(re, s, '<$&>')"), + njs_str("αβγδε") }, + + { njs_str("var n = 0;" + "var a = [{0: 'yyyy', index: 0}, {0: 'z', index: 3}];" + "var re = {global: true, flags: 'g'," + " exec: () => n < a.length ? a[n++] : null};" + "RegExp.prototype[Symbol.replace].call(re, 'abc', '[1]')"), + njs_str("[1]") }, + + { njs_str("var n = 0;" + "var re = {global: false, flags: '', exec: () =>" + " n++ ? null : {0: 'yyyyyyyyy', index: 1}};" + "RegExp.prototype[Symbol.replace].call(re, 'abcdef'," + " \"[$&|$`|$']\")"), + njs_str("a[yyyyyyyyy|a|]") }, + { njs_str("var a = [];" "Object.defineProperty(a, 32768, {});" "var re = /any_regexp/;" From 40193e1642e2c405477835d7632f89049be382b5 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Wed, 2 Sep 2026 16:12:01 -0700 Subject: [PATCH 3/3] Memory pool: allocate the block descriptor separately with the sanitizer Previously, njs_mp_alloc_large() embedded the block descriptor at the end of the allocation for sizes which are not a power of two, so a read past the end of an object stayed inside the malloc()ed region and was not detected by the address sanitizer. The fix is to always allocate the descriptor separately when the address sanitizer is enabled. --- src/njs_mp.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/njs_mp.c b/src/njs_mp.c index 4288343e9..f7e0df1d1 100644 --- a/src/njs_mp.c +++ b/src/njs_mp.c @@ -585,6 +585,7 @@ njs_mp_alloc_large(njs_mp_t *mp, size_t alignment, size_t size) u_char *p; size_t aligned_size; uint8_t type; + njs_bool_t discrete; njs_mp_block_t *block; /* Allocation must be less than 4G. */ @@ -600,7 +601,14 @@ njs_mp_alloc_large(njs_mp_t *mp, size_t alignment, size_t size) size += size == 0; #endif - if (njs_is_power_of_two(size)) { + discrete = njs_is_power_of_two(size); + +#if (NJS_HAVE_ADDRESS_SANITIZER) + /* Keep the sanitizer redzone right after the object. */ + discrete = 1; +#endif + + if (discrete) { block = njs_malloc(sizeof(njs_mp_block_t)); if (njs_slow_path(block == NULL)) { return NULL;