Skip to content

fix: skip native write for zero-length callback writeFile#146

Closed
caneraltinbasak wants to merge 2 commits into
42-x-y-bsfrom
cal/42-x-y-bs/OS-20458
Closed

fix: skip native write for zero-length callback writeFile#146
caneraltinbasak wants to merge 2 commits into
42-x-y-bsfrom
cal/42-x-y-bs/OS-20458

Conversation

@caneraltinbasak

@caneraltinbasak caneraltinbasak commented Jun 25, 2026

Copy link
Copy Markdown

Description of Change

v8 built with V8_ENABLE_SANDBOX in Chromium. Every Buffer's bytes
must live inside the V8 memory cage, and pointers are cage-relative.
A zero-length buffer has no allocation in the cage, so Buffer::Data()
resolves to the cage base / an empty-store sentinel — an address
the kernel rejects when it's handed to write(2), even with len == 0

Plain Node, V8 sandbox off. Buffers come from Node's normal heap
allocator; an empty buffer yields an ordinary valid pointer, so
write(fd, ptr, 0) is a harmless no-op → returns 0.

Callback fs.writeFile()/writeAll() always issued a native
fs.write(fd, buffer, 0, 0, ...) syscall even when the data was empty.
On the Electron-based roHtmlWidget Node integration that zero-length
write returns "EFAULT: bad address in system call argument, write",
whereas it succeeds under the standalone roNodeJs runtime and under
fs/promises.writeFile().

Add a zero-length guard to writeAll() that skips the syscall and runs
the normal completion path (optional fsync, close the fd only when
writeFile() opened it, invoke callback(null)), matching the early
return already present in fs/promises.writeFile(). Non-empty writes,
validation, abort/signal handling and caller-owned fd semantics are
unchanged.

Checklist

Release Notes

Notes:
Fixed an error(EFAULT) when Electron render process writes a file with zero length using fs.writeFile()

caneraltinbasak and others added 2 commits June 25, 2026 12:21
v8 built with V8_ENABLE_SANDBOX in Chromium. Every Buffer's bytes
must live inside the V8 memory cage, and pointers are cage-relative.
A zero-length buffer has no allocation in the cage, so Buffer::Data()
resolves to the cage base / an empty-store sentinel — an address
the kernel rejects when it's handed to write(2), even with len == 0

Plain Node, V8 sandbox off. Buffers come from Node's normal heap
allocator; an empty buffer yields an ordinary valid pointer, so
write(fd, ptr, 0) is a harmless no-op → returns 0.

Callback fs.writeFile()/writeAll() always issued a native
fs.write(fd, buffer, 0, 0, ...) syscall even when the data was empty.
On the Electron-based roHtmlWidget Node integration that zero-length
write returns "EFAULT: bad address in system call argument, write",
whereas it succeeds under the standalone roNodeJs runtime and under
fs/promises.writeFile().

Add a zero-length guard to writeAll() that skips the syscall and runs
the normal completion path (optional fsync, close the fd only when
writeFile() opened it, invoke callback(null)), matching the early
return already present in fs/promises.writeFile(). Non-empty writes,
validation, abort/signal handling and caller-owned fd semantics are
unchanged.
Add renderer (nodeIntegration / roHtmlWidget) regression tests for the
zero-length callback fs.writeFile() EFAULT fix. Covers empty string and
empty Buffer writes, a non-empty write, truncation of an existing file,
and a caller-owned fd that must not be closed by fs.writeFile().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@caneraltinbasak caneraltinbasak changed the title fix: skip native write for zero-length callback writeFile fix: skip native write for zero-length callback writeFile Jun 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses an Electron-specific failure mode in the callback-based fs.writeFile() path when writing zero-length data under V8 sandboxing, by skipping the underlying native write(2) syscall for length === 0 and completing via the normal success/flush/close path (aligning behavior with fs/promises.writeFile()).

Changes:

  • Added a Node patch to short-circuit writeAll() for zero-length writes while preserving the usual completion behavior (optional fsync, close semantics, callback).
  • Added renderer regression tests covering empty-string/empty-buffer writes, truncation, non-empty writes, and user-owned file descriptor behavior.
  • Registered the new Node patch in patches/node/.patches.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
spec/node-spec.ts Adds renderer regression coverage for callback fs.writeFile() zero-length writes and fd ownership behavior.
patches/node/fs_skip_native_write_for_zero-length_callback_writefile.patch Updates Node’s writeAll() to skip the native write syscall when length === 0 and reuse a shared completion path.
patches/node/.patches Includes the new patch in the Node patch application list.

@caneraltinbasak

Copy link
Copy Markdown
Author

This is not the right fix. I've found the root cause of the problem, this was just a workaround.
I've filed https://issues.chromium.org/issues/527930354 to address the right issue and will send a patch upstream and add a temporary fix to our yocto build until my patch makes its way.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants