Skip to content

fix(ci): build TSan QtMultimedia with uninstrumented host tools - #6123

Merged
ten9876 merged 3 commits into
mainfrom
ci/tsan-image-host-tools
Oct 4, 2026
Merged

ten9876 merged 3 commits into
mainfrom
ci/tsan-image-host-tools

Conversation

@ten9876

@ten9876 ten9876 commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

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:tsan is 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, and qsb runs spirv-opt through QProcess. With the TSan-instrumented Qt 6.12 qsb, the child process exits and becomes a zombie, and the parent blocks in anon_pipe_read forever. A diagnostic build confirmed this by capturing the process tree and stacks.

Fix

All changes are in .github/docker/Dockerfile.tsan:

  • QtMultimedia uses plain host tools. It's configured with QT_HOST_PATH=${QT_HOST_ROOT} and QT_FORCE_FIND_TOOLS=ON, so it runs the base image's uninstrumented qsb. The other modules don't call qsb and are unchanged. Build tools don't need instrumentation; the libraries keep it.
  • The host Qt path is pinned. ENV QT_HOST_ROOT=${QT_ROOT} is set before the file re-points QT_ROOT at the TSan Qt. The layer stops before configuring anything if that qsb is missing, can't be read, or has its own DT_NEEDED entry for libtsan.
  • Every module is checked for TSan. After install, all four add-on libraries (Multimedia, WebSockets, SerialPort, ShaderTools) must have their own DT_NEEDED entry for libtsan, read with readelf -d. ldd isn't enough, because it lists libtsan for anything linked against the TSan QtCore, whether instrumented or not.

AetherSDR's own qt_add_shaders calls (CMakeLists.txt:2519, :2561) don't pass OPTIMIZED, so they never start spirv-opt.

Validation

  • Diagnostic image build, run 37140665591 (diagnostic SHA 258c88fd): success.
    • It used /opt/Qt/6.12.0/gcc_64/bin/qsb … -O, built QtMultimedia in about 4 minutes, and exported the image.
    • It exercised the host-tool flags. Its instrumentation check was the earlier Multimedia-only ldd form.
  • The readelf checks, tested locally:
    • A TSan shared library and a plain library linked against it differ in readelf (1 vs 0) but not in ldd (both 1).
    • The host guard accepts a plain ELF, and rejects a TSan ELF, an unreadable non-ELF file and a missing qsb.
    • Qt's ECM sanitizer setup applies 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:

  • The full image build and publish. docker-ci-image.yml publishes :tsan on a push to main, so it wasn't dispatched from a branch. If a check is wrong, the build fails loudly and the existing :tsan stays in place.
  • AetherSDR's own build under the TSan qsb. It's first seen in the first sanitizers run on the new image.

🤖 Generated with Claude Code

@ten9876
ten9876 requested a review from a team as a code owner October 3, 2026 18:11

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 because ENV 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_PATH silently 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 libtsan assertion covers Multimedia only (line 121, inline). The other three module libraries also carry it, and the existing for m in Multimedia WebSockets SerialPort ShaderTools loop 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 && … — an if with a false condition and no else exits 0 in POSIX sh, so the && chain survives the three non-multimedia iterations rather than aborting the loop. host_tools is re-cleared at the top of every iteration, so no leakage from qtmultimedia (and it's last anyway). Unquoted $host_tools is the intended word split and neither path contains spaces. All clean.
  • Host/target version drift. QT_HOST_PATH requires the host Qt to match the target version exactly. Base Dockerfile:95 sets QT_VERSION=6.12.0 and QT_ROOT=/opt/Qt/6.12.0/gcc_64; this file derives both QT_TSAN_ROOT and QT_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. Base Dockerfile:106 installs -m qtmultimedia qtwebsockets qtserialport qtshadertools via aqt, so bin/qsb plus moc/rcc/uic are present. No qtdeclarative in either Qt, so multimedia's QML plugin (and qmltyperegistrar/qmlcachegen) never enters the picture. Clean.
  • Could the assertion pass vacuously? QT_FORCE_FIND_TOOLS changes which tools run, not the target's FEATURE_sanitize_thread, so the libraries keep -fsanitize=thread and libtsan stays a DT_NEEDED. The check uses the same ldd … grep -q libtsan form 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_shaders doesn't use OPTIMIZED" is accurate — I checked both calls at CMakeLists.txt:2519 and :2561, neither passes OPTIMIZED, so no spirv-opt child is spawned. But the Dockerfile's trailing PATH=${QT_TSAN_ROOT}/bin:${PATH} means sanitizers.yml's cmake --build build --parallel bakes our 12 shaders with the TSan-built 6.12 qsb — the same binary that hangs — and since the 6.12 :tsan image has never published, that has never happened anywhere. The reasoning that it's fine is sound, not observed. It would fail loud (job timeout at timeout-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

Comment thread .github/docker/Dockerfile.tsan Outdated
Comment thread .github/docker/Dockerfile.tsan Outdated
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

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 qsb are still reasoned, not observed. Neither qt_add_shaders call passes OPTIMIZED, so no spirv-opt child is started. That is the only path that hung. The plan is to watch build-tsan after merge and then the first sanitizers run on the new image. As you note, both fail loudly.

Current state of that lane, for context: the TSan job in sanitizers run 37141490698 failed at Configure with Qt 6.8.3 found, but AetherSDR requires Qt 6.12. The stale :tsan image is what this PR replaces.

readelf on the module libraries is reasoned, not observed in an image. ECM's sanitizer setup applies link_libraries(-fsanitize=thread) to every target, and GCC records a DT_NEEDED entry for libtsan when it links a shared library with that flag (checked locally). If that doesn't hold, the image build fails at the check and :tsan stays as it is.

@ten9876 ten9876 self-assigned this Oct 3, 2026

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

  1. Fail closed when host-tool inspection fails — .github/docker/Dockerfile.tsan:105. ! readelf ... | grep -q ... returns success when readelf fails 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 \
  2. 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-only ldd assertion and calls the diagnostic loop identical. Current head captures QT_HOST_ROOT, adds its host guard, and checks all four direct dependencies with readelf; 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. Neither qt_add_shaders call uses OPTIMIZED, verified at CMakeLists.txt:2519 and :2561; absence of that flag supports the reasoning but does not itself constitute a TSan build run.

  3. 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 ... -O at [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 readelf guard results were 0 / 1 / 1, while ldd reported 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 with Qt6<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_tools resets for every iteration. Existing clone/configure/build/install failures still hit exit 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.

@ten9876 ten9876 removed their assignment Oct 3, 2026
ten9876 and others added 3 commits October 3, 2026 12:34
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>
@ten9876
ten9876 force-pushed the ci/tsan-image-host-tools branch from 7fa8338 to 1810b81 Compare October 3, 2026 19:34
@ten9876

ten9876 commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator Author

All three nits are addressed in 1810b81, rebased onto current main (579efd73f):

  1. Fails closed now. The host guard captures readelf -d output first (host_dynamic="$(readelf …)"), so a failed read stops the layer. Checked under bash --posix: a plain ELF is accepted; a TSan ELF, an unreadable non-ELF file and a missing qsb are rejected.
  2. The body is refreshed. It describes the final checks and separates what run 37140665591 exercised (host-tool flags, Multimedia-only ldd) from what is reasoned (each module's own libtsan entry) and what is untested (the image publish, and AetherSDR's own shaders under the TSan qsb).
  3. The comment is trimmed. It now states the rule; the incident story is in the body.

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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

  1. Final integration evidence remains outstanding — .github/docker/Dockerfile.tsan:101–125, with downstream consumers at CMakeLists.txt:2519 and CMakeLists.txt:2561. The successful diagnostic image used the same host-tool flags but the older Multimedia-only ldd check, not the final four-module readelf assertions. The application’s own shaders have also not been built using this new TSan image. Both shader calls omit OPTIMIZED, 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/sh with 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-instrumented message. 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 older ldd assertion, 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.

@ten9876 ten9876 assigned ten9876 and unassigned ten9876 Oct 4, 2026
@ten9876

ten9876 commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

On the last review's nit (final integration evidence): agreed, that's the post-merge check, and it's tracked rather than assumed. docker-ci-image.yml builds and publishes :tsan on the push to main. If any of the new readelf assertions fail, that build fails and :tsan stays at its current digest. After it publishes, the TSan job of the next sanitizer run will configure and build AetherSDR, including its 12 shaders, against the new image. Until then that job fails at Configure on the stale Qt 6.8.3 image. With #6136 that failure now files a tracking issue instead of passing silently. Landing now, with the image publish and the first TSan run as the checks to follow.

@ten9876
ten9876 merged commit b255830 into main Oct 4, 2026
7 checks passed
@ten9876
ten9876 deleted the ci/tsan-image-host-tools branch October 4, 2026 03:06
ten9876 added a commit that referenced this pull request Oct 4, 2026
…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>
ten9876 added a commit that referenced this pull request Oct 4, 2026
… 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>
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