From: Dmitry Volyntsev Date: Tue, 21 Apr 2026 04:32:19 +0000 (-0700) Subject: Cap string-producing chb chains in core builtins X-Git-Tag: 1.0.0~20 X-Git-Url: http://git.kaiwu.me/postgresql/log/contrib/postgres_fdw/postgres_fdw.c?a=commitdiff_plain;h=c1635a7c11f1d6b5542f855f7f01148e656dba97;p=njs.git Cap string-producing chb chains in core builtins Overflow now trips during chain growth and surfaces as a catchable RangeError("invalid string length") instead of continuing to inflate the VM memory pool until the process OOMs. Out of scope (non-JS-string chains): - njs_vm.c AST serialization - njs_json.c njs_vm_value_dump (console.log value dump) - njs_function.c new Function() source assembly - njs_error.c error message composition - external/ modules on both engines (querystring, xml, zlib) This fixes OOM in staging/sm/String/replace-math Tests262. --- diff --git a/src/njs_array.c b/src/njs_array.c index eb700aa3..e91e6b22 100644 --- a/src/njs_array.c +++ b/src/njs_array.c @@ -1695,7 +1695,7 @@ njs_array_prototype_join(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, value = &entry; - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(vm), NJS_STRING_MAX_LENGTH); for (i = 0; i < len; i++) { ret = njs_value_property_i64(vm, this, i, value); @@ -1732,7 +1732,13 @@ njs_array_prototype_join(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, size = njs_chb_size(&chain); if (njs_slow_path(size < 0)) { - njs_memory_error(vm); + if (chain.error == NJS_CHB_ERR_OVERFLOW) { + njs_range_error(vm, "invalid string length"); + + } else { + njs_memory_error(vm); + } + return NJS_ERROR; } diff --git a/src/njs_json.c b/src/njs_json.c index 166fb1b1..e6a849a8 100644 --- a/src/njs_json.c +++ b/src/njs_json.c @@ -1081,7 +1081,8 @@ njs_json_stringify_iterator(njs_json_stringify_t *stringify, goto memory_error; } - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(stringify->vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(stringify->vm), + NJS_STRING_MAX_LENGTH); for ( ;; ) { if (state->index == 0) { @@ -1188,8 +1189,16 @@ done: size = njs_chb_size(&chain); if (njs_slow_path(size < 0)) { + if (chain.error == NJS_CHB_ERR_OVERFLOW) { + njs_range_error(stringify->vm, "invalid string length"); + + } else { + njs_memory_error(stringify->vm); + } + njs_chb_destroy(&chain); - goto memory_error; + + return NJS_ERROR; } if (size == 0) { @@ -1200,7 +1209,7 @@ done: ret = njs_string_create_chb(stringify->vm, retval, &chain); if (njs_slow_path(ret != NJS_OK)) { njs_chb_destroy(&chain); - goto memory_error; + return NJS_ERROR; } release: diff --git a/src/njs_regexp.c b/src/njs_regexp.c index 7d09d5c8..6ab21123 100644 --- a/src/njs_regexp.c +++ b/src/njs_regexp.c @@ -1321,7 +1321,7 @@ njs_regexp_prototype_symbol_replace(njs_vm_t *vm, njs_value_t *args, } } - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(vm), NJS_STRING_MAX_LENGTH); results.separate = 0; results.pointer = 0; diff --git a/src/njs_string.c b/src/njs_string.c index d49f5872..09b515ba 100644 --- a/src/njs_string.c +++ b/src/njs_string.c @@ -2958,7 +2958,7 @@ njs_string_get_substitution(njs_vm_t *vm, njs_value_t *matched, p = rep.start; end = rep.start + rep.length; - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(vm), NJS_STRING_MAX_LENGTH); while (p < end) { r = njs_strlchr(p, end, '$'); @@ -3242,7 +3242,7 @@ njs_string_prototype_replace(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, return NJS_OK; } - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(vm), NJS_STRING_MAX_LENGTH); start = string.start; @@ -3826,6 +3826,11 @@ njs_string_decode_uri(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, src = string.start; end = string.start + string.size; + /* + * Percent-decoding never produces more bytes than it consumes and + * the input is a string within NJS_STRING_MAX_LENGTH, so the chain + * needs no byte cap. + */ NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); njs_utf8_decode_init(&ctx); @@ -4100,6 +4105,11 @@ njs_string_atob(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs, len = njs_base64_decoded_length(len, pad); + /* + * The chain holds a single reservation of at most twice the decoded + * length, which is bounded by the input string size, so it cannot + * grow unbounded and needs no byte cap. + */ NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); dst = njs_chb_reserve(&chain, len * 2); diff --git a/src/njs_typed_array.c b/src/njs_typed_array.c index d007285a..508331bc 100644 --- a/src/njs_typed_array.c +++ b/src/njs_typed_array.c @@ -2141,7 +2141,7 @@ njs_typed_array_prototype_join(njs_vm_t *vm, njs_value_t *args, return NJS_ERROR; } - NJS_CHB_MP_INIT(&chain, njs_vm_memory_pool(vm)); + NJS_CHB_MP_INIT_MAX(&chain, njs_vm_memory_pool(vm), NJS_STRING_MAX_LENGTH); njs_typed_array_to_chain(vm, &chain, array, separator); diff --git a/src/test/njs_unit_test.c b/src/test/njs_unit_test.c index 92635708..26c89434 100644 --- a/src/test/njs_unit_test.c +++ b/src/test/njs_unit_test.c @@ -8721,6 +8721,18 @@ static njs_unit_test_t njs_test[] = "String.prototype.concat.apply(s, a.slice(1))"), njs_str("RangeError: invalid string length") }, +#if (NJS_64BIT) + /* + * Adversarial regex replace must throw RangeError instead of + * OOM-killing the process. The chain accumulates ~2GiB before the + * cap trips, hence 64-bit only. + */ + { njs_str("var s = 'x'.repeat(1 << 18);" + "var r = '$1'.repeat(1 << 14);" + "s.replace(/(.+)/g, r)"), + njs_str("RangeError: invalid string length") }, +#endif + { njs_str("var a = 'abcdefgh'; a.substr(3, 15)"), njs_str("defgh") },