Skip to content

Fix panic when socket handler validation fails in Bun.listen/Bun.connect - #31819

Closed
robobun wants to merge 1 commit into
mainfrom
farm/fa558331/fix-handlers-unprotect-panic
Closed

Fix panic when socket handler validation fails in Bun.listen/Bun.connect#31819
robobun wants to merge 1 commit into
mainfrom
farm/fa558331/fix-handlers-unprotect-panic

Conversation

@robobun

@robobun robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

What

Fixes a fuzzer-found panic (assertion failed: self.protection_count > 0) in debug builds:

Bun.connect({ hostname: "localhost", port: 1, socket: { open: () => {} } });
// panic: assertion failed: self.protection_count > 0
//   Handlers::unprotect   src/runtime/socket/Handlers.rs:361
//   <Handlers as Drop>::drop

Why

Handlers::from_generated returned validation errors (non-callable callback, missing data/drain callback) after constructing the Handlers value but before calling protect(). Unlike the Zig original (which simply abandons the stack value on the error path), the Rust port gave Handlers a Drop impl that unconditionally calls unprotect(), so those error returns dropped a never-protected Handlers:

  • debug builds trip the protection_count assertion and panic
  • release builds call JSValue::unprotect() on callbacks that were never protected, which unbalances JSC GC protection for any callback object that is protected elsewhere (e.g. the same handler function also used by a live socket)

Fix

Read and validate the callbacks from the generated config into locals first, and only construct the Handlers once no fallible step remains — so every droppable Handlers is protected. Error messages are unchanged.

Audited the other Handlers lifecycle sites (listen/connect moves, reload, upgradeTLS, mark_inactive, listener finalizer): this was the only path that drops an unprotected instance.

Test

Added a regression test to test/js/bun/net/socket.test.ts covering both error paths for Bun.listen and Bun.connect; previously the debug build aborted, now they throw the expected TypeErrors.

Handlers::from_generated returned errors (non-callable callback, missing
data/drain callback) after constructing the Handlers value but before
protect(). Dropping that value ran unprotect() on callbacks that were
never protected: debug builds panic on the protection_count assertion,
and release builds unbalance the JSC GC protection of any callback
object protected elsewhere.

Read and validate the callbacks into locals first so a Handlers only
exists once it can be protected.
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 100 minutes and 10 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56d860fd-220f-403e-8e99-81d554d6aa05

📥 Commits

Reviewing files that changed from the base of the PR and between a3464c6 and 747df35.

📒 Files selected for processing (2)
  • src/runtime/socket/Handlers.rs
  • test/js/bun/net/socket.test.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the claude label Jun 4, 2026
@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 2:17 PM PT - Jun 4th, 2026

@robobun, your commit 747df35 has 1 failures in Build #60364 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31819

That installs a local version of the PR into your bun-31819 executable, so you can run:

bun-31819 --bun

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. net: throw on an invalid socket handler config instead of aborting #31817 - Fixes the same panic in Handlers::from_generated when socket handler validation fails, using the same "validate before construct" approach on the same files

🤖 Generated with Claude Code

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #31817, which was opened earlier and covers both validation error paths with an equivalent fix and a subprocess-based regression test. Closing in favor of that PR.

@robobun robobun closed this Jun 4, 2026

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — clean reorder so validation errors return before Handlers is constructed and droppable; behavior and error messages unchanged, with a regression test.

Extended reasoning...

Overview

This PR fixes a debug-build panic (and a release-build GC protection imbalance) in Handlers::from_generated (src/runtime/socket/Handlers.rs). The old code constructed a Handlers value with zeroed callback fields, then ran a validation macro that could return Err(...) — at which point Rust's Drop for Handlers would call unprotect() on values that were never protect()ed. The fix reads/validates each callback into a local first (read_callback!), checks the data/drain requirement, and only then constructs Handlers and immediately calls with_async_context_if_needed + protect(). After construction there are no remaining fallible steps before protect(), so the Drop invariant (protection_count > 0) always holds.

A regression test in test/js/bun/net/socket.test.ts exercises both error paths (missing data/drain, non-callable callback) for both Bun.listen and Bun.connect.

Security risks

None. This is a pure control-flow reorder within a constructor; no new inputs are accepted, no parsing changes, and error messages are byte-identical to before. The release-mode side effect being fixed (spurious JSValue::unprotect()) was itself a latent memory-safety hazard, so this is strictly a hardening change.

Level of scrutiny

Low-to-moderate. While socket/GC lifecycle code is sensitive in general, this specific diff is a mechanical refactor: the macro body is identical except it evaluates to a value instead of assigning into result.$field, and the data/drain check uses the same .is_empty() predicate on the same values. No semantic change on the success path; the only behavioral change is that the error path no longer drops a half-built Handlers.

Other factors

  • The PR description includes an audit of other Handlers lifecycle sites confirming this was the only unprotected-drop path.
  • with_async_context_if_needed (the only call between construction and protect()) is infallible (FFI returning JSValue, not JsResult), so no new gap is introduced.
  • The bug-hunting system found no issues.
  • No CODEOWNERS coverage for this path.
  • CI build is in progress; tests look correct and self-contained.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant