Skip to content

ai slop - #38315

Closed
robobun wants to merge 4 commits into
mainfrom
farm/498f5576/write-file-release-completion
Closed

ai slop#38315
robobun wants to merge 4 commits into
mainfrom
farm/498f5576/write-file-release-completion

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

This PR has been marked as AI slop and the description has been updated to avoid confusion or misleading reviewers.

Many AI PRs are fine, but sometimes they submit a PR too early, fail to test if the problem is real, fail to reproduce the problem, or fail to test that the problem is fixed. If you think this PR is not AI slop, please leave a comment.

…ansform output, readdir and mkdir results

WriteFile carried the Box holding Bun.write's promise as a raw ctx pointer in
its off-thread half, freed only by the completion. A job released instead of
completed (the VM tore down first, or the pool's post was refused) leaked the
box and kept the promise rooted. The promise is now the job's Js half
(WriteFilePromise), released by the VM with its other live jobs; on Windows
WriteFileWindows holds it as a field until it settles it.

TransformTask.output_code, StringOrUndefined (ret::Mkdir) and ret::Readdir
held bare bun_core::Strings / Dirents that only the to_js path released, so
the same release paths leaked them too. output_code and the mkdir path are
OwnedStrings now and ret::Readdir releases whatever to_js did not hand over
in Drop, which also covers a conversion that fails part-way. The FromAny impl
for Box<[bun_core::String]> had no other user and is gone.

worker-refused-completion.test.ts gets rows for the four producers, starts
each row's work from a microtask so leaksan.supp's module-evaluation entry
does not hide what it allocates, and turns LSan on itself on ASAN builds.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cc6c7e21-6b77-4ec2-902c-aa63f5254604

📥 Commits

Reviewing files that changed from the base of the PR and between b555e06 and ac8b966.

📒 Files selected for processing (6)
  • src/jsc/JSValue.rs
  • src/runtime/api/JSTranspiler.rs
  • src/runtime/node/node_fs.rs
  • src/runtime/webcore/Blob.rs
  • src/runtime/webcore/blob/write_file.rs
  • test/js/web/workers/worker-refused-completion.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review (head ac8b966).

Reproduced on the unfixed build with test/js/web/workers/worker-refused-completion.test.ts under LSan (bun bd test, ASAN debug build): the new Bun.write() row fails with Direct leak of 16 byte(s) allocated by Box<write_file::WriteFilePromise>::new in write_file_with_source_destination, right after [vm_handle] refused job: Job<blob::write_file::WriteFile>; the other 20 rows pass. With this branch all 21 rows pass.

The readdir / mkdir / transform rows exercise the new release paths under ASAN but cannot show their leaks by themselves (WTF string memory is not tracked by LSan); the Bun.write() row is the one that fails without the fix.

Since the first push: the blob read / write rows are skipped on Windows, where those operations are libuv requests rather than pool jobs (15271bf); the flagged doc comments are trimmed (27a670a); the one comment still naming create_with_ctx now names create (ac8b966). No code change since 1654584. The CodeRabbit comments are rate limit notices.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR and didn't find any bugs. Because it reworks ownership across several GC/job-lifecycle boundaries (WriteFile's Js half, a new Drop for ret::Readdir, OwnedString in TransformTask/StringOrUndefined) and includes a #[cfg(windows)] path that only Windows CI exercises, a human look is still worthwhile.

Checked: Bun__Dirent__toJS transfers via transferToWTFString() so the extra Dirent::deref in the new Drop is a no-op on already-delivered entries; BunString__createArray refs (does not consume), matching the new Files arm; reject_with_async_stack preserves the async-stack behavior the old to_error_instance_with_async_stack provided; the sync readdirSync path (fs_to_js&mut self to_js) still balances via the new Drop; the removed FromAny for Box<[bun_core::String]> had no other callers.

Extended reasoning...

Overview

Six files: write_file.rs moves WriteFilePromise from an erased *mut c_void box in the off-thread half to a JsAffine Js half of the WriteFile job (and a plain field on WriteFileWindows), removing create_with_ctx/the callback pointer/the heap box; Blob.rs updates the two call sites; JSTranspiler.rs swaps output_code to OwnedString; node_fs.rs swaps StringOrUndefined::String to OwnedString, changes ret::Readdir::to_js to &mut self, and adds impl Drop for Readdir; JSValue.rs deletes the now-unused FromAny for Box<[bun_core::String]>; the test file adds five rows and turns on LSan for the ASAN lane locally.

Security risks

None. Pure resource-lifetime work on internal job/result types; no parsing of untrusted input, no auth/crypto, no user-facing API surface change.

Level of scrutiny

High — this is native memory-safety code (the most-blocked category per REVIEW.md): GC roots (JSPromiseStrong), WTF refcounts released off the JS thread, a new Drop impl on a return type that flows through both sync and async fs paths, and a cfg-gated Windows rewrite of run_from_js_thread/create. I traced the double-release hazards (Dirent::to_js_newly_created already derefs after transferToWTFString() leaves the strings Dead, so the added Drop deref is a no-op; to_js_arrayBunString__createArrayBun::toJS refs rather than transfers, so Drop releasing the caller's refs is the same balance as the removed FromAny loop) and the async-stack behavior (JSPromiseStrong::reject_with_async_stack replaces to_error_instance_with_async_stack + reject). All looked correct.

Other factors

The PR description is thorough and lists cross-platform cargo check and the relevant test suites. Verifier agents already ruled out the Windows-Bun.write()-row concern (WriteFileWindows is not a pool job, so no refusal path — but that means the row is POSIX-only fail-before proof, which the description acknowledges). Still, the Windows run_from_js_thread rewrite (dereferencing (*(*this).event_loop).global_ref() before deinit) and the new Drop for Readdir interacting with ResultListEntryValue::deinit on the recursive-readdir error path are the kind of ownership reshaping a maintainer should sign off on.

Comment thread src/runtime/api/JSTranspiler.rs Outdated
Comment thread src/runtime/node/node_fs.rs Outdated
Comment thread src/runtime/node/node_fs.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
Comment thread src/runtime/webcore/blob/write_file.rs Outdated
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:05 PM PT - Aug 13th, 2026

@robobun, your commit ac8b966 is building: #95323

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

On the points in the review above:

  • Windows row: right, on Windows Bun.write() is a WriteFileWindows libuv request on the worker's own loop, not a pool job, so nothing is refused and the row would fail on a local Windows debug build (CI only runs this describe on the Linux ASAN lane). 15271bf skips it on Windows, together with the two existing Bun.file() rows, which are in the same position (ReadFileUV on Windows). The fail-before proof for this PR is the POSIX Bun.write() row, as the description says.
  • (*(*this).event_loop).global_ref() before deinit: global_ref() returns &'static JSGlobalObject (the VM-rooted global; deinit frees only the WriteFileWindows box), so reading it before the free and settling after is the same shape CopyFileWindows::throw / resolve_promise use.
  • Drop for ret::Readdir and ResultListEntryValue::deinit: the recursive task only builds a ret::Readdir after mem::replace-ing its result_list with an empty Files(Vec::new()), so the entries have exactly one owner at any time: ret::Readdir (released by its Drop after to_js) on the success path, and result_list (released by the task's Drop via deinit) on the pending_err path, where no ret::Readdir is created. perform_work's error path frees its local Vec with destroy_entry before anything becomes a ret::Readdir.

27a670a trims the doc comments the comment lint flagged to one line each (or drops them where the OwnedString type already says it); no code change. The CodeRabbit comments are its rate limit notices.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/runtime/webcore/blob/write_file.rs:616-618 — The SAFETY comment in WriteFileWindows::on_open's debug_assert (line 819) still says "data was set to this in create_with_ctx/open", but this PR renamed create_with_ctxcreate and deleted the wrapper. One-word fix: create_with_ctxcreate (the analogous comment in deinit was already updated).

    Extended reasoning...

    What the bug is

    This PR renames WriteFileWindows::create_with_ctx to create and deletes the old thin create wrapper that used to call it. It updated one comment that referenced the old name — deinit now says "paired with the RAII note in create" — but missed a second one. The SAFETY comment inside on_open's debug_assert at write_file.rs:819 still reads:

    debug_assert!(core::ptr::eq(
        this,
        // SAFETY: req == &(*this).io_request; data was set to `this` in create_with_ctx/open.
        unsafe { (*req).data }.cast::<WriteFileWindows>()
    ));

    create_with_ctx no longer exists on WriteFileWindows after this PR.

    The specific code path

    io_request.data is now assigned in the renamed create ((*write_file).io_request.data = write_file.cast::<c_void>();) and again in open. So the substance of the SAFETY justification is still correct — only the function name it points at is stale.

    Why existing code doesn't prevent it

    This is a comment inside #[cfg(windows)]-gated code. Renames don't touch comments, and cargo check on non-Windows targets never even parses this module's body semantically, so nothing mechanical catches it. REVIEW.md calls this out explicitly: "Signature changes and renames → grep the whole repo including cfg-gated code and generated-binding inputs; stale call sites compile fine", and "SAFETY comments are required above use of unsafe and must be accurate."

    Impact

    None at runtime — it's a debug-only assertion whose SAFETY comment names a function that was renamed in the same PR. The next reader grepping for create_with_ctx to understand where data is set will find nothing.

    Step-by-step proof

    1. Before this PR, WriteFileWindows had two constructors: create_with_ctx (did the work, set io_request.data = write_file) and create (a .cast() shim over it).
    2. This PR deletes the shim and renames create_with_ctxcreate; the diff shows the deinit comment updated from "paired with the RAII note in create_with_ctx" to "paired with the RAII note in create".
    3. grep create_with_ctx src/runtime/webcore/blob/write_file.rs on the PR head yields exactly one hit — line 819 in on_open — with no corresponding function definition anywhere in the file.
    4. The assignment the comment justifies now lives in create (and open), so the correct text is "data was set to this in create/open".

    How to fix

    -                // SAFETY: req == &(*this).io_request; data was set to `this` in create_with_ctx/open.
    +                // SAFETY: req == &(*this).io_request; data was set to `this` in create/open.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Good catch on the on_open SAFETY comment: ac8b966 points it at create, which is where io_request.data is set now. create_with_ctx has no other mention left in the tree. The CodeRabbit comment is its rate limit notice again.

Comment thread src/runtime/webcore/blob/write_file.rs
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

The line comment above is the same on_open SAFETY comment already fixed in ac8b966; replied in the thread.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

I had the WriteFile half of this on a separate branch before finding this PR, so closing mine (#38332) in favour of this one. Branch, in case any of it is useful here: https://github.com/oven-sh/bun/tree/farm/d835da79/writefile-promise-js-side

Differences, all cosmetic on the fix itself: it makes type Js = JSPromiseStrong directly (the CopyFile shape) with one settle() shared by WriteFile::then and WriteFileWindows::run_from_js_thread, and deletes WriteFilePromise / WriteFileResultType / the callback type outright.

Two pieces of evidence from that branch that this PR does not have yet, if you want them:

  • test/js/bun/io/bun-write.test.js on the branch has three rows that each fail on the unfixed debug build with the same 16-byte report and pass fixed: refused (gate), process.exit() with the completion still queued, and worker.terminate() with the completion still queued. They set detect_leaks=1 themselves, so the proof is reproducible with plain bun bd test, which might help given the "no test proof" note above. They also assert the file content, so a row cannot pass unless the write really was a pool job.
  • The Windows half was built and run on a Windows x64 machine: bun-write.test.js 40 pass / 10 skip and blob-write.test.ts 15 pass, which drive WriteFileWindows through both the resolve and the reject path.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I reviewed this PR again at ac8b966 and didn't find any bugs; the earlier SAFETY-comment nit is fixed. Because it reworks cross-thread ownership of the WriteFile promise, adds a Drop to ret::Readdir, and touches the Windows libuv path, a human look is still worthwhile.

What was reviewed:

  • Refcount balance of the new Drop for ret::Readdir: BunString__createArray refs each element (so Files deref-on-drop is the caller's +1), and Bun__Dirent__toJS transferToWTFStrings name/path, so WithFileTypes deref-on-drop is a no-op on transferred entries and releases the rest on a mid-loop failure.
  • Sync readdir path (run_syncfs_to_js(&mut self)) still drops result after conversion, so no double-free and no leak with the new signature.
  • reject_with_async_stack on JSPromiseStrong attaches the same async frames the old to_error_instance_with_async_stack + reject pair did.
  • The removed FromAny for Box<[bun_core::String]> has no remaining callers.
Extended reasoning...

Overview

The PR fixes leaks on the refused/unrun-job release path for four producers: Bun.write()'s promise box, TransformTask.output_code, StringOrUndefined (mkdir), and ret::Readdir entries. It restructures WriteFile's completion from an erased fn(*mut c_void, ...) callback + heap-boxed WriteFilePromise (freed only in then) into a typed Js = WriteFilePromise half that the job machinery releases at VM teardown. WriteFileWindows holds the promise as an Option field. The three string-producing results move from BunString (Copy, no Drop) to OwnedString, and ret::Readdir gains a Drop that derefs whatever to_js didn't hand off. The FromAny for Box<[bun_core::String]> impl is removed as dead. Tests add rows to worker-refused-completion.test.ts and turn on LSan for the ASAN build.

Security risks

None identified. This is memory-lifecycle / leak-fix work; no untrusted-input parsing, auth, crypto, or permissions logic is touched.

Level of scrutiny

High. Per the repo's review guidance, "Native code: memory safety" is the most-blocked category, and this PR sits squarely in it: cross-thread ownership transfer, JSC GC rooting (JSPromiseStrong inside a JsAffine-derived struct), a new Drop on a type whose contents may already have been transferred to C++, and a Windows-only libuv path that reads global_ref() before freeing the request box. The correctness depends on FFI-side semantics (Bun::toJS refs vs. transferToWTFString transfers; String::deref() being a no-op on non-WTF tags) that aren't visible in the diff alone.

Other factors

I traced the specific ref-count questions and found them correct:

  • BunString__createArray calls Bun::toJS per element, which for WTFStringImpl constructs WTF::String(impl) → +1 ref, so Drop's per-element deref on Files releases exactly the caller's ref.
  • Bun__Dirent__toJS transferToWTFStrings both name and path unconditionally, so after to_js_newly_created each Dirent's fields are Dead and both the existing self.deref() there and the new Drop deref are no-ops on delivered entries. On a mid-loop put_index failure, undelivered Dirents keep their refs and are released by Drop — the exact partial-failure leak the PR describes.
  • run_sync binds the result as let mut result and calls fs_to_js(&mut self), then drops it — matching the async FsReturn path — so the to_js(self) → to_js(&mut self) signature change plus Drop doesn't double-free the sync path.
  • The WriteFilePromise::settle error path uses to_error_instance + JSPromiseStrong::reject_with_async_stack; the latter (in JSPromise.rs) attaches async frames from the promise before rejecting, equivalent to the old to_error_instance_with_async_stack + reject sequence, and the PR verified stack traces match.
  • transfer_to_js on the OwnedString's inner leaves it Dead, so the subsequent OwnedString::drop deref is a no-op.

The comment-cop lint findings and my earlier stale-comment nit are all resolved. The change is well-tested (fail-before demonstrated for the LSan-visible Bun.write leak; ASAN exercises the string paths) and cross-compiled for Windows/macOS. Still, this is exactly the class of change where a maintainer's eye on the ownership model — particularly the WriteFileWindows field promise + deinit ordering and the recursive-readdir result_list vs ret::Readdir single-owner argument — is worth having before merge.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the second pass. Nothing further to change from it; all review threads are resolved and the branch is at ac8b966, waiting on CI.

@github-actions

Copy link
Copy Markdown
Contributor

This PR has been closed because it was flagged as AI slop.

Many AI-generated PRs are fine, but this one was identified as having one or more of the following issues:

  • Fails to verify the problem actually exists
  • Fails to test that the fix works
  • Makes incorrect assumptions about the codebase
  • Submits changes that are incomplete or misleading

If you believe this was done in error, please leave a comment explaining why.

@github-actions github-actions Bot changed the title Release what a refused or unrun job produced: Bun.write's promise, transform output, readdir and mkdir results ai slop Aug 14, 2026
@github-actions github-actions Bot closed this Aug 14, 2026
@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Understood. For anyone picking this up later: the branch stays at ac8b966; the LSan fail-before output for the Bun.write() leak is in the status comment above, and build 95323 passed on every lane that has finished, including the x64 ASAN test shards that run the new rows.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants