FileSink: mark sink done when end() fails during flush - #31135
Conversation
When FileSink::end()/end_from_js() hit a write error while flushing, they closed the writer's handle via writer.close() but left both FileSink.done and writer.is_done false. A subsequent write()/flush() (including the deferred auto-flusher) would then call sys::write with Fd::INVALID, tripping a debug assertion. Use writer.end() instead of writer.close() in the error arm so writer.is_done is set, and set FileSink.done as well. Also guard try_write_with_write_fn against an invalid fd so any other path that reaches it with a closed handle returns Done(0) instead of issuing a syscall on fd -1.
WalkthroughThis PR modifies error handling in the file sink writer. ChangesFileSink writer error handling and defensive guards
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/js/bun/util/filesink.test.ts`:
- Line 283: Replace the use of tmpdirSync() assigned to the variable dir with
the test harness tempDir fixture: import { tempDir } from 'harness' (if not
already imported) and replace const dir = tmpdirSync(); with const dir = await
tempDir(); (or const dir = tempDir(); depending on the fixture API) so the test
uses the harness-managed temporary directory and automatic cleanup instead of
fs/tmpdirSync. Ensure any code that expects a string path still works with the
fixture return value.
- Around line 304-308: The test currently uses a synchronous assertion for
writer.flush() which only catches sync throws; change it to await the promise
and assert it resolves (e.g., use await
expect(writer.flush()).resolves.toBeUndefined() or await writer.flush() inside
try/catch) so async rejections fail the test, and similarly replace the
Promise.resolve(writer.end()).catch(() => {}) pattern with an awaited assertion
or try/catch (await writer.end() or await
expect(writer.end()).resolves.toBeUndefined()) to surface async errors from
writer.end().
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 163542fc-71c5-4537-8856-b22fec0c323d
📒 Files selected for processing (3)
src/io/PipeWriter.rssrc/runtime/webcore/FileSink.rstest/js/bun/util/filesink.test.ts
| }); | ||
|
|
||
| it.skipIf(!isPosix)("writing after end() fails during flush does not crash", async () => { | ||
| const dir = tmpdirSync(); |
There was a problem hiding this comment.
Use tempDir fixture instead of tmpdirSync in this new test.
This new test introduces tmpdirSync(), which is against the repo test harness rule and weakens cleanup guarantees.
Suggested change
- const dir = tmpdirSync();
- const target = join(dir, "ro.txt");
+ using dir = tempDir("filesink-end-fail-flush", {});
+ const target = join(dir, "ro.txt");As per coding guidelines: "**/*.test.{ts,tsx}: Use tempDir from 'harness' to create temporary directories - do not use tmpdirSync or fs.mkdtempSync."
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const dir = tmpdirSync(); | |
| using dir = tempDir("filesink-end-fail-flush", {}); | |
| const target = join(dir, "ro.txt"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/js/bun/util/filesink.test.ts` at line 283, Replace the use of
tmpdirSync() assigned to the variable dir with the test harness tempDir fixture:
import { tempDir } from 'harness' (if not already imported) and replace const
dir = tmpdirSync(); with const dir = await tempDir(); (or const dir = tempDir();
depending on the fixture API) so the test uses the harness-managed temporary
directory and automatic cleanup instead of fs/tmpdirSync. Ensure any code that
expects a string path still works with the fixture return value.
| expect(() => writer.write("y")).not.toThrow(); | ||
| expect(() => writer.start({})).not.toThrow(); | ||
| expect(() => writer.write("z")).not.toThrow(); | ||
| expect(() => writer.flush()).not.toThrow(); | ||
| await Promise.resolve(writer.end()).catch(() => {}); |
There was a problem hiding this comment.
flush() assertion is sync-only and can miss async failures.
expect(() => writer.flush()).not.toThrow() only validates synchronous throw. If flush() rejects asynchronously, this test can produce false positives or unhandled rejection noise.
Suggested change
- expect(() => writer.flush()).not.toThrow();
- await Promise.resolve(writer.end()).catch(() => {});
+ await expect(writer.flush()).resolves.toBeUndefined();
+ await Promise.resolve(writer.end()).catch(() => {});🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@test/js/bun/util/filesink.test.ts` around lines 304 - 308, The test currently
uses a synchronous assertion for writer.flush() which only catches sync throws;
change it to await the promise and assert it resolves (e.g., use await
expect(writer.flush()).resolves.toBeUndefined() or await writer.flush() inside
try/catch) so async rejections fail the test, and similarly replace the
Promise.resolve(writer.end()).catch(() => {}) pattern with an awaited assertion
or try/catch (await writer.end() or await
expect(writer.end()).resolves.toBeUndefined()) to surface async errors from
writer.end().
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
|
Updated 10:32 AM PT - May 20th, 2026
✅ @robobun, your commit 917756aa88a77632b67242398102e646e632193e passed in 🧪 To try this PR locally: bunx bun-pr 31135That installs a local version of the PR into your bun-31135 --bun |
There was a problem hiding this comment.
LGTM — focused fix aligning the error arm with the other terminal arms, plus a defensive fd guard and regression test.
Extended reasoning...
Overview
This PR fixes a fuzzer-found debug assertion where FileSink::end()/end_from_js() hitting a write error during flush would call writer.close() without setting self.done or writer.is_done. A subsequent write()/flush() (or start({}) resetting done) would then attempt I/O on Fd::INVALID. The fix is two lines per error arm (set done = true, call writer.end() instead of writer.close()), a 3-line defensive Fd::INVALID check in PosixPipeWriter::try_write_with_write_fn, and a regression test.
Security risks
None. This is a state-machine consistency fix in the file-sink error path; no auth, parsing, or trust-boundary changes. The defensive guard returns Done(0) rather than issuing a syscall on fd -1, which is strictly safer.
Level of scrutiny
Low-to-medium. The change is mechanical: writer.end() is exactly is_done = true; close() (PipeWriter.rs:1070-1077), so the only behavioral delta vs. the old close() call is that is_done gets set — which is precisely what the other WriteResult arms (Done/Wrote) already do via writer.end(). Setting self.done = true mirrors the Pending arm. The on_close re-entry path is unchanged since end() still calls close().
Other factors
The defensive Fd::INVALID → Done(0) guard matches the existing is_done || closed_without_reporting → Done(0) early-returns in write/flush/write_latin1/write_utf16, so it's consistent with established semantics. The new POSIX-only test exercises the exact repro (read-only fd → flush error → write/start/flush after) and verifies no crash. No outstanding reviewer comments; no prior reviews on the timeline.
When
FileSink::end()/end_from_js()hit a write error while flushing buffered data, they closed the writer's handle viawriter.close()but left bothFileSink.doneandwriter.is_doneunset. A subsequentwrite()/flush()(including the deferred auto-flusher microtask, or afterstart({})resetdone) would then attempt to write toFd::INVALID, tripping a debug assertion (fd != Fd::INVALIDinsys::Error::with_fd, or the rustix fd validity assert depending on the write path taken).Fix
WriteResult::Errarm ofFileSink::endandFileSink::end_from_js, setself.done = trueand callwriter.end()(which setsis_donebefore closing) instead ofwriter.close(). This matches the other arms which all leave the writer in a terminal state.PosixPipeWriter::try_write_with_write_fnnow returnsDone(0)when the handle has no fd instead of issuing a syscall on fd-1.Repro
Found by Fuzzilli.