Skip to content

fix: a WebSocket program hung instead of exiting (and why the UCRT throw crash is still open) - #638

Merged
nbeerbower merged 1 commit into
mainfrom
fix/websocket-exit-hang
Aug 8, 2026
Merged

fix: a WebSocket program hung instead of exiting (and why the UCRT throw crash is still open)#638
nbeerbower merged 1 commit into
mainfrom
fix/websocket-exit-hang

Conversation

@nbeerbower

Copy link
Copy Markdown
Collaborator

Two known Windows issues were on the table. One is fixed; the other is not, and the failed attempt is the more useful half of this PR.

Fixed: a WebSocket program hung instead of exiting

Handing a WebSocketServer to spawn() produced correct output and then left the process alive forever — interpreter and compiled binaries alike.

The service thread loops:

while (!shutdown) lws_service(ctx, 50);

That timeout argument has been ignored since libwebsockets 3.2. Under the libuv event loop MSYS2 builds against, lws_service() blocks until something happens, so setting shutdown was never noticed and the pthread_join() in close() waited forever.

Narrowed by elimination, which is what ruled out the traffic and the shutdown ordering:

program exits?
server + close(), no spawn yes
server + spawn + client round trip no
server + spawn, no client at all no
server + spawn, no await no

Both close paths now call lws_cancel_service() before joining — the documented way to interrupt a blocked lws_service() from another thread. rc=0 over five runs in each backend, against rc=124 (killed) every time before.

This is the same root cause as the connect timeout fixed in 2.10.0. That ignored argument is worth suspecting whenever libwebsockets appears to ignore a deadline — two separate bugs from it so far.

Not fixed: the UCRT throw crash, and why

I got a real backtrace this time:

#0 ntdll!RtlVirtualUnwind
#2 ntdll!RtlUnwindEx
#3 ucrtbase!.intrinsic_setjmpex
#4 hml_throw (builtins_func.c:50)

longjmp unwinds and faults partway through. I tried a third mechanism — calling the CRT's non-ex _setjmp directly through an __asm__("_setjmp") alias, bypassing mingw's redirect to the intrinsic. It passes an isolated 400-frame test and then makes the real runtime worse:

mechanism msvcrt UCRT64 + GCC 16
mingw default setjmp ~1 in 8 8 in 15
Frame = 0 (shipped) 0 in 60 6-13 in 30
__builtin_setjmp/longjmp 0 in 60 25 in 25
CRT non-ex _setjmp(buf, NULL) (works) array_sort 30 in 30

Reverted. Three failures in a row point away from a setjmp spelling being the answer at all: Hemlock's throw longjmps out of native frames — sort comparators, map, filter — and this target's unwinder cannot survive that.

The fix that would work is a propagation convention where native callback sites check a pending-exception flag and return, rather than being jumped out of. That removes the unwinding entirely, and would also fix the scratch-buffer leak the sort code already documents. It is a runtime design change rather than a platform shim, so it is written up in docs/advanced/windows.md rather than attempted here.

Also

CI keeps its bounded WebSocket invocations, now as a regression guard rather than a workaround; the comments say so.

Parity 295/323 with 0 compiler-only failures and compiler suite 47/54 — both unchanged from before this PR (the spread comes from the intermittent throw crash above). gn.hml 52/52.

🤖 Generated with Claude Code

https://claude.ai/code/session_0135hioXD4pvKZUCWmv929sZ

Handing a WebSocketServer to spawn() produced correct output and then left
the process alive forever, in the interpreter and in compiled binaries
alike. The service thread loops

    while (!shutdown) lws_service(ctx, 50);

and that timeout argument has been ignored since libwebsockets 3.2 --
under the libuv event loop MSYS2 builds against, lws_service() blocks until
something happens, so setting shutdown was never noticed and the
pthread_join() in close() waited forever.

It reproduced with no client connected at all, which is what ruled out the
traffic and the shutdown ordering as causes:

    server + close, no spawn ............... exits
    server + spawn + client round trip ..... HANGS
    server + spawn, no client at all ....... HANGS
    server + spawn, no await ............... HANGS

Both close paths now call lws_cancel_service() before joining, the
documented way to interrupt a blocked lws_service() from another thread.
Verified rc=0 over five runs in each backend, against rc=124 (killed) every
time before. Same root cause as the connect timeout fixed in 2.10.0: that
ignored argument is worth suspecting whenever lws appears to ignore a
deadline.

The UCRT throw-from-a-native-callback crash is NOT fixed. A third mechanism
was tried and rejected: calling the CRT's non-ex _setjmp directly through an
__asm__ alias, bypassing mingw's redirect to __intrinsic_setjmpex. It
survives an isolated 400-frame test and then makes the real runtime worse --
array_sort goes from intermittent to 30/30, nested_exceptions to 21/30 --
so it is reverted. docs/advanced/windows.md now carries the UCRT backtrace
(RtlVirtualUnwind <- RtlUnwindEx <- ucrtbase!.intrinsic_setjmpex <-
hml_throw) and measurements for all four mechanisms tried. Three failures
in a row point away from a setjmp spelling: throw longjmps out of native
frames (sort comparators, map, filter) and this target's unwinder cannot
survive that. A pending-exception convention that lets native callback
sites return instead of being jumped out of would remove the unwinding
entirely, and would also fix the scratch-buffer leak the sort code already
documents -- a runtime design change rather than a platform shim.

CI keeps its bounded WebSocket invocations, now as a regression guard
rather than a workaround, with the comments updated to say so.

Parity 295/323 with 0 compiler-only failures and compiler suite 47/54, both
unchanged from before; gn.hml 52/52.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0135hioXD4pvKZUCWmv929sZ
@nbeerbower
nbeerbower merged commit 3dd9981 into main Aug 8, 2026
24 checks passed
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.

1 participant