s3: don't double-free path when presign throws after blob creation#30467
s3: don't double-free path when presign throws after blob creation#30467robobun wants to merge 2 commits into
Conversation
Blob.Store.initS3 takes ownership of the PathLike it receives: toThreadSafe() transfers the underlying StringImpl ref for string-backed paths, and for the other variants the store ends up holding the only handle to the same allocation. The S3Client / S3File callers passed path by value and kept an 'errdefer path.deinit()' active across the subsequent presign()/stat()/write() call, so when signRequest failed (e.g. missing credentials) the store was torn down via blob.detach() and then the caller's errdefer freed the same path a second time. The extra deref under-flowed the StringImpl refcount, which later shows up as a SIGFPE in StringImpl::costDuringGC (division by refCount()==0) or as a 'reached unreachable code' panic in debug builds. Thread *PathLike through the constructS3File* helpers and neuter the caller's copy once the store has taken ownership, so the outer errdefer becomes a no-op on every post-construction error path while still cleaning up if getCredentialsWithOptions throws before the store is built.
|
Updated 6:11 PM PT - May 10th, 2026
❌ @robobun, your commit 6319bc1 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 30467That installs a local version of the PR into your bun-30467 --bun |
WalkthroughRefactor: pass S3 PathLike by pointer through S3File constructors so Blob.Store can take ownership and caller PathLike copies are neutered; update all S3Client and JS call sites accordingly and add a regression test verifying missing-credentials error handling. ChangesS3 PathLike Ownership Refactoring
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
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 `@src/runtime/webcore/S3Client.zig`:
- Around line 157-159: Temporary S3File blobs created via
S3File.constructS3FileWithS3CredentialsAndOptions (and the other
constructS3FileWithS3Credentials* variants) are leaking heap allocations because
blob.detach() only drops the store ref; replace those detach() calls with
blob.deinit() for each temporary blob (e.g., the blob used before calling
S3File.getPresignUrlFrom and the other occurrences listed) so the heap-owned
fields (like content_type) are released; keep the same usage pattern (deinit
after the call or via defer) to preserve async stat/exists/size behavior since
tasks take their own store ref.
🪄 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: 0f41ba7f-24f0-4f1f-966f-6b7d4ddcb287
📒 Files selected for processing (4)
src/runtime/webcore/Blob.zigsrc/runtime/webcore/S3Client.zigsrc/runtime/webcore/S3File.zigtest/js/bun/s3/s3-presign-missing-credentials.test.ts
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
args.deinit() already unprotects .buffer paths via the protectEat bitset; deinitAndUnprotect() here would unprotect a second time on the pre-initS3 error path. deinit() still frees the string-backed variants and matches the other S3 call sites.
|
Superseded by #30495 (same fix against current main, minimal diff, deterministic test). |
Fuzzilli found a flaky SIGFPE in the REPRL loop. The minimal repro is:
What
Blob.Store.initS3takes ownership of thePathLikeit receives:toThreadSafe()transfers the underlyingStringImplref for string-backed paths, and for.encoded_slice/.threadsafe_stringthe store ends up holding the only handle to the same allocation.The
S3Client/S3Fileentry points passedpathby value and kept anerrdefer path.deinit()active across the subsequentgetPresignUrlFrom()/stat()/write()call. When that call threw (e.g.signRequest→error.MissingCredentials), the store's copy was torn down viablob.detach()and the caller'serrdeferfreed the same underlying path a second time.The extra deref under-flows the
StringImplrefcount. On the next collectionStringImpl::costDuringGC()doesdivideRoundedUp(size, refCount())withrefCount() == 0→ SIGFPE. In debug builds it surfaces earlier as areached unreachable codepanic from the throw-scope assertion.Fix
Thread
*PathLikethrough theconstructS3File*helpers and neuter the caller's copy (path.* = .{ .string = bun.PathString.empty }) once the store has taken ownership. The outererrdefer path.deinit()now:getCredentialsWithOptionsthrows before the store is built, andPathLikehas been emptied.This is the same neuter-after-transfer pattern
findOrCreateFileFromPathalready uses atBlob.zig:2119-2121.Test
test/js/bun/s3/s3-presign-missing-credentials.test.tsspawns a subprocess with all AWS/S3 env vars cleared, exercisesBun.s3.presign,new S3Client().presign,S3Client.presign, andBun.file("s3://…").presign(), thenBun.gc(true). On current main the subprocess aborts after the first call; with this patch it printsERR_S3_MISSING_CREDENTIALS×4 and exits 0.Supersedes #28423 with a narrower change that fixes the ownership bug instead of avoiding the blob construction.
Verification (robobun, build #53198 complete): All error-level failures are pre-existing on other PRs and unrelated to S3 path ownership:
test-http-should-emit-close-when-connection-is-aborted.ts(Windows ×3) — fails on 10/10 recent PR builds (#53072–53194).s3-storage-class.test.ts"writer + options on big file"S3Error: UnknownError(macOS 14 aarch64) — pre-existing S3 upload flake in 10/11 recent PR builds; this test exercises multipart upload, not the presign/construct path touched here. Same test passed on retry on macOS 14 x64 in this build.node-http-backpressure-max.test.tstimeout (macOS 14 x64) — HTTP test, also seen in #53107.The new
s3-presign-missing-credentials.test.tspassed on every platform that ran tests. Not re-rolling since the HTTP-close failure is 10/10 deterministic across PRs.