Commit b9bb1bd36c4 for nodejs
commit b9bb1bd36c4fac33d986326edb8f1943168d205d
Author: Christian Aurich Zanettini Martins <christian.aurichzm@gmail.com>
Date: Wed Sep 30 10:30:12 2026 -0300
ffi: reject unsafe integers as length or offset
GetValidatedSize() checks the value against
static_cast<double>(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 <christian.aurichzm@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66216
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
diff --git a/src/ffi/data.cc b/src/ffi/data.cc
index c42d51600c9..cd2a2481ef6 100644
--- a/src/ffi/data.cc
+++ b/src/ffi/data.cc
@@ -50,7 +50,10 @@ Maybe<size_t> GetValidatedSize(Environment* env,
return Nothing<size_t>();
}
- if (length > static_cast<double>(std::numeric_limits<size_t>::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<double>(std::numeric_limits<size_t>::max())) {
THROW_ERR_OUT_OF_RANGE(env, "The %s is too large", label);
return Nothing<size_t>();
}
diff --git a/test/ffi/test-ffi-memory.js b/test/ffi/test-ffi-memory.js
index 38ea177b6cb..be9160337dd 100644
--- a/test/ffi/test-ffi-memory.js
+++ b/test/ffi/test-ffi-memory.js
@@ -342,8 +342,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/);
@@ -351,6 +356,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'];