# A05 — SocketPlugin: AIO closeHandler use-after-free in sqSocketDestroy

Bug ref      : always.md A.5 ; pharo.md §3.22
Severity     : HIGH (use-after-free on every async close + immediate destroy)
File         : extracted/plugins/SocketPlugin/src/common/SocketPluginImpl.c
Lines (HEAD) : 1109-1123 (`sqSocketDestroy`), 1040-1091 (`sqSocketCloseConnection`),
               647-663 (`closeHandler`)

## Problem

```c
void sqSocketDestroy(SocketPtr s)
{
  if (!socketValid(s))
    return;

  if (SOCKET(s))
    sqSocketAbortConnection(s);     // may queue closeHandler(.., pss, ..)
  if (PSP(s))
    free(PSP(s));                   // closeHandler may still fire on freed pss

  _PSP(s)= 0;
}
```

`sqSocketAbortConnection` calls `sqSocketCloseConnection`, which in
its asynchronous path registers a `closeHandler` against the fd via
`aioHandle(SOCKET(s), closeHandler, AIO_RWX)` (line 1088). The
handler closure binds the `privateSocketStruct *pss` as the
user-data argument. Control then returns to `sqSocketDestroy`, which
immediately `free(PSP(s))`s that struct. The kernel-level AIO
dispatch later invokes `closeHandler(fd, pss, …)` (line 647) and
writes `pss->sockState`, `pss->s`, `pss->waitingToSend` plus calls
`notify(pss, …)` on freed memory.

## Fix

Definitively unregister the descriptor from AIO before freeing the
struct that AIO will dereference. `aioDisable` is idempotent and
already called in every other path; calling it here is the same
discipline.

```diff
diff --git a/plugins/SocketPlugin/src/common/SocketPluginImpl.c b/plugins/SocketPlugin/src/common/SocketPluginImpl.c
index b7b58ef5c..d46d86145 100644
--- a/plugins/SocketPlugin/src/common/SocketPluginImpl.c
+++ b/plugins/SocketPlugin/src/common/SocketPluginImpl.c
@@ -1116,6 +1116,15 @@ void sqSocketDestroy(SocketPtr s)
   if (SOCKET(s))
     sqSocketAbortConnection(s);		/* close if necessary */
 
+  /* sqSocketAbortConnection may have registered an async closeHandler
+   * with AIO that captures PSP(s) as its user-data argument. Any AIO
+   * dispatch must be torn down before we free PSP(s); otherwise the
+   * handler fires on freed memory. */
+  if (SOCKET(s) > 0) {
+    aioDisable(SOCKET(s));
+    SOCKET(s) = -1;
+  }
+
   if (PSP(s))
     free(PSP(s));			/* release private struct */

```

## Test plan

- Open a TCP connection, write some bytes, call `sqSocketDestroy`
  immediately. Under ASAN, before: heap-use-after-free reported in
  `closeHandler` writing `pss->sockState`. After: no report.
- Stress test: open/close 10⁵ sockets in a tight loop on a busy
  AIO loop; valgrind / ASAN clean.

## Risk notes

- `aioDisable` is safe to call on a descriptor that was never
  registered (it walks the descriptor list looking for the fd).
- `SOCKET(s) = -1` mirrors what the synchronous-close path in
  `sqSocketCloseConnection` does (line 1079) and prevents a stale
  fd from being accidentally reused by the caller.
- The async-close path no longer fires `closeHandler`, so the
  `pss->sockState = Unconnected` and `notify(pss, …)` calls inside
  the handler are skipped. Both are already redundant once
  sqSocketDestroy has freed `pss`.
