From 71282c00cb358958b55222574f9df76bbfc27a13 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Wed, 8 Apr 2026 07:50:18 +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 Signed-off-by: semimikoh --- src/encoding_binding.cc | 4 +- .../test-whatwg-encoding-encodeinto-large.js | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-whatwg-encoding-encodeinto-large.js diff --git a/src/encoding_binding.cc b/src/encoding_binding.cc index cddc88f1e03090..5e0606352d3587 100644 --- a/src/encoding_binding.cc +++ b/src/encoding_binding.cc @@ -8,6 +8,8 @@ #include "string_bytes.h" #include "v8.h" +#include +#include #include namespace node { @@ -102,7 +104,7 @@ void BindingData::EncodeInto(const FunctionCallbackInfo& args) { int written = source->WriteUtf8( isolate, write_result, - dest_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); From b1de92b8cf865f12a9340f88b604798abb674c02 Mon Sep 17 00:00:00 2001 From: semimikoh Date: Wed, 8 Apr 2026 07:50:26 +0900 Subject: [PATCH 2/2] test: simplify encodeInto large buffer regression test Drop the bounded subarray case, remove the unnecessary error code check, and use a 2**31 byte Uint8Array directly instead of slicing an offset subarray. Signed-off-by: semimikoh --- .../test-whatwg-encoding-encodeinto-large.js | 27 +++++-------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/test/parallel/test-whatwg-encoding-encodeinto-large.js b/test/parallel/test-whatwg-encoding-encodeinto-large.js index 66360430675307..09d9caa411e18a 100644 --- a/test/parallel/test-whatwg-encoding-encodeinto-large.js +++ b/test/parallel/test-whatwg-encoding-encodeinto-large.js @@ -10,32 +10,17 @@ 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; + dest = new Uint8Array(2 ** 31); +} catch { + common.skip('insufficient space for Uint8Array allocation'); } -const large = encoder.encodeInto(source, dest.subarray(offset)); -assert.deepStrictEqual(large, { +const result = encoder.encodeInto(source, dest); +assert.deepStrictEqual(result, { 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); +assert.deepStrictEqual(dest.subarray(0, expected.length), expected);