From 0558424a5bdb66154f78494a504e803f5e301281 Mon Sep 17 00:00:00 2001 From: Christian Aurich Zanettini Martins Date: Tue, 22 Sep 2026 20:27:52 -0300 Subject: [PATCH] ffi: reject unsafe integers as length or offset GetValidatedSize() checks the value against static_cast(SIZE_MAX), which rounds up to 2^64 on 64-bit platforms. A length or offset of 2 ** 64 gets through, and the cast to size_t after it is undefined behavior. With GCC on x64 it gives 0, so ffi.setUint8(ptr, 2 ** 64, 42) writes to ptr instead of throwing. Anything above Number.MAX_SAFE_INTEGER may already have been rounded by the time it gets here, so reject those values too. The export*() helpers already cap their length there, and so does setInt64() for number values. SIZE_MAX is still the limit on 32-bit platforms. When buffer.constants.MAX_LENGTH is Number.MAX_SAFE_INTEGER, as on 64-bit builds without the V8 sandbox, toBuffer() and toArrayBuffer() now throw ERR_OUT_OF_RANGE for MAX_LENGTH + 1 instead of ERR_BUFFER_TOO_LARGE. Signed-off-by: Christian Aurich Zanettini Martins --- src/ffi/data.cc | 5 ++++- test/ffi/test-ffi-memory.js | 24 ++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/ffi/data.cc b/src/ffi/data.cc index dbeba94cd1b9..8cf2e025f303 100644 --- a/src/ffi/data.cc +++ b/src/ffi/data.cc @@ -50,7 +50,10 @@ Maybe GetValidatedSize(Environment* env, return Nothing(); } - if (length > static_cast(std::numeric_limits::max())) { + // Values beyond Number.MAX_SAFE_INTEGER may already have been rounded + // by the caller. On 32-bit platforms SIZE_MAX is the tighter bound. + if (length > kMaxSafeJsInteger || + length > static_cast(std::numeric_limits::max())) { THROW_ERR_OUT_OF_RANGE(env, "The %s is too large", label); return Nothing(); } diff --git a/test/ffi/test-ffi-memory.js b/test/ffi/test-ffi-memory.js index 53fe3928029b..8c42b677cbf9 100644 --- a/test/ffi/test-ffi-memory.js +++ b/test/ffi/test-ffi-memory.js @@ -322,8 +322,13 @@ test('ffi validates memory access arguments', () => { assert.throws(() => ffi.toBuffer(maxPointer, 8), /pointer and length exceed the platform address range/); assert.throws(() => ffi.toArrayBuffer(maxPointer, 8), /pointer and length exceed the platform address range/); - assert.throws(() => ffi.toBuffer(1n, bufferConstants.MAX_LENGTH + 1), { code: 'ERR_BUFFER_TOO_LARGE' }); - assert.throws(() => ffi.toArrayBuffer(1n, bufferConstants.MAX_LENGTH + 1), { code: 'ERR_BUFFER_TOO_LARGE' }); + + // If MAX_LENGTH is Number.MAX_SAFE_INTEGER, MAX_LENGTH + 1 is an unsafe + // integer and is rejected before the buffer length is checked. + if (bufferConstants.MAX_LENGTH < Number.MAX_SAFE_INTEGER) { + assert.throws(() => ffi.toBuffer(1n, bufferConstants.MAX_LENGTH + 1), { code: 'ERR_BUFFER_TOO_LARGE' }); + assert.throws(() => ffi.toArrayBuffer(1n, bufferConstants.MAX_LENGTH + 1), { code: 'ERR_BUFFER_TOO_LARGE' }); + } if (process.arch === 'ia32' || process.arch === 'arm') { assert.throws(() => ffi.toBuffer(2n ** 32n, 0), /platform pointer range/); @@ -331,6 +336,21 @@ test('ffi validates memory access arguments', () => { })); }); +test('ffi rejects unsafe integers as an offset or length', () => { + withAllocations(common.mustCall((alloc) => { + const ptr = alloc(8); + const range = { code: 'ERR_OUT_OF_RANGE' }; + + // On 64-bit platforms SIZE_MAX rounds up to 2 ** 64 as a double. + for (const value of [Number.MAX_SAFE_INTEGER + 1, 2 ** 64]) { + assert.throws(() => ffi.getUint8(ptr, value), range); + assert.throws(() => ffi.setUint8(ptr, value, 42), range); + assert.throws(() => ffi.toBuffer(ptr, value), range); + assert.throws(() => ffi.toArrayBuffer(ptr, value), range); + } + })); +}); + test('ffi memory helpers reject missing required arguments', () => { const widths = ['Int8', 'Uint8', 'Int16', 'Uint16', 'Int32', 'Uint32', 'Int64', 'Uint64', 'Float32', 'Float64'];