s3: fix path double-deinit when operation throws after store creation - #30567
s3: fix path double-deinit when operation throws after store creation#30567robobun wants to merge 6 commits into
Conversation
Store.initS3/initS3WithReferencedCredentials take ownership of the PathLike via toThreadSafe(), which for .slice_with_underlying_string derefs the source WTFStringImpl when installing an isolated copy. The callers pass the path by value (a shallow copy sharing the same impl), so after the store is created the caller's PathLike points at an impl whose ref has already been transferred. When a subsequent step throws (e.g. Blob.writeFileInternal with data whose string coercion throws, or getPresignUrlFrom with missing credentials), the caller's `errdefer path.deinit()` fires and derefs the impl a second time, tripping the hasAtLeastOneRef() assert in debug builds. Make the S3 construct helpers take `*PathLike` and null it out once the store has consumed it, so the caller's cleanup becomes a no-op. This matches the pattern already used by findOrCreateFileFromPath.
|
Updated 11:38 AM PT - May 12th, 2026
❌ @robobun, your commit 446fdf7 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 30567That installs a local version of the PR into your bun-30567 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughRefactors S3File constructors to accept PathLike pointers and clears input paths after store init. Updates S3Client and Blob call sites to build local PathLike variables with errdefer cleanup and pass pointers. Adds tests for S3 error propagation and credential validation. ChangesS3 Path Pointer Refactoring
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/s3/s3-write-throwing-data.test.ts`:
- Around line 15-43: Add a test that exercises the public Bun.file("s3://...")
wrapper to ensure it propagates coercion/ownership-transfer errors: mirror the
existing S3Client assertions by adding something like expect(() =>
Bun.file("s3://bucket/key", throwing)).toThrow("boom") and a variant with an
array expect(() => Bun.file("s3://bucket/key", [throwing])).toThrow("boom");
reference the Bun.file symbol and reuse the existing throwing helper and opts
from this test file so the failure is asserted on the public wrapper path.
🪄 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: 12cb1579-e6fa-412a-b603-940a4d7d7211
📒 Files selected for processing (1)
test/js/bun/s3/s3-write-throwing-data.test.ts
|
CI status on 446fdf7 (Build #53783) and fb1a44c (Build #53775):
The only hard failure on both runs is the HTTP close-on-abort timeout, a pre-existing Windows flake unrelated to this S3 change. It's also the sole/primary failure on other recent PR builds including #53771, #53760, #53751, and #53740 (all unrelated branches), and was noted on #30495 as having a fix in progress on Already retriggered once (f03a4c2) with the same result — not retriggering again since this flake is hitting ~every recent Windows PR run. |
…itS3
getCredentialsWithOptions reads opts.type before initS3, so a getter that
throws immediately never reaches the ownership-transfer point. Return
undefined on the first read and throw on the second so the exception
fires from the post-initS3 getTruthyComptime("type") call.
|
Closing: this fix targets |
Fuzzer found a
reached unreachable codepanic. Fingerprints:4ada451d887da85a,5308c20fced315a7.Supersedes #30495 — implements the
*PathLikeapproach recommended in that PR's review, which fully closes the gap instead of trading the double-free for a leak on thetype-getter-throws path.Root cause
Store.initS3/initS3WithReferencedCredentialstake ownership of thePathLikeviapath.toThreadSafe(). For.slice_with_underlying_string, that callsBunString__toThreadSafewhich creates an isolatedWTFStringImplcopy and derefs the original. But the caller passed the path by value — a shallow struct copy that still points at the same impl — so the caller'sPathLikeis now left pointing at an impl whose ref has already been transferred away.If a later step in the same call throws — e.g.
Blob.writeFileInternalwhen the data argument'sSymbol.toPrimitivethrows, orgetPresignUrlFromwith missing credentials / invalidexpiresIn, oroptions.typegetter throwing inside the constructor — the caller'serrdefer path.deinit()fires and derefs the impl again, trippingbun.assert(self.hasAtLeastOneRef())inWTFStringImpl.deref.Minimal repro:
(Single-char paths like
"x"don't crash becauseisolatedCopy()returns the cached single-char atom unchanged and no deref happens.)The crash report's displayed trace points at
bun_string_jsc.zig:52/Blob.zig:4343because Bun's panic handler prints the Zig error return trace (whereerror.JSErrorpropagated) rather than the actual stack, so the real panic site in theerrdeferwasn't visible.Fix
Make the S3 construct helpers (
constructS3FileWithS3CredentialsAndOptions,constructS3FileWithS3Credentials, and wrappers) take*PathLikeand set it to an empty.stringimmediately afterinitS3*consumes it. This way:getCredentialsWithOptionsthrows beforeinitS3, the caller's path is intact and itserrdefercleans it up;initS3(either inside the constructor atgetTruthyComptime("type"), or after it returns), the caller's path is already empty so itserrdeferis a no-op, and the store's own cleanup frees the threadsafe copy.This is the same ownership-transfer pattern already used by
findOrCreateFileFromPath.Covers
S3Clientinstance methods (write/presign/exists/size/stat/unlink/file/list), the static equivalents inS3File, andBun.file("s3://...").