From 249b825e07a937c889cf59c868a7b2066d819cd4 Mon Sep 17 00:00:00 2001 From: Temuulen Undrakhbayar Date: Wed, 2 Sep 2026 15:50:43 +0800 Subject: [PATCH] fs: fix writev UNKNOWN on large buffers Check total byteLength across all buffers before the syscall. A total over kIoMaxLength (2147483647) wraps to -2147483648 inside libuv and surfaces as UNKNOWN. Validate first and throw ERR_OUT_OF_RANGE so the error is actionable. Matches the earlier fix for fs.write in c4e7dca8f30, which validated length against INT32_MAX before uv_fs_write. Fixes: https://github.com/nodejs/node/issues/40779 Refs: https://github.com/nodejs/node/commit/c4e7dca8f30 Signed-off-by: Temuulen Undrakhbayar Assisted-by: Muse Spark --- lib/fs.js | 20 +++++ lib/internal/fs/promises.js | 8 ++ test/parallel/test-fs-writev-buffer-large.js | 85 ++++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 test/parallel/test-fs-writev-buffer-large.js diff --git a/lib/fs.js b/lib/fs.js index e1406596c583..77370c1f9d47 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -77,6 +77,7 @@ const { ERR_ACCESS_DENIED, ERR_FS_FILE_TOO_LARGE, ERR_INVALID_ARG_VALUE, + ERR_OUT_OF_RANGE, }, } = require('internal/errors'); @@ -1233,6 +1234,17 @@ function writev(fd, buffers, position, callback) { fd = getValidatedFd(fd); validateBufferArray(buffers); + // Most platforms do not allow writes >= 2 GiB (kIoMaxLength). + // Validate total byteLength before the syscall to avoid an + // opaque UNKNOWN error from the binding (see #40779). + let totalLength = 0; + for (let i = 0; i < buffers.length; i++) { + totalLength += buffers[i].byteLength; + if (totalLength > kIoMaxLength) { + throw new ERR_OUT_OF_RANGE.HideStackFramesError( + 'length', `<= ${kIoMaxLength}`, totalLength); + } + } callback ||= position; validateFunction(callback, 'cb'); @@ -1276,6 +1288,14 @@ ObjectDefineProperty(writev, kCustomPromisifyArgsSymbol, { */ function writevSync(fd, buffers, position) { validateBufferArray(buffers); + let totalLength = 0; + for (let i = 0; i < buffers.length; i++) { + totalLength += buffers[i].byteLength; + if (totalLength > kIoMaxLength) { + throw new ERR_OUT_OF_RANGE.HideStackFramesError( + 'length', `<= ${kIoMaxLength}`, totalLength); + } + } if (buffers.length === 0) { return 0; diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index a9d03ebb1d18..d3d013a7027a 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -1510,6 +1510,14 @@ async function write(handle, buffer, offsetOrOptions, length, position) { async function writev(handle, buffers, position) { validateBufferArray(buffers); + let totalLength = 0; + for (let i = 0; i < buffers.length; i++) { + totalLength += buffers[i].byteLength; + if (totalLength > kIoMaxLength) { + throw new ERR_OUT_OF_RANGE.HideStackFramesError( + 'length', `<= ${kIoMaxLength}`, totalLength); + } + } if (typeof position !== 'number') position = null; diff --git a/test/parallel/test-fs-writev-buffer-large.js b/test/parallel/test-fs-writev-buffer-large.js new file mode 100644 index 000000000000..03bfee2cb50f --- /dev/null +++ b/test/parallel/test-fs-writev-buffer-large.js @@ -0,0 +1,85 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const fsp = require('fs').promises; +const tmpdir = require('../common/tmpdir'); + +tmpdir.refresh(); + +// fs.writev with buffers totalling > INT32_MAX should throw ERR_OUT_OF_RANGE +// Refs: https://github.com/nodejs/node/issues/40779 +// Precedent: test/parallel/test-fs-write-buffer-large.js (c4e7dca8f30) + +common.skipIf32Bits(); + +let buf; +try { + buf = Buffer.allocUnsafe(0x7FFFFFFF + 1); +} catch (e) { + if (e.message !== 'Array buffer allocation failed') throw e; + common.skip('skipped due to memory requirements'); +} + +const filename = tmpdir.resolve('writev-large.txt'); +const filename2 = tmpdir.resolve('writev-large2.txt'); +const filename3 = tmpdir.resolve('writev-large3.txt'); + +// writevSync throws synchronously +{ + const fd = fs.openSync(filename, 'w'); + assert.throws(() => { + fs.writevSync(fd, [buf], 0); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + message: /The value of "length" is out of range.*2147483648/, + }); + // Two buffers that sum to > kIoMaxLength also throw + const small = Buffer.allocUnsafe(10); + assert.throws(() => { + fs.writevSync(fd, [buf, small], 0); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + fs.closeSync(fd); +} + +// writev (callback) throws synchronously before the syscall +{ + const fd = fs.openSync(filename2, 'w'); + assert.throws(() => { + fs.writev(fd, [buf], 0, common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + }); + assert.throws(() => { + fs.writev(fd, [buf, Buffer.allocUnsafe(1)], common.mustNotCall()); + }, { + code: 'ERR_OUT_OF_RANGE', + }); + fs.closeSync(fd); +} + +// fs.promises.writev rejects with ERR_OUT_OF_RANGE +(async () => { + const handle = await fsp.open(filename3, 'w'); + await assert.rejects( + handle.writev([buf], 0), + { + code: 'ERR_OUT_OF_RANGE', + name: 'RangeError', + } + ); + await assert.rejects( + handle.writev([buf, Buffer.allocUnsafe(1)]), + { + code: 'ERR_OUT_OF_RANGE', + } + ); + // Empty array still succeeds + const { bytesWritten } = await handle.writev([], null); + assert.strictEqual(bytesWritten, 0); + await handle.close(); +})().then(common.mustCall());