Browse Source

G-036 round 3: ROOT CAUSE - bundled tcludp 1.0.12 closes process-global events at thread exit

Live CloseHandle breakpoint (WinDbgX conditional bp on the sockListLock
handle value) caught the closer: Tcl thread-exit-handler walk in a dying
Thread-extension worker, tail-calling CloseHandle. The tcludp fossil diff
1.0.12 -> current names it: 1.0.12 Windows UDP_ExitProc ("called at thread
exit") closes the process-global waitForSock and sockListLock events, so the
first udp-loaded thread to die destroys the lock for every other udp-loaded
thread. Recycled handle values turn subsequent INFINITE waits into permanent
blocks (unrecycled values fail fast - why standalone probes stayed green
while punkshell run-2 startup churn wedged 4/4). Upstream already fixed in
1.0.13/trunk (UdpThreadExitProc per-thread + ExitSockets at process exit).
Remedy: upgrade bundled tcludp to 1.0.13+; loose ends recorded (punksys 8.6
udp version check, residual trunk weaknesses as optional upstream tickets).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
3ed9e2af6e
  1. 55
      goals/G-036-tcl9-udp-console-worker-wedge.md

55
goals/G-036-tcl9-udp-console-worker-wedge.md

@ -215,6 +215,61 @@ live shell. Next candidates:
Round-2 artifacts: %TEMP%/punk_wedge2.dmp + _stacks/_disasm/_h2 logs. Round-2 artifacts: %TEMP%/punk_wedge2.dmp + _stacks/_disasm/_h2 logs.
## Findings round 3: ROOT CAUSE - tcludp 1.0.12 UDP_ExitProc closes process-global events at thread exit (2026-07-08)
Live-attach WinDbgX with a conditional breakpoint on KERNELBASE!CloseHandle for the
sockListLock handle value (read from the tcludp statics at attach; trap script listing in
scratchpad/notes) caught the closer red-handed: the CloseHandle is invoked from Tcl's
thread-exit-handler walker in a dying Thread-extension worker, with no udp frame visible
(tail-call). The fossil diff `tcludp-1_0_12 -> current` names it exactly - **tcludp 1.0.12's
Windows per-thread exit handler**:
/* UDP_ExitProc - called at thread exit */
void UDP_ExitProc(ClientData clientData) {
Tcl_DeleteEventSource(UDP_SetupProc, UDP_CheckProc, NULL);
CloseHandle(waitForSock); <- process-global
CloseHandle(sockListLock); <- process-global
}
So in the bundled tcludp 1.0.12, the FIRST udp-loaded thread to exit closes the process-wide
lock/wake events. Threads already waiting keep waiting on the orphaned object forever (no
handle left to signal it); later waiters wait on whatever kernel object RECYCLED the handle
value (the round-2 dump's IoCompletion) and block forever; if the value happens to be
unrecycled, WaitForSingleObject fails fast and execution limps on silently - which is exactly
why the standalone die-worker probes stayed green (little handle churn after the close) while
punkshell's run-2 startup (heavy handle churn immediately after the transition-time worker
death) wedged 4/4.
Full causal account of the original symptom:
1. Piped-stdin run 1 creates a syslog log worker (udp loaded, socket opened).
2. At the run-1 -> run-2 transition, punkshell's between-runs teardown terminates that worker
(pre-fix via unsubscribe/shutdown path; post-fix shutdown_free_threads still terminates
free workers between runs) -> UDP_ExitProc closes the globals.
3. Run-2's syslog workers load udp (event-source procs registered), their first event-loop
passes block forever in WaitForSingleObject(sockListLock) on a recycled handle.
4. exit/quit's synchronous teardown send to the wedged worker froze the shell (mitigated in
shellthread 1.6.3 / shellfilter 0.2.4).
**Upstream status: already fixed.** The 1.0.12 -> 1.0.13/trunk rewrite (tcludp-1-0-13 tagged
2026-06-27; "Code Not Thread-Safe" ticket closed 2026-07-05) replaces UDP_ExitProc with
UdpThreadExitProc (per-thread: deletes the event source only) + ExitSockets (process exit
handler: closes the events). No upstream report needed for the killer bug.
**Remedy for punkshell: upgrade the bundled tcludp to 1.0.13+** (kits bundle tcl9udp1012.dll
= 1.0.12). Re-verify with the in-context batch harness after the upgrade (baseline: punk902z
run-2 syslog workers wedged 4/4 -> expect 4/4 alive).
Loose ends / optional follow-ups:
- Check which tcludp the punksys (8.6) kit bundles and why 8.6 appeared immune (different udp
version, different exit-handler ordering, or fast-fail luck).
- Residual weaknesses still present in tcludp trunk, candidates for polite upstream tickets:
auto-reset-event-used-as-lock (lost-SetEvent risk, no ownership); packetNum not decremented
on the recvfrom error path (ICMP port-unreachable leaves packetNum>0 -> permanent
Tcl_SetMaxBlockTime({0,0}) poll mode for the owning thread and the socket excluded from
select forever); get_tag_config-style INFINITE waits with no watchdog.
- The latent hazard class in punkshell remains mitigated (teardown never blocks on a wedged
worker) regardless of the upgrade.
## Alternatives considered ## Alternatives considered
- Mitigate-only (make the sync send robust, default syslog off) - rejected as the *whole* - Mitigate-only (make the sync send robust, default syslog off) - rejected as the *whole*

Loading…
Cancel
Save