Fix double-free of path in S3Client static methods on error#29110
Fix double-free of path in S3Client static methods on error#29110robobun wants to merge 2 commits into
Conversation
When S3Client static methods (presign, unlink, write, size, exists, stat) are called with a string path, constructS3FileInternalStore takes ownership of the path and stores it in the blob. If the subsequent operation throws, defer blob.deinit() releases the path, then the outer errdefer path_or_blob.path.deinit() releases it again, tripping the refcount assert in WTFStringImpl.deref. Clear path_or_blob after ownership transfers so the errdefer becomes a no-op.
|
Updated 3:10 AM PT - Apr 10th, 2026
❌ @autofix-ci[bot], your commit bd99738 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 29110That installs a local version of the PR into your bun-29110 --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 (2)
WalkthroughThis change modifies S3File.zig to explicitly reassign 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 |
There was a problem hiding this comment.
LGTM — clean targeted fix for the double-free with good test coverage.
Extended reasoning...
Overview
This PR fixes a memory safety crash in 6 S3Client static methods (, , , , , ). Each function has an guard, but after succeeds the blob's S3 store takes ownership of the same . If the subsequent operation throws, both the blob's and the outer run, causing a double-free / debug assert failure.
Fix correctness
The fix — resetting to immediately after the store is constructed — neutralizes the without touching the blob or its store. carries no allocation, so on it is a no-op. This is a minimal, idiomatic pattern in Zig for transferring ownership out from under an errdefer.
Security risks
No new security surface. The change only affects error-path cleanup; the happy path and all credential/auth logic are untouched.
Level of scrutiny
Low-to-medium. The change is a 1-line addition per function, mechanically applied to 6 call sites. The root cause is clearly documented and the fix is straightforward. A new test subprocess validates the error-path behavior without requiring live S3 credentials.
Other factors
- Pre-existing minor issue (wrong error message "to get size" vs "to get stat" in the function) is flagged as an inline comment; it does not affect correctness.
- No CODEOWNER-flagged paths are touched.
- CI build was triggered at the time of review.
| return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{}); | ||
| } | ||
| var blob = try constructS3FileInternalStore(globalThis, path.path, options); | ||
| path_or_blob = .{ .path = .{ .path = .{ .string = bun.PathString.empty } } }; | ||
| defer blob.deinit(); | ||
|
|
||
| return S3BlobStatTask.stat(globalThis, &blob); |
There was a problem hiding this comment.
🟣 The stat() function contains two error messages copy-pasted from size() that say "Expected a S3 or path to get size" instead of "Expected a S3 or path to get stat". This is a pre-existing issue, but since this PR directly modifies the stat() function, it's a good opportunity to fix these misleading messages.
Extended reasoning...
What the bug is: In the stat() function in src/bun.js/webcore/S3File.zig, both input-validation error messages were copy-pasted from the size() function and still read "Expected a S3 or path to get size" instead of "to get stat".
Specific code paths: There are two affected locations in the stat() function:
- The blob-type guard check near line 572:
return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{});— triggered when the caller passes a non-S3 blob. - The
.fdcheck inside the.pathbranch near line 579:return globalThis.throwInvalidArguments("Expected a S3 or path to get size", .{});— triggered when the caller passes a file descriptor instead of a path.
Why existing code doesn't prevent it: Both messages are valid Zig string literals that compile without error; the incorrect wording is invisible to the compiler. The size() function at lines ~186 and ~193 has the same two-guard pattern with the correct "to get size" wording, and those strings were simply cloned when stat() was written without updating the verb.
Impact: Any user who calls S3Client.stat(nonS3Blob) or S3Client.stat(fd) receives the error message "Expected a S3 or path to get size", which is confusing because they called stat(), not size(). This makes debugging harder but does not affect correctness — the error is still thrown and the function still rejects invalid input.
How to fix it: Change both occurrences in stat() from "Expected a S3 or path to get size" to "Expected a S3 or path to get stat".
Step-by-step proof:
- A user calls
Bun.S3Client.stat(new Blob(["hello"]))(a plain Blob, not an S3Blob). PathOrBlob.fromJSNoCopyreturns.blobsince it is a Blob.- The guard
if (path_or_blob == .blob and (path_or_blob.blob.store == null or path_or_blob.blob.store.?.data \!= .s3))is true. - The function throws with message "Expected a S3 or path to get size".
- The user is confused — they called
stat(), notsize(), and the error says "size".
What does this PR do?
Fixes a crash (debug assert / double-free) in
Bun.S3Clientstatic methods (presign,unlink,write,size,exists,stat) when called with a string path and the subsequent operation throws.How did you verify your code works?
ERR_S3_MISSING_CREDENTIALSas expected.test/js/bun/s3/s3-static-path-error.test.ts.Root cause
constructS3FileInternalStorestores thePathLikedirectly in the blob's S3 store without adding a ref (despite the "this actually protects/refs the pathlike" comment,PathLike.toThreadSafedoes not add a ref in the common case). So once the blob is constructed, it owns the path.If the subsequent call (e.g.
getPresignUrlFrom) throws:defer blob.deinit()runs → store deinit →pathlike.deinit()→ deref (refcount → 0)errdefer path_or_blob.path.deinit()runs → deref again →bun.assert(self.hasAtLeastOneRef())failsFix: after
constructS3FileInternalStoresucceeds, resetpath_or_blobto an emptyPathStringso the errdefer becomes a no-op.Fuzzilli fingerprint:
641d23ceb14c14b2