Skip to content

fix(ffi,net): recycle reserved socket handle ids; degrade exhaustion instead of panicking (#6441) - #6600

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/6441-net-handle-id-leak-exhaustion
Jul 18, 2026
Merged

fix(ffi,net): recycle reserved socket handle ids; degrade exhaustion instead of panicking (#6441)#6600
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/6441-net-handle-id-leak-exhaustion

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #6441. Follow-up to #6407, which fixed the ext-lib handle-id collision; this addresses the remaining leak + exhaustion panic.

Problem

reserve_handle_id() mints a globally-unique id per ext-net socket, but nothing ever returns it, and next_fresh_handle_id() panic!d on exhaustion — so a long-running server leaks the whole [1, 0x40000) band and crashes after ~262k accepted connections.

The full free-when-unreachable fix is a large, critical-path change (represent net.Socket as a GC heap object, or add a GC handle-band liveness sweep — see the issue). A net.Socket stays inspectable after 'close', so its id's lifetime is the JS object's, not the TCP connection's, and freeing on 'close' alone would just trade the leak for the fast cross-object aliasing #6407 fixes. That rewrite is deferred. This PR lands the two pieces the issue calls out as belonging here now: the shared primitive both candidate fixes need, and the exhaustion de-risk.

Changes

perry-ffi (handle.rs)

  • Add free_handle_id / free_handle_id_until — recycle a reserve_handle_id id through the existing quarantine (one-tick and deadline-gated tiers), mirroring drop_handle / drop_handle_until. A stale bare reference dispatched before the next drain_quarantined_handles tick spends against an empty slot instead of aliasing a freshly reserved id. This is the building block the eventual free-when-unreachable fix wires up; it performs no reachability analysis itself (deliberately), so it is exported but not yet called from a free site.
  • next_fresh_handle_id now returns Option; reserve_handle_id degrades to INVALID_HANDLE on exhaustion instead of aborting. register_handle still panics — it has no valid key to insert under, and its ids recycle via drop_handle, so it isn't the leaker.

perry-ext-net

  • Synchronous entry points (new net.Socket(), createServer, net.connect, new net.BlockList(), new net.SocketAddress()) route through next_id_or_throw, which throws a recoverable, JS-visible EMFILE-coded error on exhaustion (Node's behavior for fd exhaustion) rather than registering a phantom id-0 socket.
  • Background paths (server accept loop, adopt_upgraded_tcp_stream) have no JS frame to unwind to, so they drop the connection instead of registering under the 0 sentinel; the raw-upgrade caller in perry-ext-http-server aborts the upgrade and reclaims the incoming-message handle.
  • Id helpers extracted into handle_ids.rs to keep lib.rs under the file-size gate.

Tests

5 new perry-ffi unit tests: reserved-id recycling decouples fresh-id consumption from cumulative reservations (churn past the band size); the ABA quarantine guarantee for reserved ids; the deadline-gated tier; the INVALID_HANDLE no-op; and the exhaustion boundary (unit-tested via an extracted pure helper, so it doesn't advance the process-wide counter past the band and break other tests).

cargo test -p perry-ffi (17 pass) and cargo test -p perry-ext-net (23 pass) green locally; all three touched crates cargo check clean.

Notes for reviewers

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented invalid network handles from creating phantom sockets or disrupting HTTP upgrade handling.
    • Network resource allocation now reports a recoverable “too many open files” error when capacity is exhausted.
    • Improved recycling of released handles while preventing unsafe immediate reuse.
  • Reliability

    • Accepted connections and upgraded streams are safely discarded when no valid handle is available.
    • Added safeguards for handle exhaustion and cleanup paths.

…instead of panicking (PerryTS#6441)

`reserve_handle_id()` mints a globally-unique id per ext-net socket
(follow-up to PerryTS#6407's collision fix), but nothing ever returned it and
`next_fresh_handle_id()` `panic!`d on exhaustion — so a long-running
server leaked the whole `[1, 0x40000)` band and crashed after ~262k
accepted connections.

perry-ffi:
- Add `free_handle_id` / `free_handle_id_until`: recycle a
  `reserve_handle_id` id through the existing quarantine (one-tick and
  deadline-gated tiers), so a stale bare reference dispatched before the
  next `drain_quarantined_handles` tick spends against an empty slot
  instead of aliasing a freshly reserved id (the ABA class PerryTS#6407 fixes).
  This is the primitive both candidate free-when-unreachable fixes build
  on; it does no reachability analysis itself (a `net.Socket` id outlives
  its `'close'`), so wiring the actual free is deferred.
- `next_fresh_handle_id` now returns `Option`; `reserve_handle_id`
  degrades to `INVALID_HANDLE` on exhaustion instead of aborting the
  process. `register_handle` still panics (it has no valid key to insert
  under, and its ids recycle via `drop_handle` so it does not leak).

perry-ext-net:
- Synchronous entry points (`new net.Socket()`, `createServer`,
  `net.connect`, `new net.BlockList()`, `new net.SocketAddress()`) route
  through `next_id_or_throw`, which throws a recoverable `EMFILE`-coded
  error on exhaustion instead of registering a phantom id-0 socket.
- Background paths (accept loop, `adopt_upgraded_tcp_stream`) can't unwind
  to a JS frame, so they drop the connection rather than register under
  the `0` sentinel; the raw-upgrade caller aborts the upgrade and reclaims
  the incoming-message handle.
- Extract the id helpers into `handle_ids.rs` to keep `lib.rs` under the
  file-size gate.

Tests: 5 new perry-ffi unit tests pin reserved-id recycling, the ABA
quarantine guarantee, the deadline-gated tier, the invalid-handle no-op,
and the exhaustion boundary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 96547fc7-6eeb-4765-b483-e046d8a72f6e

📥 Commits

Reviewing files that changed from the base of the PR and between b389a58 and ab803b4.

📒 Files selected for processing (7)
  • crates/perry-ext-http-server/src/raw_upgrade.rs
  • crates/perry-ext-net/src/adopt.rs
  • crates/perry-ext-net/src/classes.rs
  • crates/perry-ext-net/src/handle_ids.rs
  • crates/perry-ext-net/src/lib.rs
  • crates/perry-ffi/src/handle.rs
  • crates/perry-ffi/src/lib.rs

📝 Walkthrough

Walkthrough

Handle allocation now distinguishes recoverable exhaustion from throwing allocation, recycles reserved ids through quarantine, and prevents invalid socket or upgrade dispatch state when allocation fails.

Changes

Handle exhaustion and socket allocation

Layer / File(s) Summary
FFI handle recycling and exhaustion
crates/perry-ffi/src/handle.rs, crates/perry-ffi/src/lib.rs
Reserved-id exhaustion returns INVALID_HANDLE, freed ids can be quarantined until a deadline, and recycling behavior is covered by tests.
Networking handle allocation
crates/perry-ext-net/src/handle_ids.rs, crates/perry-ext-net/src/lib.rs, crates/perry-ext-net/src/classes.rs
Networking constructors and entry points use next_id_or_throw(), while accept-loop allocation continues with next_id() and drops streams on exhaustion.
Invalid socket adoption cleanup
crates/perry-ext-net/src/adopt.rs, crates/perry-ext-http-server/src/raw_upgrade.rs
Upgraded streams and incoming-message handles are dropped when socket-id allocation returns INVALID_HANDLE.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: andrewtdiz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main change: recycling reserved handle IDs and degrading exhaustion instead of panicking.
Description check ✅ Passed The description includes a summary, concrete changes, tests, and issue reference; only some non-critical template sections are omitted.
Linked Issues check ✅ Passed The PR implements the requested reserved-ID recycling primitive and recoverable exhaustion behavior, while correctly deferring the larger GC-based fix.
Out of Scope Changes check ✅ Passed The code changes stay focused on handle-ID recycling, exhaustion handling, and related cleanup paths; no unrelated changes are evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@proggeramlug
proggeramlug merged commit db10112 into PerryTS:main Jul 18, 2026
23 of 25 checks passed
@proggeramlug
proggeramlug deleted the fix/6441-net-handle-id-leak-exhaustion branch July 18, 2026 18:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

net: reserved socket handle ids leak (reserve_handle_id never freed); exhaustion panics

1 participant