Commit cebc4337c4b for nodejs

commit cebc4337c4b1801df36d483bb568f0fa0850f605
Author: James M Snell <jasnell@gmail.com>
Date:   Sat Oct 3 14:39:27 2026 +0000

    fs: serialize FileHandle writer() async writes

    When writer() was created without a `start` offset, every write() and
    writev() call targeted the file descriptor's current position, and
    nothing prevented several of them from being in flight at once.
    Overlapping un-awaited writes then raced on the shared file offset
    (and partial writes were completed in follow-up syscalls), silently
    writing data at the wrong offsets while the reported byte count and
    the final file size still looked correct.

    Issue async writes one at a time, in call order. A write that is still
    queued when the writer fails, or whose signal aborts while it is
    queued, is no longer started.

    Assisted-by: OpenCode
    Signed-off-by: James M Snell <jasnell@gmail.com>
    PR-URL: https://github.com/nodejs/node/pull/66483
    Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>

diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js
index efa981c55e3..5ab7e1dacdc 100644
--- a/lib/internal/fs/promises.js
+++ b/lib/internal/fs/promises.js
@@ -751,6 +751,11 @@ if (getOptionValue('--experimental-stream-iter')) {
     let asyncPending = 0;
     let released = false;
     const pendingWrites = new SafeSet();
+    // Async writes are issued one at a time, in call order. Without this,
+    // overlapping write() calls would race on the file position: when no
+    // `start` is given every write targets the fd's current (shared)
+    // position, and partial writes are completed in follow-up syscalls.
+    let writeQueue = PromiseResolve();

     validateBoolean(autoClose, 'options.autoClose');

@@ -866,6 +871,22 @@ if (getOptionValue('--experimental-stream-iter')) {
       });
     }

+    function ignoreWriteQueueResult() {}
+
+    // Run `start` once every previously queued write has settled.
+    function enqueueWrite(start, signal) {
+      const operation = PromisePrototypeThen(writeQueue, () => {
+        // A failed writer must not touch the file again, and a write whose
+        // signal aborted while it was queued must not be started.
+        if (errored) throw error;
+        signal?.throwIfAborted();
+        return start();
+      });
+      writeQueue = PromisePrototypeThen(operation, ignoreWriteQueueResult,
+                                        ignoreWriteQueueResult);
+      return operation;
+    }
+
     function trackOperation(operation) {
       const { promise, resolve, reject } = PromiseWithResolvers();
       const pending = { __proto__: null, reject };
@@ -950,8 +971,9 @@ if (getOptionValue('--experimental-stream-iter')) {
         if (bytesRemaining > 0) bytesRemaining -= chunk.byteLength;
         const position = pos;
         if (pos >= 0) pos += chunk.byteLength;
-        return trackOperation(
-          writeAll(chunk, 0, chunk.byteLength, position, signal));
+        return trackOperation(enqueueWrite(
+          () => writeAll(chunk, 0, chunk.byteLength, position, signal),
+          signal));
       },

       writev(chunks, options = kNullPrototo) {
@@ -983,7 +1005,8 @@ if (getOptionValue('--experimental-stream-iter')) {
         if (bytesRemaining > 0) bytesRemaining -= totalSize;
         const position = pos;
         if (pos >= 0) pos += totalSize;
-        return trackOperation(writevAll(chunks, position, signal));
+        return trackOperation(enqueueWrite(
+          () => writevAll(chunks, position, signal), signal));
       },

       writeSync(chunk) {
diff --git a/test/parallel/test-fs-promises-file-handle-writer.js b/test/parallel/test-fs-promises-file-handle-writer.js
index a636844f8e5..1074c8156aa 100644
--- a/test/parallel/test-fs-promises-file-handle-writer.js
+++ b/test/parallel/test-fs-promises-file-handle-writer.js
@@ -1133,6 +1133,69 @@ async function testWriterWebIDLConversion() {
 // Run all tests
 // =============================================================================

+// =============================================================================
+// Overlapping (un-awaited) writes must land in call order
+// =============================================================================
+
+function makeTestData(size) {
+  const data = Buffer.allocUnsafe(size);
+  let seed = 0x9e3779b9;
+  for (let i = 0; i < size; i++) {
+    seed ^= seed << 13; seed ^= seed >>> 17; seed ^= seed << 5;
+    data[i] = seed & 0xff;
+  }
+  return data;
+}
+
+async function testConcurrentWritesPreserveOrder() {
+  const data = makeTestData(512 * 1024);
+  for (const options of [{}, { start: 0 }]) {
+    for (const useWritev of [false, true]) {
+      const filePath = path.join(
+        tmpDir,
+        `writer-concurrent-${options.start ?? 'none'}-${useWritev}.bin`);
+      const fh = await open(filePath, 'w');
+      const w = fh.writer(options);
+      const writes = [];
+      let offset = 0;
+      let i = 0;
+      while (offset < data.length) {
+        const size = 1 + ((i++ * 7919) % 9000);
+        const end = Math.min(offset + size, data.length);
+        const chunk = data.subarray(offset, end);
+        writes.push(useWritev ?
+          w.writev([chunk.subarray(0, 1), chunk.subarray(1)]) :
+          w.write(chunk));
+        offset = end;
+      }
+      await Promise.all(writes);
+      assert.strictEqual(await w.end(), data.length);
+      await fh.close();
+      assert.deepStrictEqual(fs.readFileSync(filePath), data);
+    }
+  }
+}
+
+async function testFailStopsQueuedWrites() {
+  const filePath = path.join(tmpDir, 'writer-fail-queued.bin');
+  const fh = await open(filePath, 'w');
+  const w = fh.writer();
+  const reason = new Error('stop');
+  const writes = [];
+  for (let i = 0; i < 10; i++) {
+    writes.push(w.write(Buffer.alloc(1024, i)));
+  }
+  w.fail(reason);
+  const results = await Promise.allSettled(writes);
+  for (const result of results) {
+    assert.strictEqual(result.status, 'rejected');
+    assert.strictEqual(result.reason, reason);
+  }
+  await fh.close();
+  // None of the queued writes may reach the file after fail().
+  assert.strictEqual(fs.statSync(filePath).size, 0);
+}
+
 Promise.all([
   testBasicWrite(),
   testBasicWritev(),
@@ -1187,4 +1250,6 @@ Promise.all([
   testWriterLimitAndStart(),
   testWriterArgumentValidation(),
   testWriterWebIDLConversion(),
+  testConcurrentWritesPreserveOrder(),
+  testFailStopsQueuedWrites(),
 ]).then(common.mustCall());