Skip to content

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

Open

Description

@oxura

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

  1. oxura commented on Oct 9, 2026

    @oxura
    ContributorAuthor

    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.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions