Skip to content

fix(io): resume invalid descriptor operations after EBADF - #1456

Open
oxura wants to merge 3 commits into
bendlang:mainfrom
oxura:fix/io-badfd-readiness
Open

oxura wants to merge 3 commits into
bendlang:mainfrom
oxura:fix/io-badfd-readiness

Conversation

@oxura

@oxura oxura commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Current-head review qualification

Head adb45dd5 is rebased on current main d71f42ee and preserves the maintainer's two-filter simplification. The selector invariant comment is shorter, but still names macOS FIFO EOF and _DARWIN_UNLIMITED_SELECT; no executable policy or cap was altered. The current repository gate passes 54 / 54.

Fresh Linux installed-CLI smoke: select_badfd prints exactly EBADF then continued, exit 0, in emitted C, Bun JS and interpreted IO. The valid unwritten descriptor remains parked. select_eintr passes all three paths; spawn_sleep, sleep_timer, udp_loopback and tcp_loopback pass C and Bun against their existing expected outputs. No new macOS or distributed-mini run is claimed. The verification below that mentions ASan and 34 consumer/backend runs is historical qualification from the original head, not a rerun of those lanes on this rebase.

Fixes #1455.

Change

  • Handle readiness EBADF in C and Bun by checking parked descriptors with F_GETFD on that error path only. Invalid descriptors resume their normal effect handlers; valid descriptors remain parked unless their deadlines expire.
  • Preserve EINTR retries and fatal handling of other selector errors. No selector-backend replacement, new retry policy, or normal-path descriptor validation.
  • Keep the existing JS two-filter queue partition, as simplified by the maintainer. Descriptor checks still run only after EBADF, before callbacks execute.
  • Add select_badfd: a real closed UDP descriptor must produce Fail(EBADF), a separate valid unwritten pipe must not wake, and a later timer must complete. The idle pipe is terminated by explicit IO.die, following the existing select_eintr convention.
  • Document the error-path contract in the existing runtime paper.

Actual verification

  • Permanent regression on unchanged baseline: both emitted C and Bun exit 1 with bend: the poller failed; no expected result is produced.
  • Corrected regression: exact stdout EBADF\ncontinued\n, exit 0, in C and Bun. The C consumer also passes ASan + UBSan with four workers; no sanitizer diagnostics.
  • A separate real timed binary receive returns Ready(Fail(EBADF)), rather than Wait, in Bun and ASan + UBSan native C with four workers.
  • 34 / 34 existing consumer/backend runs match the unchanged golden output: 17 programs in emitted C and Bun, covering real interrupted select, timer order, spawned computations, TCP/UDP readiness, byte payloads, EOF, send/receive deadlines, backlog/timer fairness, and channel operations.
  • Repository gate: 54 / 54. comp.ts: 64,000 / 64,000 ttok on rebased head adb45dd5. No cap, gate, allowlist, protected checker, or compiler-splitting change.
  • Updated Typst paper compiles; actual rendered Effects section inspected on page 4. Release changelog left to maintainers.

Boundaries

This is controlled real-OS error injection through a foreign fill, not a valid closed-alias program under affine Socket ownership. Ordinary valid waiters and signal handling are exercised separately above.

Linux x86_64 local verification only. macOS, GPU/mini-cluster gates, and hosted CI are not claimed. Socket readiness uses Bun FFI; no unrelated Node FFI port is included. Draft #1442 remains the separate unqualified #1250 indexing/budget proposal; this patch does not include that redesign.

AI-assisted: omp / GPT-6.1 Sol; working alone without subagents.

@nicolas-abril

Copy link
Copy Markdown
Collaborator

Unsupervised AI review (Claude, run by @nicolas-abril). Nobody has read this by hand yet, so please double-check it.

I pushed 4984a32 on top of this PR. It's a small simplification: the JS io_wait goes back to main's two filter passes. On the EBADF path, due asks fcntl, and a closed descriptor is still closed on the second pass, so the loop didn't need to be rewritten. This saves 13 tokens.

I also looked at moving io_wait to poll(), which reports POLLNVAL for each bad descriptor. That doesn't work here: comp.ts says macOS poll misses FIFO EOF, which is why io_wait uses select. So probing with F_GETFD only after an EBADF looks like the right approach to me.

Checks on macOS: gates/repo.ts 54/54 and gates/test.ts 1598/1598 on this branch's base. Merged onto current main (d71f42e), comp.ts is 64,006 tokens, 6 over the 64k cap. So this still needs a rebase plus 6 more tokens, or the room #1506 frees.

oxura and others added 3 commits October 12, 2026 13:29
…oth times

The EBADF path only changes which waiters are due; a closed descriptor
stays closed between the two filter passes, so the original split of
io.waits needs no rewrite. -13 tokens.
@oxura
oxura force-pushed the fix/io-badfd-readiness branch from 4984a32 to adb45dd Compare October 12, 2026 07:41
@oxura

oxura commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. I preserved your two-filter simplification and rebased the PR onto current main d71f42ee; published head is adb45dd5.

The compiler is now 64,000 / 64,000 ttok and gates/repo.ts passes 54/54. I only tightened the selector invariant comment for the remaining room; the macOS FIFO EOF explanation and _DARWIN_UNLIMITED_SELECT constraint are still there, and no cap was changed.

Fresh Linux proof from a compiled CLI of the rebased head:

  • select_badfd: exact EBADF\ncontinued\n, exit 0, in C, Bun JS and interpreted IO; its separate valid idle pipe does not wake.
  • select_eintr: passes C/Bun/interpreted.
  • spawn_sleep, sleep_timer, udp_loopback, tcp_loopback: pass C and Bun against existing golden output.

I updated the PR body to remove the obsolete single-partition description and separate the fresh rebase proof from the older ASan/34-consumer evidence. No new macOS or distributed-mini run is claimed.

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.

IO: an invalid descriptor aborts the readiness loop instead of returning EBADF

2 participants