Repository navigation
fix(ci): build TSan QtMultimedia with uninstrumented host tools - #6123
Conversation
There was a problem hiding this comment.
Issue fit
No linked issue, and GOVERNANCE.md doesn't require one here — a bug fix with a named root cause, not an architectural change. Reviewed against the PR's own stated intent, which it satisfies: the one module that invokes qsb -O is the one module that gets host tools, the libraries keep their instrumentation, and the new ldd line turns "still instrumented" into a build-time assertion rather than a claim.
Context that makes this load-bearing: Dockerfile.tsan last moved in bc4f2b0 (#5842, the Qt 6.12.0 pin), which is what dragged QT_GIT_TAG=v6.12.0 into this image. docker-ci-image.yml publishes only on push: branches: [main], and a failed build doesn't push — so ghcr.io/aethersdr/aethersdr-ci:tsan has been stuck at its pre-6.12 digest since then, and the weekly TSan lane is running a Qt that no longer matches the base image. This PR is the only path to unsticking it. The same property bounds the blast radius: if it's still wrong, the publish fails and :tsan stays where it is rather than being replaced by something broken.
Scope
| File | What it changes | Claimed by the body? | Verdict |
|---|---|---|---|
.github/docker/Dockerfile.tsan |
host_tools variable in the addon-module loop, set only for qtmultimedia; comment block explaining why; new post-install ldd assertion on libQt6Multimedia.so.6 |
Yes, all three | In scope |
One file, one coherent change — everything in the diff is explained by the stated fix. No CHANGELOG.md entry (correct; don't add one). No new public surface. No deleted behavior: the only - line is the done that gained a continuation backslash. No sibling left unfixed — qtshadertools, qtwebsockets and qtserialport don't invoke qsb, and the loop leaves host_tools empty for them.
No socket tests or fake peers added, modified or removed by this PR.
Nits
${QT_ROOT}is correct only by line ordering (line 104, inline). It resolves to the base image's uninstrumented Qt becauseENV QT_ROOT=${QT_TSAN_ROOT}is 41 lines below at line 145. That's right today, but the thing being pointed at is named by a variable the file deliberately overwrites, and nothing detects the mistake — if the re-point block ever moves above the module layer,QT_HOST_PATHsilently becomes the TSan Qt and the hang returns. Capturing it once before the loop, or asserting the host tool is not instrumented, would make it self-documenting.- The
libtsanassertion covers Multimedia only (line 121, inline). The other three module libraries also carry it, and the existingfor m in Multimedia WebSockets SerialPort ShaderToolsloop right above is already the right shape to fold it into.
What I tried to break
Everything below is reasoned from the Dockerfile and workflows in the head checkout; I have no build and could not run the image build or the TSan lane.
- Shell mechanics under Dash.
host_tools="" && if …; then …; fi && …— anifwith a false condition and noelseexits 0 in POSIX sh, so the&&chain survives the three non-multimedia iterations rather than aborting the loop.host_toolsis re-cleared at the top of every iteration, so no leakage fromqtmultimedia(and it's last anyway). Unquoted$host_toolsis the intended word split and neither path contains spaces. All clean. - Host/target version drift.
QT_HOST_PATHrequires the host Qt to match the target version exactly. BaseDockerfile:95setsQT_VERSION=6.12.0andQT_ROOT=/opt/Qt/6.12.0/gcc_64; this file derives bothQT_TSAN_ROOTandQT_GIT_TAG=v${QT_VERSION}from the same variable, so they cannot drift. Clean. - Does the host Qt actually have the tools? With
QT_FORCE_FIND_TOOLS=ON, a missing tool is a configure error, not a silent fallback. BaseDockerfile:106installs-m qtmultimedia qtwebsockets qtserialport qtshadertoolsvia aqt, sobin/qsbplus moc/rcc/uic are present. Noqtdeclarativein either Qt, so multimedia's QML plugin (andqmltyperegistrar/qmlcachegen) never enters the picture. Clean. - Could the assertion pass vacuously?
QT_FORCE_FIND_TOOLSchanges which tools run, not the target'sFEATURE_sanitize_thread, so the libraries keep-fsanitize=threadandlibtsanstays aDT_NEEDED. The check uses the sameldd … grep -q libtsanform as the qtbase layer at line 86, which passes today — so it's a guard with precedent, and it fails the image build loudly if host tools ever did uninstrument the library. - Could not verify: our own shaders under the TSan
qsb. The body's "qt_add_shadersdoesn't useOPTIMIZED" is accurate — I checked both calls atCMakeLists.txt:2519and:2561, neither passesOPTIMIZED, so nospirv-optchild is spawned. But the Dockerfile's trailingPATH=${QT_TSAN_ROOT}/bin:${PATH}meanssanitizers.yml'scmake --build build --parallelbakes our 12 shaders with the TSan-built 6.12qsb— the same binary that hangs — and since the 6.12:tsanimage has never published, that has never happened anywhere. The reasoning that it's fine is sound, not observed. It would fail loud (job timeout attimeout-minutes: 330), not silently green. Did the diagnostic run (37140665591) go on to build the tree, or is watching the first post-merge sanitizers run the plan? - Validation gap, disclosed honestly in the body. The publish path is untested because it only runs on
main; the checks on this SHA are the ordinary CI set and none of them build either Docker image. I confirmed that rather than taking it on trust.
Recommendation
Approve with nits. The root cause is specific, the fix is the narrowest thing that addresses it (one module, build tools only), and it ships its own regression guard rather than asking a reviewer to believe the instrumentation survived. The validation run is cited by ID and the two residual gaps — the publish path and our own shaders under the TSan qsb — are both fail-loud, with a failed publish leaving the existing :tsan tag untouched. Next step for the maintainer: merge and watch the build-tsan job, then the first weekly sanitizers run on the new image. Nice comment block, too — it explains why, with the symptom and the mechanism, which is exactly what the next person debugging this layer will need.
🤖 aethersdr-agent · cost: $3.1266 · model: claude-opus-5
|
The nits are addressed in 7fa8338, with details in the resolved threads. On the open question: no, diagnostic run 37140665591 built only the image, not the AetherSDR tree. So our own 12 shaders under the TSan Current state of that lane, for context: the TSan job in sanitizers run 37141490698 failed at Configure with
|
ten9876
left a comment
There was a problem hiding this comment.
Issue fit: Yes, within the demonstrated build boundary. There is no linked issue; this is a build fix with a specific root cause, so GOVERNANCE.md does not require an RFC. Reviewed 7fa833864 against merge base fe1fec519. The module-specific host-tool flags address the reported qsb -O hang without changing the target-library sanitizer configuration. I independently inspected the successful diagnostic run and tested the current shell guards locally; I did not rebuild the full image.
Scope
| File/group | Change | Claimed in title/body? | Verdict |
|---|---|---|---|
.github/docker/Dockerfile.tsan:94–114 |
Capture plain Qt path, reject missing/instrumented host qsb, select host tools only for Multimedia | Main fix yes; new guard described in follow-up comment | In scope |
.github/docker/Dockerfile.tsan:123–127 |
Check each add-on library's direct libtsan dependency | Follow-up comment; body still describes old ldd check | In scope; refresh body |
Two coherent commits authored on 2026-10-03. Everything in the diff belongs to the stated fix or its review hardening. No source/UI changes, new runtime feature, dependency, public protocol, changelog entry, or weakened baseline. No new or modified socket test, synthetic peer, or test target. No separate scope checklist is present.
Blockers
None.
Nits — non-blocking
-
Fail closed when host-tool inspection fails —
.github/docker/Dockerfile.tsan:105.! readelf ... | grep -q ...returns success whenreadelffails and emits no matching output. Reproduced locally using an executable non-ELF file:readelf: Error: ... Failed to read file header, but the host guard returned 0. Plain and TSan ELF executables were correctly accepted/rejected. This is defensive hardening, not a demonstrated failure with the actual aqt base image. Replace the current RUN opening with:RUN test -x "${QT_HOST_ROOT}/bin/qsb" \ && host_dynamic="$(readelf -d "${QT_HOST_ROOT}/bin/qsb")" \ && ! printf '%s\n' "$host_dynamic" | grep -q 'NEEDED.*libtsan' \ && for m in qtshadertools qtwebsockets qtserialport qtmultimedia; do \
-
Update the description to the final implementation and its evidence boundary —
.github/docker/Dockerfile.tsan:103–127. The PR body still quotes the old Multimedia-onlylddassertion and calls the diagnostic loop identical. Current head capturesQT_HOST_ROOT, adds its host guard, and checks all four direct dependencies withreadelf; run 37140665591 exercised the earlier loop and ldd assertion. The follow-up comment explains this honestly, but fold it into the body so the reviewable description stands alone. The final image/publish path and AetherSDR's own shaders under TSan qsb remain untested integration boundaries. Neitherqt_add_shaderscall usesOPTIMIZED, verified atCMakeLists.txt:2519and:2561; absence of that flag supports the reasoning but does not itself constitute a TSan build run. -
Trim the incident narrative from the code comment —
.github/docker/Dockerfile.tsan:94–102. Keep the invariant (plain host qsb avoids the Qt 6.12 QProcess/spirv-opt hang; target libraries retain TSan; read direct dependencies), and leave “sat at [2/300] until the job timed out” in the PR body. This follows AGENTS.md's comment guidance and is not merge-blocking.
What I tried to break
- Diagnostic evidence: retrieved run 37140665591, which completed successfully at diagnostic SHA
258c88fd. Its log explicitly runs/opt/Qt/6.12.0/gcc_64/bin/qsb ... -Oat[2/300], reaches[300/300], prints the instrumentation assertion success, and exports the image. The log contains 114 Multimedia compile/link commands with-fsanitize=thread. Compared its Dockerfile with current head: diagnostic gdb/watchdog/verbose scaffolding is absent here, and the subsequent guard changes are the differences disclosed above. I did not independently reproduce the original zombie/pipe hang. - Guard mutation checks: compiled actual plain and TSan shared libraries locally, plus a plain library depending on the TSan one. Direct
readelfguard results were 0 / 1 / 1, whilelddreported libtsan for the transitive case. Thus the new check rejects the old false positive. Extracted the exact RUN shell, syntax-checked it, then executed it with clone/build/install/removal stubbed and real ELF files. The successful case configured all four modules and gave host-tool flags only to Multimedia. Replacing each module library individually with a plain one failed withQt6<module> is not TSan-instrumented; missing and instrumented host tools failed before any configure call. This tests shell flow and assertions, not a Qt build. No network peer or socket test was used. - Environment and sibling paths: checked that the host root is captured before the final ENV redirection, the base installs ShaderTools at the same Qt version, loader variables still point at plain Qt during the module layer, and
host_toolsresets for every iteration. Existing clone/configure/build/install failures still hitexit 1. No runtime source or engine boundary changes make the engine ratchets applicable here. - Limits: Docker daemon access is unavailable to this account (
permission denied ... /var/run/docker.sock), so I did not rebuild the full image or dispatch the publishing workflow. No GUI/bridge session: this change is exclusively a headless build-image change, and a demo GUI cannot verify the TSan Qt toolchain. The separate code-review skill was unavailable; completed manual review plus an independent source audit and verified retained findings above.
CI and recommendation
Approve with nits once required CI completes; this is not a merge-ready assertion. Windows, macOS, and Static checks passed; Linux build is still running at review time. CI/Static runs for this head were created at 2026-10-03 19:18:32 UTC; current main 579efd73 is dated 19:25:50 UTC, so the pull_request checks cover an older merge snapshot. strict: false does not force a refresh. Ordinary PR checks do not build Dockerfile.tsan; the image workflow publishes on main push or manual dispatch.
Next steps: finish required CI, clarify the body, optionally harden the negative guard, then monitor the post-merge build-tsan image build and first sanitizer build. No change to the operator's checkout, no image publication, and no approval or merge performed by this review.
The TSan image build hung at QtMultimedia's [2/300] until the job timed out. QtMultimedia compiles its shaders with `qsb -O`, which runs spirv-opt through QProcess; under the TSan-instrumented Qt 6.12 qsb the child exits and the parent blocks reading its pipe forever. Configure qtmultimedia with QT_HOST_PATH pointing at the base image's plain Qt and QT_FORCE_FIND_TOOLS so it runs that qsb instead. The libraries are still instrumented; assert libQt6Multimedia links libtsan. Proven in a diagnostic image build: QtMultimedia built in about 4 minutes using /opt/Qt/6.12.0/gcc_64/bin/qsb, and the libtsan check passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tsan. Review follow-ups: - QT_HOST_ROOT captures the base image's plain Qt before QT_ROOT is re-pointed at the TSan build, and the layer refuses to start if that qsb is missing or instrumented, so reordering the file cannot quietly bring the hang back. - All four add-on modules must carry their own DT_NEEDED libtsan, read with readelf. ldd lists libtsan for anything linked against the TSan QtCore, so the previous check could pass for an uninstrumented library. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`! readelf ... | grep -q` succeeded when readelf itself failed, so an unreadable host qsb passed the "not instrumented" check. Capture the dynamic section first so a failed read stops the layer, and trim the comment to the rule it enforces. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7fa8338 to
1810b81
Compare
|
All three nits are addressed in 1810b81, rebased onto current main (
|
ten9876
left a comment
There was a problem hiding this comment.
PR #6123 — fix(ci): build TSan QtMultimedia with uninstrumented host tools (@ten9876)
Issue fit: Yes within the demonstrated build boundary. No linked issue; the stated problem is a QtMultimedia shader-build hang that prevents refreshing the TSan image. Reviewed head 1810b81b0 against merge base 579efd73f. Selecting plain host tools for Multimedia addresses that failure mechanism; capturing the host root and checking each module’s direct dependency preserve the intended separation. This is a build fix with a clear root cause, not an RFC-sized architecture change. The final image and downstream application build remain unverified integration boundaries, explicitly disclosed by the author.
Scope
| File/group | What changes | Claimed in body? | Verdict |
|---|---|---|---|
.github/docker/Dockerfile.tsan:94–103 |
Capture plain host Qt and reject missing, unreadable or directly TSan-linked qsb | Yes | In scope |
.github/docker/Dockerfile.tsan:104–119 |
Pass host-tool options only while configuring Multimedia | Yes | In scope |
.github/docker/Dockerfile.tsan:121–125 |
Require direct libtsan dependency in each installed add-on | Yes | In scope |
Everything in the diff belongs to the fix or its validation. All three commits were authored October 3; no unrelated old commit, public protocol/API change, UI preference, changelog entry, or weakened enforcement baseline appears. No separate scope checklist is present. No added or modified test source, socket-owning target, or synthetic peer: test-boundary preflight passed before running the local shell/ELF checks. Engine-boundary checks and GUI rules are not implicated by this image-only change.
Blockers
None found.
Nits — explicitly non-blocking
- Final integration evidence remains outstanding —
.github/docker/Dockerfile.tsan:101–125, with downstream consumers atCMakeLists.txt:2519andCMakeLists.txt:2561. The successful diagnostic image used the same host-tool flags but the older Multimedia-onlylddcheck, not the final four-modulereadelfassertions. The application’s own shaders have also not been built using this new TSan image. Both shader calls omitOPTIMIZED, as claimed, but that inspection does not prove the complete downstream build succeeds. The body now describes these limits accurately; retain them and capture the first final image build plus sanitizer application build. No synthetic-peer regression test is appropriate for this boundary.
What I tried to break
- Host guard and failure propagation, executed locally: extracted the current RUN shell verbatim and syntax-checked it. Ran it under
/bin/shwith real plain/TSan ELF executables and shared libraries; only clone/configure/build/install/removal operations were stubbed. Valid input returned 0. Missing, executable non-ELF, and TSan-linked host qsb each returned 1, with zero configure/build operations. Thus the earlier review’s fail-open unreadable-file nit is fixed. Injected clone, configure, build and cleanup failures all returned 1 rather than allowing a later module to hide them. - Module instrumentation guard, executed locally: substituting an uninstrumented library for each of Multimedia, WebSockets, SerialPort and ShaderTools independently returned 1, with the corresponding
Qt6<module> is not TSan-instrumentedmessage. A plain library linked to an instrumented dependency had no direct libtsan entry; the instrumented library did. These are mutation checks of the delivered guard’s behavior, not claims that a mock shell execution builds Qt. Successful trace showed host-tool flags on Multimedia only and a reset for the other three modules. - Diagnostic evidence, independently retrieved: run 37140665591 succeeded at diagnostic SHA
258c88fd. Its log explicitly invokes/opt/Qt/6.12.0/gcc_64/bin/qsb ... -O, reaches Multimedia[300/300], and completes the image build. It also exposes the olderlddassertion, confirming the author’s distinction between diagnostic and final evidence. I did not reproduce the original zombie/pipe hang. - Environment and sibling audit: base Dockerfile installs ShaderTools with the same Qt version used for the source build; host root capture occurs before final ENV redirection, and the module layer still inherits the plain Qt loader environment. No Dockerfile/base-image/workflow changes intervened between this PR’s merge base and fetched current main. Existing module failure exits survive the patch. Prior review findings were rechecked against the final diff instead of carried forward.
- Limits: no full image rebuild: this account cannot access
/var/run/docker.sock. No workflow was dispatched and no image published. No GUI/automation-bridge instance was launched, because a demo session cannot validate this headless Qt toolchain change. The separate code-review skill was unavailable; this was a manual adversarial audit with local executable checks and independent CI-log inspection.
CI and recommendation
Approve with nits, subject to the normal CODEOWNERS/merge gate and the explicitly outstanding integration check above. All four required checks passed for this head. However, CI and Static Checks runs were created 2026-10-03 19:34:52 UTC (the earliest head-associated run was Code Quality at 19:34:50 UTC), while current main 3e4b14e2 is dated 2026-10-04 01:13:20 UTC. These pull_request checks cover the older merge snapshot; branch protection has strict: false, so they are not proof against current main. Ordinary PR CI does not build this Dockerfile.
The host-tool selection has successful diagnostic-image evidence and the final shell guards survived direct failure/mutation checks. Next step is maintainer review, followed by monitoring the final build-tsan image build and first application sanitizer build; a failed image build leaves the existing TSan tag in place. This review grants no GitHub approval and performs no merge.
|
On the last review's nit (final integration evidence): agreed, that's the post-merge check, and it's tracked rather than assumed. |
…n disk. Principle VIII. (#6136) ## Problem The weekly **ASan + UBSan** sanitizer lane has failed in its Build step for about three weeks with `final link failed: No space left on device`, so it has tested nothing in that time. About 250 of the test executables statically link the whole of `libaethercore`. In a Debug ASan build each of those averages about 200 MB, and together they outgrow the runner's ~83 GB of free disk. The runner's `/` and `/mnt` are the same 145 GB disk, so building on `/mnt` doesn't help. Trimming debug info (`-g1` plus compressed debug sections) still measured 127 GB locally. ## Fix - **`CMakeLists.txt`:** a new option, `AETHER_SHARED_CORE` (default **OFF**), builds `libaethercore` as a shared library with position-independent code, because the vendored static libraries end up inside it. On Windows it's a configure error. Release, ordinary and TSan builds keep the static core unchanged. - **`.github/workflows/sanitizers.yml`:** the ASan entry passes `shared_core: "ON"`, and TSan passes `"OFF"`. The Build step prints `df -h /` before and after the build, including when the build fails (it keeps the build status and exits with it after the second reading). - **`.github/workflows/sanitizers.yml`, reporting:** every report step was gated on ctest's exit code, so a Configure or Build failure skipped them all, with no artifact and no tracking issue. That is how the lane went dark. A new step runs only when Configure or Build failed. It writes a report naming the step, with a stable per-step fingerprint, and the artifact and tracking-issue steps also run on that path. - **`.github/workflows/static-checks.yml`:** the "Sanitizer option configures" job gains a fifth case: `-DAETHER_SHARED_CORE=ON` must generate a `libaethercore.so` rule, and the default configure must still generate `libaethercore.a`. That's the per-PR check of the option; the sanitizer lane itself only runs weekly. ## Validation **Locally** (Linux, Debug ASan + UBSan): | | static core | shared core | |---|---|---| | Build tree | 127 GB even at `-g1` + zlib | **17 GB** | | Average test executable | ~200 MB | **13 MB** (`libaethercore.so` 668 MB once) | | Test targets that build and link | — | all 591 | Running the full suite under ASan gave **the same failures with the shared core as with the static core**, so linking doesn't change results. **On the runner** (validation run [37141490698](https://github.com/aethersdr/AetherSDR/actions/runs/37141490698), dispatched from this branch): - **Disk:** `/` went from 62 GB used / 83 GB free to **81 GB used / 64 GB free** after the build. - **Tests:** the suite ran to completion. **618 of 623 tests passed.** All 5 failures are existing issues in the code under test: - `offline_health_registry_test`: an ASan stack-use-after-scope in the test itself (`offline_health_registry_test.cpp:95`). - `rx_applet_squelch_reconciliation_test` and `vfo_dsp_launcher_width_test`: a UBSan downcast in `VfoWidget::eventFilter` (`VfoWidget.cpp:6766`) during `~VfoWidget`. - `hl2_backend_seam_test`: main was red when the run started; #6126 has since fixed it. - `weather_radar_loading_test`: a timeout, which also fails locally. - **TSan job:** it failed at Configure with `Qt 6.8.3 found, but AetherSDR requires Qt 6.12`. That's the stale `:tsan` image, fixed separately by #6123, and unrelated to this change (TSan keeps `shared_core: "OFF"`). **Checks:** the new static-checks case, run locally, passes on this head. It fails with its `::error::` when the ON branch is forced to STATIC, and also when the default is forced to SHARED. The Build step exits 1 when the build fails and 0 otherwise, checked under `bash --posix`. The real check is the next scheduled sanitizer run on `main` after merge: it should reach Run tests under sanitizer instead of dying at Build. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… VIII. (#6152) ## Problem **Full Suite on `main` is red.** Run [37181657060](https://github.com/aethersdr/AetherSDR/actions/runs/37181657060) on `a325fb99e` (#5924) died at link 4934/4976 with `final link failed: No space left on device`. The previous commit's run was green, so the tree is right at the runner's limit. - **ci.yml's required `build` job builds the same tree.** It only survives because #5924 gave it a step that frees 18 GB of host toolchains (84 → 102 GB free). That's about 50 more core-linking tests of headroom before the PR gate hits the same wall. - **The TSan lane ran out of disk too.** With the Qt 6.12 `:tsan` image from #6123 it got far enough to build for the first time, then filled the runner: `overlay 145G 145G 163M 100%` at link 4438/4854 (run [37176807431](https://github.com/aethersdr/AetherSDR/actions/runs/37176807431), #6148). - **The red Full Suite run filed no issue.** When Build fails, ctest is skipped, and a skipped step's output reads as `null`. GitHub's expression `exit != '0'` coerces `null` to 0, so every report step was skipped. Underneath all three: ~250 test executables each link a static `aethercore` and carry their own copy of its debug info. ## Fix - **`ci.yml` build job + `full-suite.yml`:** configure RelWithDebInfo at `-g1` (line tables only), via `CMAKE_{C,CXX}_FLAGS_RELWITHDEBINFO`. Backtraces keep file:line; only debugger-level locals and types are dropped. Both jobs change together because Full Suite restores the build job's ccache, so the first `main` run after merge builds cold and the next is warm again. - **`full-suite.yml`:** the same host-toolchain mounts and "Free runner disk" step as ci.yml's build job, and a `df` after the build, including when it fails. - **`full-suite.yml`:** a "Report a suite that did not build" step runs only when Configure or Build failed. It writes the report with a stable per-step fingerprint, and the artifact and tracking-issue steps also run on that path. Same pattern as #6136. - **`sanitizers.yml`:** TSan's `shared_core` goes to `"ON"`, the change the ASan lane proved in #6136. ## Validation Measured locally (GCC, this tree, `aethercore` plus four core-linking tests): | | `-g` (today) | `-gsplit-dwarf` | **`-g1`** | |---|---|---|---| | core-linking test executable | 325 MB | 214 MB | **120 MB** | | `libaethercore.a` | 496 MB | 165 MB | **114 MB** | At ~250 core-linking executables, that's roughly 50 GB off a tree that needs a little over 84 GB. `actionlint` is clean on `full-suite.yml` and `sanitizers.yml`. It reports two errors in `ci.yml` (`runner.workspace`, lines 451 and 841), which were already on `main` and are untouched here. **On the runner:** - **Full Suite**, branch run [37208804048](https://github.com/aethersdr/AetherSDR/actions/runs/37208804048) on this head, green: - Free runner disk: 84 → 102 GB free. - The build tree used **32 GB** (44 → 76 GB used), leaving 69 GB free. Before, it needed more than 84 GB. - **653/653 tests passed** (13.5 min). - The build took 46 min at a 52% ccache hit rate, because no cached `-g1` objects exist yet. The first `main` build after merge saves them, and later runs are warm again. - **ci.yml `build`**, the required PR check on this head: green, so the `-g1` configure works on the build job. - **Sanitizers**, branch run [37208806037](https://github.com/aethersdr/AetherSDR/actions/runs/37208806037): still running at merge time. ASan finished building and is running tests; TSan is still building on the shared core. The result is reported on this PR after merge. If TSan then fails, it is a sanitizer-lane problem, separate from the Full Suite and `build` fixes proven above. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
The TSan CI image build (
Dockerfile.tsan) hangs at QtMultimedia's[2/300]until the job times out. Because a failed build never publishes,ghcr.io/aethersdr/aethersdr-ci:tsanis stuck on Qt 6.8.3, and the TSan sanitizer job now fails at Configure (Qt 6.8.3 found, but AetherSDR requires Qt 6.12, run 37141490698).QtMultimedia compiles its shaders with
qsb -O, andqsbrunsspirv-optthroughQProcess. With the TSan-instrumented Qt 6.12qsb, the child process exits and becomes a zombie, and the parent blocks inanon_pipe_readforever. A diagnostic build confirmed this by capturing the process tree and stacks.Fix
All changes are in
.github/docker/Dockerfile.tsan:QT_HOST_PATH=${QT_HOST_ROOT}andQT_FORCE_FIND_TOOLS=ON, so it runs the base image's uninstrumentedqsb. The other modules don't callqsband are unchanged. Build tools don't need instrumentation; the libraries keep it.ENV QT_HOST_ROOT=${QT_ROOT}is set before the file re-pointsQT_ROOTat the TSan Qt. The layer stops before configuring anything if thatqsbis missing, can't be read, or has its ownDT_NEEDEDentry for libtsan.DT_NEEDEDentry for libtsan, read withreadelf -d.lddisn't enough, because it lists libtsan for anything linked against the TSan QtCore, whether instrumented or not.AetherSDR's own
qt_add_shaderscalls (CMakeLists.txt:2519,:2561) don't passOPTIMIZED, so they never startspirv-opt.Validation
258c88fd): success./opt/Qt/6.12.0/gcc_64/bin/qsb … -O, built QtMultimedia in about 4 minutes, and exported the image.lddform.readelfchecks, tested locally:readelf(1 vs 0) but not inldd(both 1).qsb.link_libraries(-fsanitize=thread)to every target, so each module should record libtsan. That part is reasoned from Qt's scripts, not observed in an image.Not exercised:
docker-ci-image.ymlpublishes:tsanon a push to main, so it wasn't dispatched from a branch. If a check is wrong, the build fails loudly and the existing:tsanstays in place.qsb. It's first seen in the first sanitizers run on the new image.🤖 Generated with Claude Code