From a36efd414f885d7cb1f70e890d8ff9510d73acee Mon Sep 17 00:00:00 2001 From: semimikoh Date: Tue, 7 Apr 2026 09:43:51 +0900 Subject: [PATCH 1/2] src: clamp WriteUtf8 capacity to INT_MAX in EncodeInto In TextEncoder.encodeInto, the destination buffer's byte length is read as a size_t but then implicitly narrowed to int when passed as the capacity argument to v8::String::WriteUtf8. When the destination view is larger than INT_MAX (2,147,483,647 bytes), the narrowing conversion underflows to a negative value, V8 treats it as "no capacity", and writes 0 bytes - returning { read: 0, written: 0 } even though the buffer has plenty of room. Clamp the capacity to INT_MAX before passing it to WriteUtf8. This is sufficient because the source string in encodeInto is bounded in practice and never requires more than INT_MAX bytes to encode; only the destination view length can exceed INT_MAX. This issue is already fixed on main and v24.x as a side effect of PR #58070, which migrated to the non-deprecated WriteUtf8V2 method whose capacity parameter is size_t. WriteUtf8V2 is not available in v22.x's V8 version, so this minimal patch fixes only the EncodeInto path instead of backporting the full migration. Refs: https://github.com/nodejs/node/pull/58070 Fixes: https://github.com/nodejs/node/issues/62610 --- src/encoding_binding.cc | 5 +- .../test-whatwg-encoding-encodeinto-large.js | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 test/pummel/test-whatwg-encoding-encodeinto-large.js diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index cddc88f1e03090..5182528589d68b 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -8,6 +8,7 @@ #include "string_bytes.h" #include "v8.h" +#include #include namespace node { @@ -97,12 +98,14 @@ void BindingData::EncodeInto(const FunctionCallbackInfo& args) { Local buf = dest->Buffer(); char* write_result = static_cast(buf->Data()) + dest->ByteOffset(); size_t dest_length = dest->ByteLength(); + int max_length = + dest_length > static_cast(INT_MAX) ? INT_MAX : dest_length; int nchars; int written = source->WriteUtf8( isolate, write_result, - dest_length, + max_length, &nchars, String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8); diff --git a/test/pummel/test-whatwg-encoding-encodeinto-large.js b/test/pummel/test-whatwg-encoding-encodeinto-large.js new file mode 100644 index 00000000000000..d970afd382e567 --- /dev/null +++ b/test/pummel/test-whatwg-encoding-encodeinto-large.js @@ -0,0 +1,46 @@ +'use strict'; + +const common = require('../common'); +const os = require('os'); + +common.skipIf32Bits(); +if (os.totalmem() < 3 * 1024 * 1024 * 1024) { + common.skip('requires at least 3 GiB of system memory'); +} + +const assert = require('assert'); + +const encoder = new TextEncoder(); +const source = 'a\xFF\u6211\u{1D452}'; +const expected = encoder.encode(source); + +const size = 2 ** 31 + expected.length; +const offset = expected.length - 1; + +try { + const dest = new Uint8Array(size); + + const large = encoder.encodeInto(source, dest.subarray(offset)); + assert.deepStrictEqual(large, { + read: source.length, + written: expected.length, + }); + assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); + + const bounded = encoder.encodeInto(source, + dest.subarray(offset, + offset + expected.length)); + assert.deepStrictEqual(bounded, { + read: source.length, + written: expected.length, + }); + assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); +} catch (e) { + if (e.code === 'ERR_MEMORY_ALLOCATION_FAILED') { + common.skip('insufficient space for Uint8Array allocation'); + } + if (/Array buffer allocation failed/.test(e.message)) { + common.skip('insufficient space for Uint8Array allocation'); + } + throw e; +} From bb48d929056a4ae1d77694ca916bb40dbbcf1991 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Tue, 7 Apr 2026 11:27:55 +0900 Subject: [PATCH 2/2] src: clamp WriteUtf8 capacity to INT_MAX in EncodeInto Signed-off-by: semimikoh --- src/encoding_binding.cc | 5 +- .../test-whatwg-encoding-encodeinto-large.js | 41 +++++++++++++++++ .../test-whatwg-encoding-encodeinto-large.js | 46 ------------------- 3 files changed, 43 insertions(+), 49 deletions(-) create mode 100644 test/parallel/test-whatwg-encoding-encodeinto-large.js delete mode 100644 test/pummel/test-whatwg-encoding-encodeinto-large.js diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index 5182528589d68b..5e0606352d3587 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -8,6 +8,7 @@ #include "string_bytes.h" #include "v8.h" +#include #include #include @@ -98,14 +99,12 @@ void BindingData::EncodeInto(const FunctionCallbackInfo& args) { Local buf = dest->Buffer(); char* write_result = static_cast(buf->Data()) + dest->ByteOffset(); size_t dest_length = dest->ByteLength(); - int max_length = - dest_length > static_cast(INT_MAX) ? INT_MAX : dest_length; int nchars; int written = source->WriteUtf8( isolate, write_result, - max_length, + std::min(dest_length, static_cast(INT_MAX)), &nchars, String::NO_NULL_TERMINATION | String::REPLACE_INVALID_UTF8); diff --git a/test/parallel/test-whatwg-encoding-encodeinto-large.js b/test/parallel/test-whatwg-encoding-encodeinto-large.js new file mode 100644 index 00000000000000..66360430675307 --- /dev/null +++ b/test/parallel/test-whatwg-encoding-encodeinto-large.js @@ -0,0 +1,41 @@ +'use strict'; + +const common = require('../common'); + +common.skipIf32Bits(); + +const assert = require('assert'); + +const encoder = new TextEncoder(); +const source = 'a\xFF\u6211\u{1D452}'; +const expected = encoder.encode(source); + +const size = 2 ** 31 + expected.length; +const offset = expected.length - 1; +let dest; + +try { + dest = new Uint8Array(size); +} catch (e) { + if (e.code === 'ERR_MEMORY_ALLOCATION_FAILED' || + /Array buffer allocation failed/.test(e.message)) { + common.skip('insufficient space for Uint8Array allocation'); + } + throw e; +} + +const large = encoder.encodeInto(source, dest.subarray(offset)); +assert.deepStrictEqual(large, { + read: source.length, + written: expected.length, +}); +assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); + +const bounded = encoder.encodeInto(source, + dest.subarray(offset, + offset + expected.length)); +assert.deepStrictEqual(bounded, { + read: source.length, + written: expected.length, +}); +assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); diff --git a/test/pummel/test-whatwg-encoding-encodeinto-large.js b/test/pummel/test-whatwg-encoding-encodeinto-large.js deleted file mode 100644 index d970afd382e567..00000000000000 --- a/test/pummel/test-whatwg-encoding-encodeinto-large.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -const common = require('../common'); -const os = require('os'); - -common.skipIf32Bits(); -if (os.totalmem() < 3 * 1024 * 1024 * 1024) { - common.skip('requires at least 3 GiB of system memory'); -} - -const assert = require('assert'); - -const encoder = new TextEncoder(); -const source = 'a\xFF\u6211\u{1D452}'; -const expected = encoder.encode(source); - -const size = 2 ** 31 + expected.length; -const offset = expected.length - 1; - -try { - const dest = new Uint8Array(size); - - const large = encoder.encodeInto(source, dest.subarray(offset)); - assert.deepStrictEqual(large, { - read: source.length, - written: expected.length, - }); - assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); - - const bounded = encoder.encodeInto(source, - dest.subarray(offset, - offset + expected.length)); - assert.deepStrictEqual(bounded, { - read: source.length, - written: expected.length, - }); - assert.deepStrictEqual(dest.slice(offset, offset + expected.length), expected); -} catch (e) { - if (e.code === 'ERR_MEMORY_ALLOCATION_FAILED') { - common.skip('insufficient space for Uint8Array allocation'); - } - if (/Array buffer allocation failed/.test(e.message)) { - common.skip('insufficient space for Uint8Array allocation'); - } - throw e; -}