Repository navigation
IO: an invalid descriptor aborts the readiness loop instead of returning EBADF #1455
Description
Actual failure
A closed descriptor in a readiness wait makes both the native C and Bun IO loops terminate with:
bend: the poller failed
Exit status is 1. The waiting fallible receive never returns its Result, and unrelated computations stop too. This was reproduced on Linux x86_64 using baseline 8578b7cbf2a7944218b750dcb62bd917babe5994 and actual emitted programs, not a simulated select response.
Controlled OS-fault reproducer
The foreign fill below creates and closes a real UDP descriptor before returning its opaque handle. This intentionally injects an externally invalidated resource; it is not a claim that an ordinary affine Bend program can copy a Socket, close one alias, and keep using another. The actual recvfrom should be allowed to report EBADF through the fallible effect, without treating valid parked descriptors as ready.
probe.bend:
import Base
def Probe.closed() -> IO(Result<&1, &1, U32 & String, Socket>):
import "./closed.c"
import "./closed.js"
def report(r: Result<&1, &1, U32 & String, String & U32 & String>) -> IO(Unit):
match r:
case Done{value}:
IO.print("unexpected datagram")
case Fail{error}:
(code, message) = error
IO.print(String.append("error:", U32.show(code)))
def finish(m: Socket & Result<&1, &1, U32 & String, String & U32 & String>) -> IO(U32):
(kept, r) = m
do IO<U32>:
done : Unit <- report(r)
return 0
def main() -> IO(U32):
do IO<U32>:
s : Socket <- IO.try(Socket, Probe.closed())
m : Socket & Result<&1, &1, U32 & String, String & U32 & String> <- UDP.recv_from(s, 64)
finish(m)
#|error:9
closed.c:
static Term probe_closed_run(Env e, Term* f, IoWork* w) {
int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) return io_fail(e, (u32)errno, NULL);
if (close(fd) < 0) return io_fail(e, (u32)errno, NULL);
return io_done(e, io_hand(fd));
}
static void __attribute__((constructor)) probe_closed_use(void) {
io_eff(CID(Probe.closed), probe_closed_run);
}closed.js:
function probe_closed() {
const sys = io_sys();
const fd = sys.socket(2, 2, 0);
if (fd < 0) return io_fail(sys.errno());
if (sys.close(fd) < 0) return io_fail(sys.errno());
return io_done(fd);
}
io_eff(CID(Probe.closed), probe_closed);Generate each backend with bun bend2/main.ts probe.bend -o probe.c / -o probe.js; compile C with clang -std=c11 -O2 probe.c -lpthread -lm -o probe and run ./probe --threads 1 --gpu off / bun probe.js. The intended observable result is error:9, exit 0; the baseline aborts instead.
Cause and review boundary
Both readiness loops only recover from EINTR. select reports EBADF for the whole set, so the receive handler is bypassed. A narrow fix can query descriptor flags only on that error path, resume the invalid descriptors' handlers, preserve valid waiters and deadlines, and leave other selector errors fatal.
No open PR or issue specifically addressing this fault was found in the paginated check at 2026-10-09T17:54:12.986Z. Draft #1442 is the separate #1250 scheduler-indexing/budget proposal, not this error-path fix.
AI-assisted: omp / GPT-6.1 Sol; working alone without subagents.
Activity
oxura commentedon Oct 9, 2026
Fix proposed in #1456. The permanent real-descriptor regression fails on baseline C and Bun, then produces the exact expected EBADF answer and continued timer output on the corrected runtimes without waking the separate valid idle descriptor. The four-worker native ASan/UBSan consumer and the timed binary error consumer also pass; 34 existing C/Bun program-lane runs preserve their golden output. Repository gate is 54/54, compiler 63,995/64,000 ttok. macOS, GPU/mini-cluster gates, and hosted CI are not claimed.