From c4cc7a943119ba5c680cc3203ed3dfba8b6f6afd Mon Sep 17 00:00:00 2001 From: Christian Aurich Date: Mon, 7 Sep 2026 17:07:36 -0300 Subject: [PATCH] fs: fix crash on negative zero file descriptor `isInt32()` accepts -0 because `-0 === (-0 | 0)`, but V8 does not represent -0 as an Int32 value, so `Value::IsInt32()` rejects it. The utf8 fast paths of `readFileSync()` and `writeFileSync()` hand the value straight to the binding, which then took it for a path and aborted on the null check. Coerce -0 to 0 before the call, matching `getValidatedFd()` and the rest of fs, where -0 is a valid way to name file descriptor 0. Signed-off-by: Christian Aurich --- lib/fs.js | 12 ++++++++++-- test/parallel/test-fs-negative-zero.js | 15 +++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index ad90551f40f1..50bea658163c 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -614,7 +614,11 @@ function readFileSync(path, options) { if ((options.encoding === 'utf8' || options.encoding === 'utf-8') && !hasUserBuffer) { - if (!isInt32(path)) { + if (isInt32(path)) { + // V8 does not report -0 as an int32, so it would reach the binding as a + // path instead of a file descriptor. + path |= 0; + } else { path = getValidatedPath(path); } return binding.readFileUtf8(path, stringToFlags(options.flag)); @@ -2995,7 +2999,11 @@ function writeFileSync(path, data, options) { // C++ fast path for string data and UTF8 encoding if (typeof data === 'string' && (options.encoding === 'utf8' || options.encoding === 'utf-8')) { - if (!isInt32(path)) { + if (isInt32(path)) { + // V8 does not report -0 as an int32, so it would reach the binding as a + // path instead of a file descriptor. + path |= 0; + } else { path = getValidatedPath(path); } diff --git a/test/parallel/test-fs-negative-zero.js b/test/parallel/test-fs-negative-zero.js index 538cea67faaa..09f965eb171f 100644 --- a/test/parallel/test-fs-negative-zero.js +++ b/test/parallel/test-fs-negative-zero.js @@ -2,6 +2,8 @@ require('../common'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); @@ -29,5 +31,18 @@ ignoreExpectedError(() => fs.mkdirSync(missing, { mode: -0 })); ignoreExpectedError(() => fs.chmodSync(missing, -0)); ignoreExpectedError(() => fs.writeFileSync(missing, '', { mode: -0 })); +// -0 is accepted as file descriptor 0. Writing an empty string reaches the +// utf8 fast path without issuing a write on the descriptor. +fs.writeFileSync(-0, ''); +fs.appendFileSync(-0, ''); + +const child = spawnSync( + process.execPath, + ['-e', 'process.stdout.write(require("fs").readFileSync(-0, "utf8"))'], + { input: 'hello' }, +); +assert.strictEqual(child.status, 0); +assert.strictEqual(child.stdout.toString(), 'hello'); + fs.watchFile(missing, { interval: -0 }, () => {}); fs.unwatchFile(missing);