fix(socket): don't unprotect never-protected handlers on socket config validation errors - #31360
fix(socket): don't unprotect never-protected handlers on socket config validation errors#31360robobun wants to merge 2 commits into
Conversation
…paths don't unprotect unprotected callbacks
|
Warning Review limit reached
Your plan currently allows 5 reviews/hour. Refill in 47 minutes and 43 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 5:26 PM PT - May 24th, 2026
❌ @robobun, your commit e5d1b59 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 31360That installs a local version of the PR into your bun-31360 --bun |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — straightforward reordering so validation errors return before Handlers is constructed, keeping the Drop → unprotect() invariant intact.
Extended reasoning...
Overview
This PR touches one function, Handlers::from_generated in src/runtime/socket/Handlers.rs, plus adds four regression assertions in test/js/bun/net/socket.test.ts. The change moves callback validation (the is_callable() checks and the data/drain-required check) to before the Handlers struct is constructed, instead of after. The macro body, validation order, and error messages are byte-identical to before — only the sequencing changes so that early return Err(...) paths no longer drop a Handlers value whose Drop impl unconditionally calls unprotect() on never-protect()ed callbacks.
Security risks
None introduced. The change actually closes a release-build hazard where JSValue::unprotect() could be called on values that were never protected, potentially releasing a GC root held elsewhere. No new FFI surface, no user-controlled data handling changes, no auth/crypto involvement.
Level of scrutiny
Medium — this is GC-root lifecycle code in the Rust port of the socket layer, so correctness matters for memory safety. However, the change is a pure mechanical reordering within a single function: the success path is semantically identical (same fields assigned the same values, same with_async_context_if_needed + protect() tail), and the error paths now simply return before any Handlers exists. There are no new control-flow branches and no changes to Drop, protect, or unprotect themselves.
Other factors
- The bug hunting system found no issues.
- Regression tests cover all four error shapes (empty handlers and non-callable handlers, for both
Bun.connectandBun.listen). - No CODEOWNERS entry covers these paths.
- No outstanding human review comments on the PR.
- The PR description clearly explains the root cause (Rust
Dropsemantics diverging from the Zig reference, which never deinits on these error paths) and the fix matches that analysis exactly.
|
Re the duplicate flag: #30948 was an earlier fix of mine for the same fuzzer fingerprint that skipped the unprotect when |
|
CI note: build #57649 had no test failures — all 73 jobs that ran passed. The only red entries were |
There was a problem hiding this comment.
LGTM — validating callbacks before constructing Handlers cleanly closes the drop-before-protect gap, and the success path is byte-for-byte equivalent.
Extended reasoning...
Overview
This PR reorders Handlers::from_generated in src/runtime/socket/Handlers.rs so that callback validation (callable check + data/drain presence check) happens before the Handlers struct is constructed, rather than after. Previously, an early return Err on invalid handlers would drop a partially-built Handlers whose Drop impl calls unprotect() — tripping debug_assert!(protection_count > 0) in debug builds and unbalancing JSC protect counts in release builds. A regression test is added to test/js/bun/net/socket.test.ts.
Security risks
None. This is an input-validation ordering fix on a user-facing error path; it does not touch auth, crypto, permissions, or network I/O. The release-build behavior it fixes (spurious JSValue::unprotect() on never-protected values) was a GC-safety hazard, so the change is strictly a hardening.
Level of scrutiny
Low-to-moderate. The diff is a mechanical hoist: the callback! macro is the old assign_callback! macro reshaped to return the value instead of assigning into result.$field, with identical branching and error messages. Field iteration order is unchanged, so user-visible validation order and messages are preserved. After construction there are no remaining fallible operations before protect() (with_async_context_if_needed is infallible FFI), so the stated invariant — a Handlers only ever exists protected — holds.
Other factors
- Regression tests cover both
Bun.connectandBun.listen, both error variants (missing data/drain, non-callable handler). - No CODEOWNERS entry for these paths.
- The bug-hunting system found no issues.
- The author already closed the weaker alternative fix (#30948) in favor of this approach, and CI "failures" were macOS agent expirations, not test failures.
|
Final status for build #57659: the macOS lanes that had expired eventually ran and passed, so the only red lane is The fix itself is complete and verified. I've already used one CI retrigger for this PR, so I'm not pushing another — a retry of that single lane (or treating it as the known slow-agent timeout) is all that's left. |
|
Superseded by #31817, which fixes the same crash ( Verified on a debug build of #31817 that both of this PR's repro shapes now throw cleanly instead of aborting:
The regression test in #31817 now covers both validation error paths (non-callable handler + missing data/drain), so this PR's test coverage is subsumed. Closing in favor of #31817. |
What does this PR do?
Fixes a crash found by fuzzing (
panic: assertion failed: self.protection_count > 0).Bun.connect()/Bun.listen()with an invalidsockethandlers object — e.g. missing bothdataanddrain, or a non-callable callback — aborted debug/assert builds instead of throwing:Handlers::from_generatedconstructed theHandlersvalue first and validated the callbacks afterwards, returningErrbeforeprotect()was ever called. On that early return the partially-built value is dropped, andDrop for Handlersunconditionally callsunprotect():debug_assert!(self.protection_count > 0)fires and the process aborts.JSValue::unprotect()on callbacks that were never protected, which can release a protection legitimately held elsewhere on the same function objects.The Zig reference (
Handlers.zig) never deinits the partially-built struct on these error paths; the Rust port'sDropintroduced the imbalance.This change validates the callbacks (and the data/drain requirement) before constructing the
Handlersvalue, so aHandlersonly ever exists in itsprotect()ed state and every drop stays balanced. Error messages and validation order are unchanged.How did you verify your code works?
Bun.connect({ socket: {} }), also with a circulartlsobject +Bun.gc(true)) panicked before the fix and now throws the expectedExpected at least "data" or "drain" callbackerror / exits cleanly with a debug build.test/js/bun/net/socket.test.tscovering missing and non-callable handlers for bothBun.connectandBun.listen; they pass withbun bd test.test/js/bun/net/socket.test.tssuite with the debug build: same pass/fail set as the unmodified baked Bun in this environment (remaining failures are pre-existing localECONNREFUSEDissues unrelated to this change).