Pass the job to JobContext::run and FileOpener::get_fd by pointer, not &mut - #37820
Pass the job to JobContext::run and FileOpener::get_fd by pointer, not &mut#37820robobun wants to merge 3 commits into
Conversation
JobContext::run took the job's off-thread part as &mut, and FileOpener::get_fd / get_fd_by_opening and their fn(&mut Self, Fd) continuation took the task as &mut as well. ReadFile and WriteFile hand the object on from inside that chain (to the io thread, or by finishing the completion token, after which the JS thread reads and frees the job through its own pointer), and ReadFileUV's continuation can free it, so every one of those reference arguments was still protected on the publishing thread's stack while that happened. Job::run_on_pool now passes &raw mut (*job).off to an unsafe fn run(off: *mut OffThread, ..); the synchronous implementations reborrow for their work (inline, or through a &mut self helper) and return the token as before. FileOpener::get_fd / get_fd_by_opening take this: *mut Self and an OpenCallback<Self> = unsafe fn(*mut Self, Fd), do their accessor calls through call-scoped reborrows and invoke the continuation last; the POSIX open loop moves into open_pathlike(&mut self). ReadFile::run_async / WriteFile::run_async take the pointer, and the continuations (run_async_with_fd / run_with_fd) decide the next step in prepare_read / prepare_write under a reborrow that ends before the step runs, then perform it through the pointer. ReadFileUV's on_file_open is the same continuation shape on Windows. test/internal/source-lints/self-receiver-job-start.test.ts bans the reference shapes: a JobContext run (declaration or impl) whose first parameter is not a pointer, FileOpener entry points taking self, and an open continuation spelled fn(&mut Self, Fd).
|
Warning Review limit reached
Next review available in: 3 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (18)
Comment |
|
Status: fix is up in this PR. Reproduced how: Verified: debug (ASAN) build on Linux with the tests listed in the description; native Windows debug build of 3ace166 (the libuv branch of |
There was a problem hiding this comment.
I reviewed this PR and the bug-hunting pass found no issues. Because it changes the JobContext::run trait signature and FileOpener::get_fd across 18 files — core pool-job and file-I/O infrastructure with extensive new unsafe blocks, a restructured Windows libuv open path, and merge-ordering ties to #37705/#37768/#37787 — a human look is still worthwhile.
What was reviewed:
- All 19
JobContext::runconversions: synchronous impls reborrow-and-returndone; the three that hand off (ReadFile, WriteFile, readdir-recursive) make the hand-over their last access. FileOpenerrewrite: POSIXopen_pathlikeextraction preserves the ENOENT/mkdirp retry loop; Windowswrapped_callbackreadsresultand runsuv_fs_req_cleanupbefore touching the task, andreq.datais set beforeuv_fs_open(sync-failure ordering preserved).prepare_read/prepare_write→Nextsplit: each early-return branch maps 1:1 to the step it previously called; the collapsedcould_block && NotReadycondition is equivalent.- The new source lint's
itemBlockbounding andPOINTER_PARAMregex against the fixture cases in the test.
Extended reasoning...
Overview
This PR converts two trait entry points from &mut receivers to raw-pointer parameters to fix a Stacked/Tree Borrows protector violation: JobContext::run (src/jsc/job.rs, the pool-thread body of every Job<C>) and FileOpener::get_fd/get_fd_by_opening plus its continuation type (src/runtime/webcore/Blob.rs). It updates all 19 JobContext implementations, restructures ReadFile/WriteFile's first pool step around a Next enum so the hand-over runs after the &mut reborrow ends, converts ReadFileUV::on_file_open (Windows) to the new OpenCallback shape, and adds a source-lint test enforcing the pointer signature.
Security risks
None identified. This is an aliasing-model soundness fix with no user-facing behavior change; no new inputs are parsed, no auth/crypto/permission logic is touched (the crypto-job files only get a mechanical signature update).
Level of scrutiny
High. src/jsc/job.rs is the carrier for every off-thread job in the runtime, and the FileOpener path is the entry to all Bun.file() reads/writes. The change adds ~30 new unsafe blocks with SAFETY comments whose correctness depends on reborrow-scoping arguments, and significantly restructures the Windows libuv get_fd_by_opening thunk (request cleanup ordering, req.data assignment before dispatch). The PR is one of a coordinated series (#37681/#37705/#37768/#37787) with explicit merge-ordering notes — a maintainer tracking that series should confirm the intended landing order.
Other factors
The PR description is unusually thorough (miri reduction, per-ending test coverage map, cross-target cargo check). The bug hunter found nothing; I spot-checked the Windows wrapped_callback ordering change (uv_fs_req_cleanup now runs before the task's error fields are set rather than after — harmless, since cleanup only frees libuv-internal request state and result is copied out first), the prepare_read/prepare_write branch-to-Next mapping, and the readdir-recursive perform_work call sequencing. All look behavior-preserving. Deferring on scope and infrastructure criticality, not on any specific concern.
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not duplicates, but both are deliberately adjacent:
|
|
Updated 11:29 AM PT - Aug 12th, 2026
❌ @robobun, your commit dd27fce has 2 failures in
🧪 To try this PR locally: bunx bun-pr 37820That installs a local version of the PR into your bun-37820 --bun |
…atch The continuation type now only exists as the OpenCallback<T> alias, so the lint checks the alias definition itself (unsafe fn over *mut) and widens the spelled-out net to any fn(&mut X, Fd). The anchored checks record what they examined and assert the JobContext declaration, the two FileOpener entry points and the one alias were found, so a rename fails the lint rather than emptying it. The readdir scan's run comment says that its hand-over still happens inside perform_work's &mut self.
| /// must touch only the off-thread part. | ||
| /// | ||
| /// `off` is a pointer rather than `&mut` because a body that keeps `done` | ||
| /// hands the job on before it returns (to an io loop, to sub-tasks, or, by | ||
| /// finishing `done`, to the JS thread), and whoever ends up finishing it | ||
| /// makes the JS thread read and free the allocation through the job's own | ||
| /// pointer (`Job::complete`), possibly before this thread has returned | ||
| /// from here. A reference argument is protected for the whole call, and an | ||
| /// access through another pointer to memory a protected reference covers | ||
| /// is UB under the aliasing model whether or not the reference is used | ||
| /// again; a raw pointer makes no such claim. A body that completes | ||
| /// synchronously just reborrows for the work and returns `done`; one that | ||
| /// hands the job on does its own work through reborrows that end before the | ||
| /// hand-over, and makes the hand-over its last access. | ||
| /// | ||
| /// # Safety | ||
| /// `off` is the live job's off-thread part, and nothing else touches it | ||
| /// until this returns `Some(done)` or the body hands the job on. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // On `None` the body handed the job on (it may already be freed), and | ||
| // nothing below touches `*this`: releasing `vm` goes through our own | ||
| // `handle` clone, not the job's. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// What [`FileOpener::get_fd`] continues with once the task's fd is known: the | ||
| /// fd, or `Fd::INVALID` with `errno` / `system_error` set when the open failed. | ||
| /// It takes the task over: the implementations end by handing it on | ||
| /// (`ReadFile`, `WriteFile`; the JS thread then reads and frees the job through | ||
| /// the job's own pointer) or by freeing it right there (`ReadFileUV`). Neither | ||
| /// is allowed while a `&mut Self` argument is still protected, i.e. until the | ||
| /// continuation has returned (see [`bun_jsc::JobContext::run`]); hence the | ||
| /// pointer, here and in the `get_fd` frames that invoke it. | ||
| /// | ||
| /// # Safety | ||
| /// `this` is the live task `get_fd` was given, and the caller does not touch it | ||
| /// afterwards. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Opens the path in `pathlike()` and records the outcome: the fd in | ||
| /// `opened_fd`, or `Fd::INVALID` plus `errno` / `system_error`. Returns | ||
| /// what it recorded. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // `mkdir_if_not_exists` already populated | ||
| // `errno`/`system_error` on the impl. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// [`get_fd`](Self::get_fd) for a task whose `pathlike()` is a path. | ||
| /// | ||
| /// # Safety | ||
| /// As [`get_fd`](Self::get_fd). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Finds the task's fd (`opened_fd` if it is already set, the descriptor | ||
| /// of an fd-backed `pathlike()`, or else by opening the path) and continues | ||
| /// with `callback`, which takes the task over. | ||
| /// | ||
| /// # Safety | ||
| /// `this` is the live task and nothing else is using it; the caller does | ||
| /// not touch it afterwards, since `callback` hands it on or frees it (see | ||
| /// [`OpenCallback`]). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// What [`ReadFile::prepare_read`] decided the read continues with; performed | ||
| /// by `run_async_with_fd` once that `&mut self` stage has returned. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The job's first pool step: keeps the token and starts the read. From | ||
| /// `get_fd` on, `*this` belongs to whichever thread the read continues on. | ||
| /// | ||
| /// # Safety | ||
| /// [`bun_jsc::JobContext::run`]'s contract; the caller does not touch | ||
| /// `*this` afterwards. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The read's [`OpenCallback`](crate::webcore::blob::OpenCallback): decides | ||
| /// the next step under a reborrow that ends before the step runs, because | ||
| /// the step hands `*this` on (to the io thread, or to the JS thread, which | ||
| /// frees it). | ||
| /// | ||
| /// # Safety | ||
| /// `OpenCallback`'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The read's [`OpenCallback`]: queues the fstat, or finishes (which frees | ||
| /// the task) if the open failed or the fstat cannot be queued. | ||
| /// | ||
| /// # Safety | ||
| /// `OpenCallback`'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// What [`WriteFile::prepare_write`] decided the write continues with; | ||
| /// performed by `run_with_fd` once that `&mut self` stage has returned. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The job's first pool step: keeps the token and starts the write. From | ||
| /// `get_fd` on, `*this` belongs to whichever thread the write continues on. | ||
| /// | ||
| /// # Safety | ||
| /// [`bun_jsc::JobContext::run`]'s contract; the caller does not touch | ||
| /// `*this` afterwards. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The write's [`OpenCallback`](crate::webcore::blob::OpenCallback): | ||
| /// decides the next step under a reborrow that ends before the step runs, | ||
| /// because the step hands `*this` on (to the io thread, or to the JS | ||
| /// thread, which frees it). | ||
| /// | ||
| /// # Safety | ||
| /// `OpenCallback`'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
There was a problem hiding this comment.
I reviewed this PR and the automated bug-hunting pass found no issues. Given the scope — a core trait signature change (JobContext::run and FileOpener::get_fd/OpenCallback) across ~19 implementations, all of it unsafe cross-thread pointer handling in the work-pool and blob I/O paths, plus deliberate overlap with #37787 and #37705 that needs merge-order coordination — a human review is still warranted.
What was checked:
- Each converted
JobContext::runimpl either reborrows-and-returnsdone(synchronous jobs) or passes the pointer through without a protected reference on the hand-over path (ReadFile/WriteFile/readdir scan). FileOpener::get_fd/get_fd_by_openingand the Windowswrapped_callbackthunk: reborrows end before the continuation runs;req.datais set beforeuv_fs_openso a synchronous failure path is sound.- The
prepare_read/prepare_writeextraction preserves the original branch order and side effects; theNextdispatch matches the calls it replaced. - The new source-lint's anchor assertions guard against the checks going vacuously empty on rename.
Extended reasoning...
Overview
This PR converts two trait entry points — JobContext::run (src/jsc/job.rs) and FileOpener::get_fd/get_fd_by_opening (src/runtime/webcore/Blob.rs) — from taking &mut to taking *mut, and threads that change through every implementation (19 JobContext impls across 15 files) and the three continuations (ReadFile::run_async_with_fd, WriteFile::run_with_fd, ReadFileUV::on_file_open). The motivation is Stacked/Tree Borrows: a &mut argument is protected until the call returns, but bodies that keep the Completion hand the job to another thread (which may free it through the job's own pointer) before returning, which is a protector violation. It also adds a source-lint test enforcing the pointer shape.
Security risks
None identified. This is an aliasing-model soundness fix with no user-facing behavior change; no auth, crypto semantics, path validation, or untrusted-input handling is altered. The crypto/DNS/fs impls only had their run bodies moved into a &mut self helper called through (*this).run().
Level of scrutiny
High. This is unsafe Rust in the hottest cross-thread paths in the runtime (every pool job goes through JobContext::run; every Bun.file() read/write on POSIX goes through FileOpener::get_fd). The Windows libuv branch of get_fd_by_opening was substantially rewritten and per the author's own status comment has only been cargo checked for the Windows target — a native Windows run is noted as "in progress". The change also intentionally duplicates the Next/prepare_* split from #37787 and touches the same FileOpener surface as #37705, so a maintainer should decide landing order.
Other factors
- CI build #93427 for the head commit is still running; results are not yet in.
- The comment-cop bot has left 13 unaddressed inline comments flagging long doc comments. Several of these are on required
# Safetysections for the newly-unsafe fntrait methods (which the repo's own review rules mandate), so they may be false positives — but the author should triage them. - The recursive readdir scan (
AsyncReaddirRecursivein node_fs.rs) is explicitly only partially converted here (only the entry frame), with the rest deferred to a follow-up; the PR description acknowledgesperform_workstill forms overlapping&mutunder it. - The bug-hunting system found no issues, and my own read of each converted impl confirms the reborrow-then-return / pointer-pass-through pattern is applied consistently. But the combination of scope, unsafe density, incomplete Windows runtime verification, and merge coordination with two adjacent PRs makes this unsuitable for auto-approval.
| /// must touch only the off-thread part. | ||
| /// | ||
| /// `off` is a pointer, not `&mut`: a body that keeps `done` hands the job | ||
| /// on before returning, and the JS thread then frees it through the job's | ||
| /// own pointer (`Job::complete`), which is UB while a reference argument | ||
| /// is still protected here. Such a body reborrows only for work that ends | ||
| /// before the hand-over, which is its last access. | ||
| /// | ||
| /// # Safety | ||
| /// `off` is the live job's off-thread part and nothing else touches it | ||
| /// until this returns `Some(done)` or the body hands the job on. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| // On `None` the job may already be freed; nothing below touches it | ||
| // (`vm` is released through our own `handle` clone). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// [`FileOpener::get_fd`]'s continuation: gets the fd, or `Fd::INVALID` with | ||
| /// `errno` / `system_error` set, and takes the task over. It ends by handing | ||
| /// the task on (`ReadFile`, `WriteFile`) or freeing it (`ReadFileUV`), hence a | ||
| /// pointer rather than a `&mut` that would still be protected then, here and | ||
| /// in the `get_fd` frames that invoke it (as for [`bun_jsc::JobContext::run`]). | ||
| /// | ||
| /// # Safety | ||
| /// `this` is the live task `get_fd` was given; the caller does not touch it | ||
| /// afterwards. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Opens `pathlike()`; returns the fd it recorded in `opened_fd`, or | ||
| /// `Fd::INVALID` after recording `errno` / `system_error`. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// Finds the task's fd (already opened, the store's own, or by opening the | ||
| /// path) and continues with `callback`, which takes the task over. | ||
| /// | ||
| /// # Safety | ||
| /// `this` is the live task and nothing else is using it; the caller does | ||
| /// not touch it afterwards ([`OpenCallback`] hands it on or frees it). |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// First pool step: keeps the token and starts the read; `get_fd` hands `*this` on. | ||
| /// | ||
| /// # Safety | ||
| /// [`bun_jsc::JobContext::run`]'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The read's continuation: `prepare_read`'s reborrow has ended by the | ||
| /// time the step it chose hands `*this` on. | ||
| /// | ||
| /// # Safety | ||
| /// [`OpenCallback`](crate::webcore::blob::OpenCallback)'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The read's continuation: queues the fstat, or finishes (freeing the task). | ||
| /// | ||
| /// # Safety | ||
| /// [`OpenCallback`]'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// First pool step: keeps the token and starts the write; `get_fd` hands `*this` on. | ||
| /// | ||
| /// # Safety | ||
| /// [`bun_jsc::JobContext::run`]'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
| /// The write's continuation: `prepare_write`'s reborrow has ended by the | ||
| /// time the step it chose hands `*this` on. | ||
| /// | ||
| /// # Safety | ||
| /// [`OpenCallback`](crate::webcore::blob::OpenCallback)'s contract. |
There was a problem hiding this comment.
If you need a paragraph-long comment to justify why the workaround is OK, the code is wrong — fix the code
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Given it changes the JobContext::run signature to unsafe fn(*mut ...) across all 19 implementations, restructures FileOpener's entry points and the ReadFile/WriteFile continuation chain, and is designed to land alongside #37787 / #37705 with intentionally overlapping edits, a human look is warranted.
What was reviewed
- The 16 synchronous
JobContext::runshims: each reborrow is call/statement-scoped and returnsSome(done)— no hand-off, so(*this).run()is sound under the fn contract. get_fd/get_fd_by_opening(POSIX + the Windows libuv branch): the continuation is the last access on every path;wrapped_callbackreadsresultand cleans upreqbefore touching the task, andreq.datais set beforeuv_fs_open(sync-failure path preserved).prepare_read/prepare_writesplit: control flow matches the old bodies branch-for-branch; the&mut selfends before theNextstep hands*thison.AsyncReaddirRecursiveTask: only the entry frame is converted (perform_workstill&mut self), matching the description's stated scope.
Extended reasoning...
Overview
The PR converts two trait entry points that carry a job from the pool thread into code that hands the job on to another thread: JobContext::run (src/jsc/job.rs, the declaration and every impl block — 19 sites across 15 files) and FileOpener::get_fd / get_fd_by_opening plus the OpenCallback<T> continuation type (src/runtime/webcore/Blob.rs), from &mut to *mut. ReadFile::run_async_with_fd / WriteFile::run_with_fd are split into a prepare_*(&mut self) -> Next decision followed by a pointer-carried step, and ReadFileUV::on_file_open becomes the continuation on Windows. A new source-lint test pins the four shapes (trait first parameter, FileOpener entry points, the OpenCallback alias, and any fn(&mut X, Fd) spelling).
Security risks
None identified. This is an aliasing-model correctness change to internal unsafe Rust; no user-facing input handling, auth, crypto, or trust boundary is touched. The behavioural surface is meant to be unchanged and is exercised by existing coverage.
Level of scrutiny
High. This is memory-safety-critical unsafe Rust across the work-pool job carrier and the Blob file-open state machine, with hand-written SAFETY comments at every reborrow. The change is well-reasoned (the miri reduction in the description demonstrates the protector violation, and the pointer-carrying shape is the established fix from #37681/#37705/#37768/#37787), but the number of touched implementations, the deliberate overlap with two open PRs that must merge cleanly in either order, and the Windows libuv branch (which #37705 removes) make this a change a human should sign off on rather than an automated approval.
Other factors
- The
comment-copbot fired again at 18:12 (after the "Shorten the new doc and SAFETY comments" commit) on ~10 locations in job.rs / Blob.rs / read_file.rs / write_file.rs. The flagged comments read as legitimate design docs (why*mutand not&mut) rather than workaround justifications, but whether they need further trimming is a style call for a human. - CI on 3ace166 shows failures (
complex-workspace.test.ts,bake/deinitialization.test.tssegfault on Windows, freebsd aarch64 / linux aarch64-musl build failures) that look unrelated to this change but should be confirmed pre-existing before merge; no CI result is visible yet for dd27fce. - The PR intentionally leaves
AsyncReaddirRecursiveTask::perform_workand the steps belowrun_async_with_fd/run_with_fdon&mut selffor the sibling PRs; a reviewer should confirm the merge plan with #37787 / #37705 (which one lands first, and that the sharedNext/prepare_*edits collapse cleanly).
Problem
Bun.filereads and writes hand the job to another thread while the pool thread's frames still hold&mutreferences to it, and that thread finishes and frees the job through its own pointer. On Windows,ReadFileUVfrees itself from inside a continuation that received it as&mut.&mutargument is protected until the call returns, so another thread touching or freeing that memory before then is undefined behaviour under Stacked Borrows and Tree Borrows (whatbun run rust:mirichecks).Fix
JobContext::runandFileOpener::get_fd/get_fd_by_openingreceive the job as a raw pointer, and the open continuation type becomesunsafe fn(*mut T, Fd). The 16 synchronous jobs reborrow for their work and return the token as before; the longer bodies move unchanged into a&mut selfmethod.&mut selfmethods; filed separately), the steps under the continuations are io: hand ReadFile/WriteFile to the io thread through the owner's pointer, not &mut self.io_request #37787's, and the Windows libuv open branch gets the minimum conversion until blob(windows): free CopyFileWindows and ReadFileUV through the task pointer, not under &mut self #37705 removes it.runimplementations, 11 continuation spellings) and passes here; the existing tests for each ending (ENOENT, empty file, pipe, fd, copy) and for the other jobs pass on an ASAN debug build; clippy and a Windows and darwincargo checkpass.Background
runeither returns the completion token to finish now, or keeps it so another thread can finish later; finishing makes the JS thread complete and free the whole job.FileOpeneris the trait shared by the Blob read, write and copy state machines.get_fdfinds the fd (already open, given as an fd, or by opening the path, retrying after mkdir) and then calls a continuation that takes the task over.ReadFileUVis the Windows read path. libuv callbacks drive it instead of the pool, and it frees itself when it finishes.Original description
What
JobContext::run(src/jsc/job.rs) received the job's off-thread part as&mut, fromJob::run_on_pool'sC::run(&mut (*this).off, ..).FileOpener::get_fd/get_fd_by_opening(src/runtime/webcore/Blob.rs) took&mut selfand a continuation typedfn(&mut Self, Fd), invoked ascallback(self, fd). For aReadFileorWriteFilethat is the whole way into the object:and the last step hands the object on: to the io thread, or, by finishing the
Completion, to the JS thread, whoseJob::completereads and frees the whole job through the job's own pointer.ReadFileUV(Windows) reaches its continuation the same way and can free the task from it. No crash is known; this is the contract shape that #37681 / #37705 / #37768 / #37787 are converting elsewhere, and the one chain #37787 deliberately leaves as is (itsrun_async_with_fd/run_with_fdcomments name these two traits as the remaining piece).Why the reference shape is wrong
A reference passed as an argument is protected until the call returns, in Stacked Borrows and in Tree Borrows (what
bun run rust:miriuses). Once the job has been handed on, the thread that finishes it accesses and deallocates memory those references cover, through a pointer that is not derived from them, and the pool thread has not necessarily returned through the six frames above yet. For a read that finishes in its first step (an open error, an empty file:prepare_readreturnsFinishandon_finishposts the token at once) the JS thread is idle and gets there while those frames are still unwinding as a matter of course; for the polled endings it is a race with the io thread and a second pool thread. Both models reject a foreign access to memory a protected reference covers, and any deallocation of such memory, whether or not the reference is used again;ReadFileUVfreeing itself from under the continuation's own&mutis rejected for the same reason (the self-receiver-reclaim lint's case). A raw pointer makes no claim, so the same sequence is fine once the frames carry one. A standalone reduction of the four shapes under miri is below; the two reference shapes are rejected and the two pointer shapes accepted, and the one thing that is not rejected (another thread writing through a pointer derived from the reference, joined before the return) is also the one thing these frames do not rely on.Tree Borrows reduction (
MIRIFLAGS=-Zmiri-tree-borrows cargo miri run)Fix
JobContext::runisunsafe fn run(off: *mut Self::OffThread, vm, done), with the contract on the trait;run_on_poolpasses&raw mut (*this).off. The 16 synchronous implementations reborrow for their work and returndoneas before: the ones whose body was already a call do it through(*this).run()(or a field copy), and the longer inline bodies (ZstdJob,WalkTask,Pbkdf2Job,RandomFillJob) move unchanged into a&mut selfmethod the shim calls. The recursive readdir scan, which keepsdoneand fans out, sets the token and callsperform_workthrough the pointer; that is as far as this PR takes it.perform_work,write_resultsandfinish_concurrentlystill take&mut self, the hand-over (done.finish()by whichever thread decrementssubtask_countto zero) happens inside them, and the subtasks form their own overlapping&mutto the shared task throughParentRef::assume_mut, so for this job only the carrier frame is converted here. The rest is a separate change of its own (the state the subtasks share wants to be reached through&self/ the pointer, andfinish_concurrentlywants theReadFiletreatment); it has been filed as such and is not started by this PR.FileOpener:pub type OpenCallback<T> = unsafe fn(*mut T, Fd);get_fd/get_fd_by_openingtakethis: *mut Self, make their accessor calls through call-scoped reborrows, and invoke the continuation last. The POSIX open loop isopen_pathlike(&mut self) -> Fd, soget_fd_by_openingthere is "open, then continue". The Windows branch (libuv open) is converted to the same shape because the callback type forces it: the completion thunk reads and cleans up the request before touching the task, and the stash hooks take anOpenCallback. blob(windows): free CopyFileWindows and ReadFileUV through the task pointer, not under &mut self #37705 removes that branch andReadFileUV's use of the trait altogether; the conversion here is the minimum that keeps it compiling until then.ReadFile/WriteFile:run_async(this: *mut Self, task)replacesrun+run_async; the continuationsrun_async_with_fd/run_with_fdtake the pointer, get the decision fromprepare_read/prepare_write(the old bodies, returning aNextinstead of calling the step) under a reborrow that has ended by the time the step runs, and perform the step through the pointer. This split is the same one io: hand ReadFile/WriteFile to the io thread through the owner's pointer, not &mut self.io_request #37787 makes (same enum, same function names, same edits to the bodies) so that the two merge cleanly; whichever lands second keeps one copy and routes the shim toproceed. The steps themselves (wait_for_*,on_finish,do_close, the read/write loops) still take&mut selfhere and are io: hand ReadFile/WriteFile to the io thread through the owner's pointer, not &mut self.io_request #37787's; theWorkPoolhand-overs in these files are Hand intrusive work-pool tasks to the pool through the object's pointer, not a reference #37768's; neither is touched.ReadFileUV::on_file_openbecomes the continuation on Windows (itson_finish->finalizeis blob(windows): free CopyFileWindows and ReadFileUV through the task pointer, not under &mut self #37705's).Tests
test/internal/source-lints/self-receiver-job-start.test.tsmakes four checks: the first parameter offn runinside theJobContextdeclaration and every impl block must be a raw pointer; so must the first parameter ofget_fd/get_fd_by_openinginside theFileOpenertrait; theOpenCallback<T>definition must readunsafe fn(*mut T, Fd)(the three continuations are passed as fn items where the alias is expected, so pinning the alias pins their signatures through the type checker, and reverting the alias alone fails the lint); and, as a net under that, a continuation type spelledfn(&mut X, Fd)in any parameter syntax is banned anywhere. The anchored checks also record what they examined and assert they found the declaration in job.rs plus at least ten implementations, exactly the twoFileOpenerentry points and exactly one alias definition, so renaming or moving any of them fails the lint instead of emptying it; each regex has banned / allowed / not-examined fixtures. Withsrc/at the base commit it reports the trait declaration plus all 19 implementations, bothFileOpenerentry points and the 11 continuation spellings (Blob.rs, read_file.rs incl.ReadFileUV, write_file.rs), and the alias check reports it missing; with this branch it passes, as does the rest oftest/internal/source-lints/.Behaviour is meant to be unchanged. The converted frames are exercised by the existing coverage of each ending:
test/js/bun/util/bun-file.test.tsandbun-write.test.js(ENOENT on read and write, i.e.open_pathlikefailing and the job finishing in its first step; thecreatePathcases go throughtry_mkdirp),bun-file-fd-read.test.ts(theopened_fdbranch ofget_fd, empty file),test/regression/issue/07500andbun-stdin-slice.test.ts(a pipe:prepare_read->WaitForReadable),bun-write.test.js's pipe and fd cases (prepare_write-> wait / loop,CopyFile), plus the files covering the other implementations.Verification
Debug (ASAN) build: the files above,
bun-file-read,zstd,password,glob/scan,pbkdf2,scrypt,crypto-random,hkdf-callback-null,web/streams/compression,archive,node/fs/promises,fs.test.ts -t readdir(the recursive scan),bundler/transpiler/transpiler.test.js, and thetest-crypto-{prime,hkdf,keygen-async-*,secret-keygen,dh-stateless,sign-verify,random,pbkdf2,scrypt}parallel tests all pass. Two things time out in this container independently of the change:bun-write.test.js's 256 MB copy-without-copy_file_rangetest (the test's ownBun.hashof 256 MB takes 1.7 s each way under ASAN; the same test is noted in #37787) andglob/scan.test.ts's six node_modules cases, where thefast-globreference run the test awaits alongside takes 63 s under the debug build whileBun.Glob's scan job returns the same 17325 entries in 1.8 s.cargo clippy -p bun_jsc -p bun_runtimeis clean;cargo check -p bun_runtimepasses forx86_64-pc-windows-msvc(the libuv branch andReadFileUV) andaarch64-apple-darwin;cargo fmtis clean.