RFC: switch from select() to poll() on all platforms #36761

issue darosior opened this issue on November 4, 2026
  1. darosior commented at 5:16 PM on November 4, 2026: member

    CVE-2026-117350 prompted me to look into getting rid of select() in favour of using poll() on all platforms.

    The main advantages of doing this would be to get rid of our artificially low file descriptor limit, and to use the same (security-critical) logic on our less-tested platforms as on our main platform.

    This also allows to get rid of some platform-specific quirks, such as Windows requiring that we register the exception set, therefore subscribing us to be notified of TCP urgent data and causing unwanted behaviour. But this point is in my opinion less salient, as the move would likely trade some platform-specific quirks for some others.

    Work to switch over to poll() was started by @pstratem in #14336, which (thankfully) introduced Linux support. MacOS / BSD support was initially part of the PR, but out of an abundance of caution was then left out after @sdaftuar and @jamesob encountered issues in testing an earlier version of the code. Windows support was not included since WSAPoll was malfunctioning.

    Windows has since fixed WSAPoll in May 2020 (Windows 10 version 2004). BSD always worked. And for macOS, the latest investigation i'm aware of is [that of @jamesob](/bitcoin-bitcoin/14336/#issuecomment-437384408) in #14336. As far as i can tell, the diagnostic that knote_fdfind ignores the difference in filter is incorrect, as kev->filter == kn->kn_filter is checked (link to same commit as James' comment). The misbehaviour seems to be instead caused by how the problematic iteration of the PR listed each socket multiple times, and macOS' poll() implementation collides them in the kqueue (i.e. {fd, POLLIN} followed by {fd, POLLERR|POLLHUP} loses the POLLIN) [^1]. I have verified this by running a reproducer on a macOS VM and having an LLM read the sources.

    I have extensively tested a poll implementation, and ran probes, on macOS, Windows and BSD VMs. So i'm fairly confident poll() support isn't broken on any of these platforms (anymore). However...

    The events reported by poll() sometimes vary depending on the platform, and this is a reason why this is an RFC issue and not a slam-dunk PR. If we are going to refactor the poll() logic to support the different events reported on each platform, we should aim for consistent WaitMany reporting across platforms. But what behaviour do we want?

    I think there is two consistent positions:

    • Fail optimistically. Poll events are only hints. On a read EOF, send whatever we have left before disconnecting. On a write error, read whatever was left in the buffer.
    • Fail closed. Disconnect on any reported event that is not plain readiness, as well as on EOF or a read/write error.

    The former avoids unnecessarily dropping a potentially useful message to/from a peer. The latter doesn't, but helps avoid getting tricked into reading when we shouldn't (CVE-2026-117350) or waking but not being able to make progress (#36688). Bitcoin Core does not strictly follow either, so we don't benefit from always reading/writing as much as possible, and have also exposed ourselves to the aforementioned bugs.

    Our knee jerk reaction after the concerning vulnerability may be to just fail closed everywhere, but we should also consider that we now also use our socket implementation for the HTTP server, for which the opposite behaviour may be preferable (for instance it would make sense to respond to a client that sends a request and closes its side of the connection).

    Thoughts?

    To assist this discussion, here are tables from my probe comparing the events reported by select() today versus what poll() would report for each of FreeBSD / macOS / Windows. For select(): R/W/E means readfds/writefds/exceptfds comes back set. For poll(): IN/OUT/HUP/ERR are just the POLL suffixes.

    FreeBSD

    Peer did select() RecvEvent poll() RecvEvent select() SendEvent poll() SendEvent select() RecvEvent|SendEvent poll() RecvEvent|SendEvent
    idle – – W OUT W OUT
    urgent byte E – W OUT WE OUT
    FIN or graceful close R IN W OUT RW IN OUT
    RST R IN HUP W HUP RW IN HUP
    data R IN W OUT RW IN OUT
    data + urgent byte RE IN W OUT RWE IN OUT
    data + FIN or graceful close R IN W OUT RW IN OUT
    data + RST R IN HUP W HUP RW IN HUP
    failed connect R IN HUP W HUP RW IN HUP

    macOS

    Peer did select() RecvEvent poll() RecvEvent select() SendEvent poll() SendEvent select() RecvEvent|SendEvent poll() RecvEvent|SendEvent
    idle – – W OUT W OUT
    urgent byte E – W OUT WE OUT
    FIN or graceful close R IN HUP W OUT RW IN HUP
    RST R IN HUP W HUP RW IN HUP
    data R IN W OUT RW IN OUT
    data + urgent byte RE IN W OUT RWE IN OUT
    data + FIN or graceful close R IN HUP W OUT RW IN HUP
    data + RST R IN HUP W HUP RW IN HUP
    failed connect R IN HUP W HUP RW IN HUP

    Windows

    Note that on Windows POLLIN corresponds to RDNORM | RDBAND. So requesting POLLIN will ask to be woken up for both events, but checking for a reported POLLIN (reported_event & POLLIN) will match when either of the two is set.

    Peer did select() RecvEvent poll() RecvEvent select() SendEvent poll() SendEvent select() RecvEvent|SendEvent poll() RecvEvent|SendEvent
    idle – – W OUT W OUT
    idle; our send buffer full – – – – – –
    urgent byte E IN W OUT WE IN OUT
    urgent byte; our send buffer full E IN – – E IN
    FIN or graceful close R HUP W OUT HUP RW OUT HUP
    FIN; our send buffer full R HUP – HUP R HUP
    RST R ERR HUP W OUT ERR HUP RW OUT ERR HUP
    data R IN W OUT RW IN OUT
    data; our send buffer full R IN – – R IN
    data + urgent byte RE IN W OUT RWE IN OUT
    data + urgent byte; our send buffer full RE IN – – RE IN
    data + FIN or graceful close R IN HUP W OUT HUP RW IN OUT HUP
    data + FIN; our send buffer full R IN HUP – HUP R IN HUP
    data + RST R ERR HUP W OUT ERR HUP RW OUT ERR HUP
    failed connect E – – OUT ERR HUP E OUT ERR HUP

    [^1]: For more details, here is the call chain that would lead to the observed issue: - Each pollfd becomes a kevent tagged with its array index, and POLLHUP counts as read interest: sys_generic.c#L1745-L1758 - The lookup matches on (kqueue, fd, filter), so a second read registration finds the existing filter: kern_event.c#L6808-L6811 - Re-registering overwrites the filter’s tag with the new entry’s index: kern_event.c#L4249-L4253 - The result goes to the tagged entry, masked by that entry’s own events: sys_generic.c#L1855-L1878

  2. bitcoindev1337 commented at 6:55 PM on November 4, 2026: none

    poll/select essentially must be treated as merely a hint

    correctness is important but so is availability

  3. bitcoin blocked a user on Nov 4, 2026
  4. fanquake added the label Brainstorming on Nov 5, 2026
  5. fanquake added the label Net on Nov 5, 2026
  6. vasild commented at 5:16 PM on November 5, 2026: contributor

    So poll(POLLIN) on Windows would wake up and report urgent data? 😒 What about using poll(POLLRDNORM) on all platforms? Would that be consistent to ignore urgent data on all platforms?

  7. darosior commented at 9:01 PM on November 5, 2026: member

    Yes, substituting POLLIN for POLLRDNORM everywhere would work. I think the bigger issue here is that macOS with poll() returns HUP on a graceful close (when RecvEvent was requested). Versus the current behaviour, this means that because OUT is masked we won't send one last message to the peer before disconnecting. That's probably acceptable, but coupled with the change to immediately disconnect on error being discussed (cc @theuni), this means we would drop whatever the peer sent before closing without reading it. That may be acceptable too, though maybe more for p2p than for http, and that's why i opened this issue: it's not clear to me what behaviour we find acceptable, let alone what behaviour we desire.

  8. theuni commented at 10:49 PM on November 5, 2026: member

    Here's what I had in mind: https://github.com/theuni/bitcoin/commits/net-oob-error/

    I implemented that before seeing @darosior's chart. I'll take a look tomorrow and see if my approach aligns with that data.

  9. fanquake added this to the milestone 33.0 on Nov 6, 2026
  10. bitcoin blocked a user on Nov 7, 2026
  11. bitcoin deleted a comment on Nov 7, 2026

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-11-14 18:51 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me