Building libhv with -fsanitize=thread on the tsan CI lane (follow-up to the test_writer_connected flake fix) lets TSAN see libhv's synchronization for the first time. Beyond the fixed gmtime race, it surfaces three real cross-thread seams, currently silenced by narrow entries in .github/tsan_suppressions.txt so the lane stays green. Each is pre-existing behavior, not a regression.
1. Keep-alive handler reuse vs deferred (HTTP_STATUS_UNFINISHED) responses
WebServer_Adapter::makeCtxHandler returns HTTP_STATUS_UNFINISHED and fills ctx->response later on the tick thread. libhv's per-connection HttpHandler reuses its request/response objects across keep-alive messages, so the loop thread can parse the next pipelined message (HttpHandler::onMessageComplete reads handler/response state) while the tick thread still writes the previous response (status_code writes in makeCtxHandler / das_resp_redirect).
Observed as 16 reports (14× makeCtxHandler write vs onMessageComplete read, 2× das_resp_redirect). With a request→response ping-pong client this window doesn't open; a pipelining client can genuinely corrupt a response. Suppressed via race:HttpHandler::onMessageComplete.
Fix directions: disable keep-alive pipelining readahead for deferred routes, clone req/resp for the deferred path, or park further reads on the connection until the deferred response is sent.
2. Cross-thread WebSocket sends
das_wss_send / das_wsc_send call hv::WebSocketChannel::send → hio_write from das threads while the channel's loop runs. libhv mutex-guards the write queue (the senders hold M0/M1 in the reports) but hio_write4 reads io state fields outside the guard. 4 reports. Suppressed via race:hio_write4.
Fix direction: marshal ws sends through the channel's loop (runInLoop), same as the writer ops now do.
3. hv::WebSocketClient open/close callback bookkeeping
Client onopen callback state vs WebSocketClient::close() from the das side. 9 reports. Suppressed via race:hv::WebSocketClient::close.
Benign-by-design libhv idioms (also suppressed, no action planned)
hloop_stop writes plain status/wakeup flags read by hloop_run / hloop_process_events (24 reports) — upstream idiom, worst case one extra loop iteration.
hio_is_opened polled cross-thread by das_writer_is_connected — a momentarily stale answer is the API's contract.
Repro: WSL Ubuntu2404-CI rig, tsan Release build, bin/test_aot -use-aot dastest/dastest.das -- --use-aot --failures-only --timeout 900 --test tests/dasHV with the suppressions file removed.
Building libhv with
-fsanitize=threadon the tsan CI lane (follow-up to thetest_writer_connectedflake fix) lets TSAN see libhv's synchronization for the first time. Beyond the fixedgmtimerace, it surfaces three real cross-thread seams, currently silenced by narrow entries in.github/tsan_suppressions.txtso the lane stays green. Each is pre-existing behavior, not a regression.1. Keep-alive handler reuse vs deferred (
HTTP_STATUS_UNFINISHED) responsesWebServer_Adapter::makeCtxHandlerreturnsHTTP_STATUS_UNFINISHEDand fillsctx->responselater on the tick thread. libhv's per-connectionHttpHandlerreuses its request/response objects across keep-alive messages, so the loop thread can parse the next pipelined message (HttpHandler::onMessageCompletereads handler/response state) while the tick thread still writes the previous response (status_codewrites inmakeCtxHandler/das_resp_redirect).Observed as 16 reports (14×
makeCtxHandlerwrite vsonMessageCompleteread, 2×das_resp_redirect). With a request→response ping-pong client this window doesn't open; a pipelining client can genuinely corrupt a response. Suppressed viarace:HttpHandler::onMessageComplete.Fix directions: disable keep-alive pipelining readahead for deferred routes, clone req/resp for the deferred path, or park further reads on the connection until the deferred response is sent.
2. Cross-thread WebSocket sends
das_wss_send/das_wsc_sendcallhv::WebSocketChannel::send → hio_writefrom das threads while the channel's loop runs. libhv mutex-guards the write queue (the senders hold M0/M1 in the reports) buthio_write4reads io state fields outside the guard. 4 reports. Suppressed viarace:hio_write4.Fix direction: marshal ws sends through the channel's loop (
runInLoop), same as the writer ops now do.3.
hv::WebSocketClientopen/close callback bookkeepingClient
onopencallback state vsWebSocketClient::close()from the das side. 9 reports. Suppressed viarace:hv::WebSocketClient::close.Benign-by-design libhv idioms (also suppressed, no action planned)
hloop_stopwrites plain status/wakeup flags read byhloop_run/hloop_process_events(24 reports) — upstream idiom, worst case one extra loop iteration.hio_is_openedpolled cross-thread bydas_writer_is_connected— a momentarily stale answer is the API's contract.Repro: WSL
Ubuntu2404-CIrig, tsan Release build,bin/test_aot -use-aot dastest/dastest.das -- --use-aot --failures-only --timeout 900 --test tests/dasHVwith the suppressions file removed.