Fix double-deref of path when S3Client.presign throws#29279
Conversation
When S3Client.presign() (and related static/instance methods) throw after the temporary S3 blob has been constructed, the errdefer cleaning up the path double-derefed the underlying WTFStringImpl. Ownership of the path had already transferred to the blob's store via initS3(), so blob.deinit()/detach() releases it; the errdefer then derefs a second time, tripping hasAtLeastOneRef() in debug builds (SIGFPE in release). After the blob is successfully constructed, clear the local path so the errdefer becomes a no-op, matching the pattern already used in Blob.findOrCreateFileFromPath.
|
Updated 7:35 PM PT - Apr 13th, 2026
❌ @autofix-ci[bot], your commit 886b1cf has 2 failures in
🧪 To try this PR locally: bunx bun-pr 29279That installs a local version of the PR into your bun-29279 --bun |
|
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 (3)
WalkthroughModified S3Client and S3File implementations to reset path variables to empty after constructing S3-backed blobs, preventing stale path retention. Added test verifying S3 presign methods throw errors when credentials are missing. Changes
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Duplicate of #28495, which already applies the same fix to both Fuzzer fingerprint |
| const arguments = callframe.arguments_old(2).slice(); | ||
| var args = jsc.CallFrame.ArgumentsSlice.init(globalThis.bunVM(), arguments); | ||
| defer args.deinit(); | ||
| const path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { | ||
| var path: jsc.Node.PathLike = try jsc.Node.PathLike.fromJS(globalThis, &args) orelse { | ||
| if (args.len() == 0) { | ||
| return globalThis.ERR(.MISSING_ARGS, "Expected a path to presign", .{}).throw(); | ||
| } |
There was a problem hiding this comment.
🔴 The S3Client.file() instance method was missed by this PR's double-deref fix — it still uses const path with errdefer path.deinit() and calls constructS3FileWithS3CredentialsAndOptions(), which can throw after initS3 has already consumed the path's WTFStringImpl reference, triggering the same use-after-free crash. Apply the same fix as the sibling methods: change const path to var path and add path = .{ .string = bun.PathString.empty }; after the successful Blob.new(...) call.
Extended reasoning...
What the bug is and how it manifests
The PR correctly identifies and fixes the double-deref pattern in six instance methods of S3Client (presign, exists, size, stat, write, unlink) and six static functions in S3File.zig. However, the file() instance method at S3Client.zig lines 128–142 was left unfixed. It still declares const path: jsc.Node.PathLike with errdefer path.deinit() in scope, and calls constructS3FileWithS3CredentialsAndOptions() without clearing path after construction — the exact pattern this PR set out to eliminate.
The specific code path that triggers it
constructS3FileWithS3CredentialsAndOptions() does the following in order:
- Calls
bun.handleOom(Blob.Store.initS3(path, ...))orinitS3WithReferencedCredentials(path, ...). Both functions copy thePathLikestruct by value and calltoThreadSafe()on the copy, which may callorig.deref()on the originalWTFStringImpl(when a new thread-safe impl is created). At this point the caller'spathis stale. - Installs
errdefer store.deinit(). - Calls
try opts.getTruthyComptime(globalObject, "type")— can throw aJSError(e.g., if a getter throws). - Calls
try file_type.toSlice(globalObject, ...)— also can throw aJSError.
Why existing code doesn't prevent it
bun.handleOom cannot return an error, so step 1 always completes and ownership of the path reference transfers to the store before the try-able operations in steps 3–4 run. The PR fixed all other methods by clearing path after step 1, but missed file(). Because path is declared const, the clearing assignment cannot be written without first changing it to var.
What the impact would be
If step 3 or 4 throws a JSError, the inner errdefer store.deinit() runs (decrementing and freeing the path ref held by the store), then file()'s errdefer path.deinit() runs on the already-freed WTFStringImpl — a use-after-free. In debug builds this trips hasAtLeastOneRef(); in release builds it is a SIGFPE or silent heap corruption, exactly as described in the PR.
How to fix it
Same pattern as every other fixed method:
- Change
const path: jsc.Node.PathLike→var path: jsc.Node.PathLikeat line 132. - Add
path = .{ .string = bun.PathString.empty };immediately after the successfulBlob.new(...)call (beforereturn blob.toJS(globalThis)).
Step-by-step proof
- User calls
new Bun.S3Client({}).file(jsStringVar, { get type() { throw new Error('oops') } })wherejsStringVaris backed by a non-thread-safe JS string (a WTFStringImplwith refcount 1). file()parses the path →pathholds aslice_with_underlying_stringPathLike referencing thatStringImpl(refcount 2).constructS3FileWithS3CredentialsAndOptions()is called;getCredentialsWithOptionssucceeds.Blob.Store.initS3(path, ...)copiespath, callstoThreadSafe()on the copy → allocates a new thread-safeStringImpl, callsorig.deref()→StringImplrefcount drops to 1. The store now holds the new ref. The caller'spathpoints to the now-stale original.errdefer store.deinit()is armed insideconstructS3FileWithS3CredentialsAndOptions.opts.getTruthyComptime(globalObject, "type")invokes the JS getter → throws →JSErrorpropagates.errdefer store.deinit()fires →StringImplrefcount → 0 → freed.- Error propagates back to
file()→errdefer path.deinit()fires → callsderef()on the already-freedStringImpl→ use-after-free / crash.
What
Bun.S3Client.presign("foo")(and the other static/instance helpers that construct a temporary S3 blob from a path) crashed withreached unreachable codein debug builds and SIGFPE in release when the underlyingsignRequestfailed (e.g. missing credentials).Repro:
Why
Blob.Store.initS3consumes the caller'sPathLikereference: it copies the struct by value and callstoThreadSafe()on the copy, which derefs the originalWTFStringImplwhen a new thread-safe impl is created. After construction, the caller'sPathLikeis stale.The static/instance helpers in
S3File.zigandS3Client.zigkept anerrdefer path.deinit()in scope across both construction and the follow-up call (getPresignUrlFrom,writeFileInternal, etc.). When that follow-up call threw,defer blob.deinit()released the store's ref and then the errdefer derefed the same (already-consumed) string a second time, trippinghasAtLeastOneRef().Fix
After the blob is successfully constructed, clear the local path so the errdefer becomes a no-op — the same pattern already used in
Blob.findOrCreateFileFromPath. The errdefer still runs for errors thrown before ownership transfers.Fuzzer fingerprint:
1f40a92dc166c1e8