Skip to content

fix: segfault when too many confurrent request were sent on macos#260

Draft
imor wants to merge 3 commits into
masterfrom
rs/fix-segfault-on-mac
Draft

fix: segfault when too many confurrent request were sent on macos#260
imor wants to merge 3 commits into
masterfrom
rs/fix-segfault-on-mac

Conversation

@imor

@imor imor commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Bug fix.

What is the current behavior?

The background worker segfaults under normal concurrent HTTP traffic on macOS/BSD (kqueue). Reproduced with as few as 30 concurrent net.http_get calls — no timeouts or exotic load required, so this is hittable in production under moderate traffic, not just in stress tests.

Because a background worker crash is treated as a backend crash, Postgres kills all other backends and restarts the whole cluster when this happens, so the blast radius extends well beyond the worker itself.

Root cause: in src/event.c's kqueue backend, multi_socket_cb() tracks which kqueue filters (EVFILT_READ/EVFILT_WRITE) are registered for a socket via SocketInfo.action, so it knows what to EV_DELETE once curl signals it's done with that socket (CURL_POLL_REMOVE). That field was only ever set once, at first registration. When curl calls back for the same socket with a different interest — the normal lifecycle of basically every HTTP request (CURL_POLL_OUT while sending, then CURL_POLL_IN while reading the response) — the code took the "socket already known" branch and never updated action, even though it still went ahead and registered the new filter.

The result: on cleanup, only the filter matching the stale action gets removed from the kqueue; the other one leaks, still pointing (via udata) at a SocketInfo struct that gets pfree()'d anyway. When that dangling kqueue registration later fires, get_socket_fd() dereferences the freed memory to get a socket fd, and that garbage value is passed straight into curl_multi_socket_action(), corrupting/crashing inside libcurl's own per-socket hash table.

Curl_hash_pick is libcurl's internal sockhash lookup used by curl_multi_socket_action — exactly what you'd expect from handing curl a corrupted/stale socket reference.

What is the new behavior?

multi_socket_cb() now computes the actual add/remove delta between the previous and new what bitmask on every call, not just at first registration, and keeps sock_info->action continuously in sync with what's actually registered in the kqueue.

This also correctly handles the "downgrade" case (e.g. CURL_POLL_INOUTCURL_POLL_IN), which a naive "just always overwrite action" fix would still get wrong — it would stop tracking the dropped filter without ever issuing the matching EV_DELETE, leaking it the same way.

The Linux/epoll backend in the same file is unaffected by this bug: it doesn't store a real heap pointer as curl's per-socket userdata (it passes the address of a local stack bool used only as a non-NULL existence marker, never dereferenced) and always calls EPOLL_CTL_MOD with the current what bits directly, so there's no stale cached bitmask to go wrong. No behavior change on Linux is expected or intended from this PR.

@steve-chavez

steve-chavez commented Jul 2, 2026

Copy link
Copy Markdown
Member

@imor Can we prefix this PR as refactor: instead of fix:? Two reasons:

  • It doesn't need a release since Macos is not used for production. Otherwise it could be confusing for packagers like supabase/postgres
  • Macos compatibility was only provided for development purposes.

@imor

imor commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@steve-chavez yeah I intend to clean this PR up. I added this as a draft so that I don't forget. I only needed this change to run the tests on my local setup.

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.

2 participants