Skip to content

Bind test listeners before spawning the responder thread - #13

Merged
philljj merged 1 commit into
wolfSSL:mainfrom
Frauschi:tsan-test-port-handoff
Jul 28, 2026
Merged

Bind test listeners before spawning the responder thread#13
philljj merged 1 commit into
wolfSSL:mainfrom
Frauschi:tsan-test-port-handoff

Conversation

@Frauschi

Copy link
Copy Markdown
Contributor

What

The mock HTTP responder threads in three tests created their listening socket inside the worker thread and handed the ephemeral port back to the main thread through a plain int in a shared context struct. The main thread picked it up by polling that field in a nanosleep loop.

That handoff has no synchronisation, so it is a data race. ThreadSanitizer reports it and, with halt_on_error=1, aborts the run:

WARNING: ThreadSanitizer: data race
  Write of size 4 by thread T1:   caps_srv_thread   test_scep_roundtrip.c:214
  Previous read of size 4 by main: test_caps_token_matching  test_scep_roundtrip.c:259

This is what has been failing the nightly Sanitizers workflow on main since 2026-07-25. Only the ThreadSanitizer (roundtrips) job is affected; ASan/UBSan and valgrind cannot see data races and stayed green.

Why it surfaced now

The idiom has been in tests/unit/test_http.c for a long time, but the TSAN job only runs -R 'roundtrip|tls_http', so the unit tests carrying it were never TSAN-exercised. 7ac927a copied the pattern into tests/integration/test_scep_roundtrip.c, which does run under TSAN, and the latent defect became a red nightly. The unit tests were racy the whole time.

How

Create the listening socket on the caller's thread and pass the descriptor into the responder through the context struct. The port is then known before pthread_create, so the shared field, the poll loop and the race all go away. As a side effect the tests no longer pay up to 200 x 5 ms of polling ticks.

The new listen_loopback() helper also checks the bind, listen and getsockname return codes. The inline versions in test_est.c ignored all three.

File Responder threads converted
tests/unit/test_http.c 4 (srv_thread, srv_retry_thread, srv_thread_overflow, srv_thread_capture)
tests/unit/test_est.c 1
tests/integration/test_scep_roundtrip.c 1 (the one breaking CI)

Net -140 / +105. The now-unused <time.h> includes are dropped.

Scope

A sweep of every nanosleep in tests/ confirms nothing else is affected:

  • tests/integration/test_tls_http.c already guards its port with WOLFSSL_ATOMIC_LOAD. Correct as it stands, left alone.
  • The sleep in tests/integration/test_est_chunked_robustness.c is an unrelated TCP-segmentation helper, not a port handoff.

No library code changes. Tests only.

@Frauschi Frauschi self-assigned this Jul 27, 2026

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fenrir Automated Review — PR #13

Scan targets checked: wolfcert-bugs, wolfcert-src

No new issues found in the changed files. ✅

@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Jul 27, 2026
The mock HTTP responders in these tests created their listening socket
inside the worker thread and handed the ephemeral port back through a
plain int in a shared context struct, which the main thread then polled
in a nanosleep loop. That handoff has no synchronisation, so it is a data
race. ThreadSanitizer flags it and, with halt_on_error=1, aborts the run.

The nightly Sanitizers workflow started failing on the scep_roundtrip
case because the pattern was copied into an integration test, and the
TSAN job runs the roundtrip tests. The unit tests carrying the same
pattern were never TSAN-exercised, so they hid the same defect.

Create the listening socket on the caller's thread instead and pass the
descriptor into the responder. The port is known before pthread_create,
so the shared field, the poll loop and the race all go away, and the
tests no longer pay the 5 ms polling ticks. The new listen_loopback
helper also checks the bind, listen and getsockname return codes, which
the inline versions did not.

test_tls_http.c already guarded its port with WOLFSSL_ATOMIC_LOAD and is
left alone.
@Frauschi
Frauschi force-pushed the tsan-test-port-handoff branch from e1d1e22 to c57c860 Compare July 28, 2026 15:28
@Frauschi
Frauschi requested a review from philljj July 28, 2026 15:40
@philljj
philljj requested a review from Copilot July 28, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a ThreadSanitizer-reported data race in test-only mock HTTP responder setup by binding loopback listening sockets on the caller thread (before pthread_create) and passing the resulting listen_fd into the responder thread, removing the unsynchronized “port handoff” polling loop.

Changes:

  • Replace racy port handoff + nanosleep polling with pre-bound loopback listeners and explicit listen_fd passing.
  • Add a listen_loopback() helper in each affected test file and start checking bind/listen/getsockname return codes.
  • Remove now-unused <time.h> includes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
tests/unit/test_http.c Pre-binds loopback listeners and passes listen_fd into multiple responder threads; removes port polling.
tests/unit/test_est.c Pre-binds loopback TLS responder listener and removes racy port polling.
tests/integration/test_scep_roundtrip.c Fixes the TSAN-failing SCEP caps responder by pre-binding the listener and removing the racy port handoff.
Comments suppressed due to low confidence (3)

tests/unit/test_http.c:328

  • If pthread_create() fails, sc.listen_fd (opened by listen_loopback()) is leaked. Close it before returning so later tests in the same process don’t accumulate open sockets.
    int port = 0;
    sc.listen_fd = listen_loopback(&port);
    REQUIRE(sc.listen_fd >= 0);
    REQUIRE(pthread_create(&tid, NULL, srv_retry_thread, &sc) == 0);

tests/unit/test_http.c:368

  • If pthread_create() fails, the listen FD opened by listen_loopback() is leaked and left open. Close it on the failure path.
    int port = 0;
    sc.listen_fd = listen_loopback(&port);
    REQUIRE(sc.listen_fd >= 0);
    REQUIRE(pthread_create(&tid, NULL, srv_thread_overflow, &sc) == 0);

tests/unit/test_http.c:436

  • If pthread_create() fails, the listener FD returned by listen_loopback() is leaked. Since REQUIRE() returns from the test function without cleanup, close cc.listen_fd on the failure path.
    int port = 0;
    cc.listen_fd = listen_loopback(&port);
    REQUIRE(cc.listen_fd >= 0);
    REQUIRE(pthread_create(&tid, NULL, srv_thread_capture, &cc) == 0);

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/unit/test_http.c
Comment thread tests/unit/test_est.c
Comment thread tests/integration/test_scep_roundtrip.c
@philljj
philljj merged commit 3dc83ac into wolfSSL:main Jul 28, 2026
21 checks passed
@Frauschi
Frauschi deleted the tsan-test-port-handoff branch July 28, 2026 19:00
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.

5 participants