# Extended 4.11 — extracted/vm/src/unix/aio.c: epoll_wait with -1 fd

Bug ref      : pharo.md §4.11
Severity     : MEDIUM (tight error loop, errno pollution)
File         : extracted/vm/src/unix/aio.c
Lines (HEAD) : 290-292 (the call site)

## Problem

```c
epollDescriptor = fillEPollDescriptor();
epollReturn = epoll_wait(epollDescriptor, ...);
```

`fillEPollDescriptor` returns `-1` on `epoll_create1` /
`epoll_ctl` failure. `-1` is not checked before `epoll_wait`,
which then returns `-1` with `EBADF` on every call until the
condition clears. Tight loop in the AIO poll path, errno
pollution, no diagnostics.

## Fix

```diff
diff --git a/src/unix/aio.c b/src/unix/aio.c
index bcf047651..05fdd4342 100644
--- a/src/unix/aio.c
+++ b/src/unix/aio.c
@@ -288,7 +288,16 @@ aio_handle_events(long microSecondsTimeout){
 	isPooling = 1;
 
 	epollDescriptor = fillEPollDescriptor();
-		
+	if (epollDescriptor < 0) {
+		/* epoll_create1 / epoll_ctl failed; skip the poll instead of
+		 * tight-looping epoll_wait(-1, ...) which returns EBADF every
+		 * call and floods errno. */
+		logError("aioPoll: fillEPollDescriptor failed (errno %d), skipping poll", errno);
+		sqLowLevelMFence();
+		isPooling = 0;
+		return 0;
+	}
+
 	epollReturn = epoll_wait(epollDescriptor, incomingEvents, INCOMING_EVENTS_SIZE, milliSecondsTimeout);
 
 	if(epollDescriptor != -1){

```

## Test plan

- Under EMFILE injection (exhaust file descriptors), `epoll_create1`
  fails. Before: tight loop in aioPoll. After: function logs the
  error and returns; the heartbeat path is cleanly exited.

## Risk notes

- `heartbeat_poll_exit(microSecondsTimeout)` must mirror the
  preceding `heartbeat_poll_enter`; verify naming against the
  helper in the same file.
- Logging once per failure can be noisy if the condition persists;
  if logs flood, gate the log behind a one-shot flag.
