Skip to content

[fix] Socket: Fix stale indexes and surface close/error reliably in _hx_std_socket_poll_events - #1252

Closed
dimensionscape wants to merge 6 commits into
HaxeFoundation:masterfrom
dimensionscape:socket-fixes
Closed

dimensionscape wants to merge 6 commits into
HaxeFoundation:masterfrom
dimensionscape:socket-fixes

Conversation

@dimensionscape

Copy link
Copy Markdown
Contributor

This PR contains two related fixes in _hx_std_socket_poll_events to make
polling safer and more consistent:

  1. Clear ridx/widx on entry and early return
    Prevents stale values when polling with empty registries or when
    select()/poll() errors out. Previously this could result in phantom
    indexes or poll([]) returning [null].

  2. Surface close/error conditions reliably

    • Windows: now pass and handle an exceptfds set to detect errors
      and connection resets. On SOCKET_ERROR, all current read fds are
      surfaced so userland can clean up.
    • POSIX: include POLLERR | POLLNVAL alongside POLLIN/HUP and
      POLLOUT/HUP so teardown/error conditions are not missed.

Together these changes ensure that:

  • Empty registries return no ready sockets.
  • Closed/reset sockets always show up as "ready" for one tick so
    userland can detect EOF and deregister them.
  • The behavior is consistent across Windows and POSIX backends.

…ents` on error/empty sets

Previously, `_hx_std_socket_poll_events` could exit early on `select()/poll()` 
errors without resetting the ready index arrays. This left stale values 
in `p->ridx/p->widx`, causing incorrect results when the socket set was 
empty or when sockets were removed. In Haxe this manifested as phantom 
entries (e.g. index "1" still marked ready after deregistration, or 
`poll([])` returning `[null]`).

This change ensures `p->ridx[0]` and `p->widx[0]` are always initialized 
to `-1` at function entry, and remain consistent even if `select()/poll()` 
returns an error. With this fix, empty socket sets no longer yield 
spurious ready indexes, and poll() on an empty registry correctly 
returns `[]` without mutating the input array.
Windows: pass/handle exceptfds in select() and on SOCKET_ERROR surface current read fds for cleanup.
POSIX: include POLLERR|POLLNVAL in readiness.
Ensures peer FIN/RST/error is observed by default once-per-frame events(0) loops without scanning or keepalives as it should.
@dimensionscape

Copy link
Copy Markdown
Contributor Author

This is actually a kind of important fix for me personally as my framework is broken without it. If it's helpful, I can demonstrate a sample case that it resolves. @Simn

But, for simplicity, I think we can agree that poll([]) should not be returning [null] and that is a bug.

@tobil4sk

Copy link
Copy Markdown
Member

Would it be feasible to add some tests?

E.g. like here:

function testPoll()

@dimensionscape

Copy link
Copy Markdown
Contributor Author

Yeah I will add tests for it.

The existing testPoll only checks that polling an empty set returns; its
timing assertions are commented out and it ends in Assert.pass(). It
never reads readIndexes or writeIndexes, which is where both bugs in this
branch lived.

testPollReportsReadableSocket covers the first: a listening socket with a
connection waiting to be accepted has to be reported readable.
_hx_std_socket_poll_events could return without reporting it, leaving the
caller to conclude nothing had arrived. Measured before and after the
fix on Windows: readIndexes[0] was -1 with a connection pending, where
select() on the same socket at the same moment saw it.

testPollClearsIndexesBetweenCalls covers the second: the ready lists are
the caller's only result, so they have to describe the poll that just ran
rather than the one before it. Polling with nothing registered makes
select() fail, which is the path that used to return early and leave the
previous call's indexes in place; the test asserts both lists come back
as -1.

Both are bounded retry loops rather than single polls, because readiness
is not required to appear on the first pass -- the assertion is that it
appears at all.
@dimensionscape

Copy link
Copy Markdown
Contributor Author

Finally got around to adding a test for this.

FDSIZE was sizeof(u_int) + n * sizeof(SOCKET). fd_array does not begin at
sizeof(u_int): on 64-bit Windows SOCKET is eight bytes, so fd_set carries
four bytes of padding after fd_count and the array starts at offset 8.
Every memcpy of an fd_set therefore copied four bytes too few, truncating
the first handle by half and leaving the remainder as whatever malloc had
returned.

select() was consequently handed something that was not a socket and
failed with WSAENOTSOCK on every call. Confirmed by instrumenting the
error branch:

  [poll] SOCKET_ERROR wsa=10038 rcount=0 wcount=1

The read path hid it. On select() failure the error branch marks every
registered read socket ready, so poll appeared to work for reads while
reporting readiness it had not measured -- an idle connection with
nothing to read came back readable, and a caller woke to read nothing for
as long as it stayed idle. Writes have no such fallback, so they simply
never reported at all.

Sizing the copy by offsetof(fd_set, fd_array) fixes all three: writes are
reported, reads are reported because select actually said so, and an idle
socket is correctly not ready. 32-bit was unaffected, where SOCKET is
four bytes and no padding exists.

Two tests come with it. testPollReportsWritableSocket covers the case
that never worked. testPollDoesNotReportIdleSocketAsReadable covers the
false positive, and is the one that distinguishes a working select from
the error path pretending: it fails whenever poll answers from the
fallback rather than from the call.

@tobil4sk tobil4sk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the last commit also related to the first two fixes described in the PR text?

It is getting a bit difficult to track which fix is which and which tests correspond to which fix, so maybe this would benefit from being split into separate PRs or at least, rebased as a series of commits like: fix 1, test 1, fix 2, test 2

Comment thread src/hx/libs/std/Socket.cpp
Comment thread test/std/Test.hx Outdated
Comment thread src/hx/libs/std/Socket.cpp
Comment thread src/hx/libs/std/Socket.cpp
@dimensionscape

dimensionscape commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor Author

They’re all in _hx_std_socket_poll_events and depend on each other: the error-path fallback in the second fix is what hid the fd_set sizing bug, so landing them separately would ship a broken state on 64-bit Windows. Each commit message explains its fix and names the tests that cover it, which is more than most PRs to hxcpp get.

This review has been passive aggressive, and the “AI diff comment” remark is the clearest example: speculating in public about how I write my code isn’t a review point. I’m upstreaming a fix for a bug that’s been there since 2016, and I’d like the review to stay on the code. If something is wrong with the fix itself, I’m happy to hear it. If not, a "thank you" would go a long way.

…ests

The four poll tests each built the same listening server and connected
client, and two of them repeated the same poll-until-ready loop. The setup
now lives in LoopbackSockets and the loop in pollUntilReady, so each test
reads as the condition it checks. Behaviour is unchanged.
@tobil4sk

Copy link
Copy Markdown
Member

This review has been passive aggressive

I am sorry the comments came across as passive-aggressive, they were intended as genuine feedback and I am regretful about creating a negative environment here. I appreciate the time you took to contribute this fix (along your other PRs).

I understand it is frustrating when a bug remains unsolved when a PR to fix it is open. I have also had my PRs sit open in this repo for a long time for obvious issues and have also wished that others could be more active about leaving reviews or merging PRs. However, I have found that often when no one feels brave enough to approve or merge my change, then there is more work I can do to increase a reviewer's confidence that my work makes sense and addresses the given problem without introducing further issues. I think my review feedback was fair as these are the changes I would make if I had a branch with some fixes that I wanted to get upstreamed.

These patches may make sense to you because you have been looking at this code. However, for someone who hasn't worked with it as recently or isn't so familiar with it (e.g. me) it would be helpful to have this patchset more organised so it is spelled-out more clearly what the individual problems are and how each commit atomically addresses one of them. Right now there are two fixes mentioned in the PR body, the second of which seems like it might include two independent fixes for different platforms. Fix 3 is not mentioned in the PR body, and if it is for a bug exposed only by fix 2 then this could be made clearer with concise commit messages. You also mentioned a bug with poll([]) returning [null], but it's not obvious to me which test case covers this. Having a concise series of minimal atomic patches and tests that clearly correspond to each other and speak for themselves would help clear things up imo.

Leaving in things like whitespace diffs and code comments that rely on removed context increases the effort required to review and means clean-up work is left for the person merging.

These are my opinions for what is missing here to move it closer to merge. From my experience and judging by the lack of activity from others here I would assume addressing some of these things might make other reviewers more confident too.

@Simn

Simn commented Oct 15, 2026

Copy link
Copy Markdown
Member

FWIW I agree with @tobil4sk. I understand that this kind of review content sometimes makes people roll their eyes, but it's good to be diligent about this.

@dimensionscape

dimensionscape commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

@Simn

FWIW I agree with @tobil4sk. I understand that this kind of review content sometimes makes people roll their eyes, but it's good to be diligent about this.

I'm frustrated, Simon. This PR fixes a broken API and sat for over a year, and the review came down to prose, and to my comments explaining the diff.

If a comment bothers you, edit the branch, run the formatter, and merge it. I upstreamed the fix and added tests when asked. The rest is on you. If there isn't time to review and merge fixes, bring in more maintainers who can or delegate the project to someone who can. On our side, this is our single most important dependency.

hxcpp hasn't had a stable release on haxelib in years. I maintain the most used libraries in the Haxe ecosystem; I don't need a lecture on diligence.

It's fine. OpenFL and Lime are moving off stock hxcpp to https://github.com/openfl/libhxcpp.

@tobil4sk I didn't mean to make this personal. I appreciate everything you contribute to the ecosystem, and I know you're not the reason this stalled. I'm feeling snubbed, though. I don't need fixes upstreamed for my ego; the stagnation is costing us materially, in more ways than one. Projects end up on heavily modified vendored forks, and that breaks the contract between developer and end user: it gets harder to tell whether a bug is in hxcpp or in someone's fork. But what choice do people have? The cost falls on everyone.

@Simn
This comment was marked as a violation of GitHub Acceptable Use Policies
@dimensionscape

dimensionscape commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

I consider it rude to just edit someone's code or comments without discussing it first, unless it's a very obvious formatting change or something along those lines. But you seem to be more interested in chimping out and causing drama, so I won't engage with this further.

I'm not causing drama; I'm frustrated, and I said so. Calling it "chimping out" is what made this personal.

For the record, "Allow edits by maintainers" was enabled on this PR:
image

What does "chimping out" mean?

I looked it up and it's a racist slur. Whether or not that was the intent, it's not something to say to a contributor. I come from a black family, this is disgusting.

image

@dimensionscape

dimensionscape commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

The TL;DR for anyone following along:

image

My own final $0.02:

When I was asked to write tests, I didn't argue. I wrote four regression tests, each with comments explicitly explaining what it tested and descriptive assertion messages. Then I was told the tests were difficult to understand because of duplicated setup code, so I refactored that too. I answered the technical questions, explained my reasoning, and accommodated the feedback. This was a small, interconnected fix, roughly +53/−14 lines with whitespace ignored, not some sprawling architectural change requiring a dozen atomic PRs.

After more than a year, the review was still focused on whitespace, commit organization, and the wording of comments rather than identifying anything actually wrong with the implementation. Simon even returned to criticize a comment explaining why sizeof(u_int) was insufficient for calculating the fd_set size. He argued that it only made sense alongside the removed code, but that's simply incorrect. The comment explains the memory layout, the padding on 64-bit Windows, and precisely how the original calculation caused the failure. It's documenting why the fix is necessary for the reviewer, and so someone doesn't reintroduce a bug that's already been there for ten years!

That's why being lectured about diligence was so ironic. Diligence isn't just demanding perfectly organized commits and aesthetically pleasing comments. It's maintaining the code, understanding the defects, writing meaningful tests, and getting fixes into users' hands. I wasn't refusing review; I repeatedly cooperated with it. Eventually, I expressed my frustration, explained the broader maintenance problem, and closed the PR while leaving the commits available. Instead of addressing that criticism, Simon characterized my frustration with a racist slur as "chimping out" and causing drama. I was criticizing the maintenance process, not attacking him personally. He made it personal.

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.

3 participants