Skip to content

fix(logging): log the failures the docs Log Analyzer could not see. Principle VIII. - #6308

Merged
ten9876 merged 8 commits into
mainfrom
fix/6285-log-analyzer-gaps
Oct 9, 2026
Merged

ten9876 merged 8 commits into
mainfrom
fix/6285-log-analyzer-gaps

Conversation

@ten9876

@ten9876 ten9876 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #6285 (items 1, 2 and 4). Item 3, Hermes-Lite 2 discovery, is #6293 by @on8st and is not touched here, so #6285 stays open until that lands; the analyzer rules for #6293's lines follow once its strings are on main (the rule test checks every cited line exists).

Why

The docs Log Analyzer can only flag a failure the app writes to the log. Three user-facing failures were never logged, so a support bundle could not show them either.

What it logs

Item Category, level Line (stable prefix in bold) When
1. VITA-49 sequence errors aether.vita49, Warning PanadapterStream: VITA-49 sequence errors on <audio|FFT|waterfall|meter|DAX> stream 0x<8-digit id>: N in the last S s, E since the stream started (P packets received) The first error on a stream logs at once; after that, at most one line per stream per 10 s, carrying every error held back since the last line. A stream torn down (disconnect; a pan, waterfall, DAX or IQ stream removed; any stream the radio reports removed, including remote_audio_rx) starts over: its next packet is not compared with the old sequence count, and E, P and the window restart; the 10 s limit still applies across the restart. All streams together are capped at 8 lines per 10 s; a line the cap refuses keeps its errors for that stream's next line, and the next line logged is preceded by PanadapterStream: N VITA-49 sequence error(s) held back: more than 8 line(s) in 10 s across all streams.
2. BNR pack aether.nvafx, Warning NvidiaAfxPack: install failed: <the message the BNR panel shows> Every failed install. A cancel that reaches fail("cancelled") (during unpacking) logs NvidiaAfxPack: install cancelled at Info; cancelling an active transfer logs nothing, because cancel() disconnects the reply before aborting it.
2. BNR pack aether.nvafx, Warning NvidiaAfxPack: transfer failed: <component>: <Qt error> or NvidiaAfxPack: transfer timed out (stall limit 30 s): <component> A network failure of the archive download or of a PyPI / simple-index lookup (the lookup's message in the panel has no reason).
4. Qt platform default, Info Platform: Qt platform plugin "<name>" (QT_QPA_PLATFORM=<value>, set by the user | set by AetherSDR), "<name>" (QT_QPA_PLATFORM unset, Qt default), or "<name>" (-platform <value>, on the command line) Every start-up, right after the log handler is installed.
4. Qt platform default, Warning Platform: Qt fell back from "<first>" to "<used>"; the first plugin in <QT_QPA_PLATFORM | -platform> did not load Only when the plugin in use is not the first one requested. Qt gives a -platform (or --platform) argument precedence over QT_QPA_PLATFORM, so when there is one, it is the request compared.

Behaviour changes beside the log lines

  • -platform drives AetherSDR's own platform logic. A -platform/--platform argument counts like a user-set QT_QPA_PLATFORM: AetherSDR no longer imposes wayland;xcb in a Wayland session over it, and GpuSelector's GLX/EGL (PRIME) choice reads the request Qt will actually use (GpuSelector::requestPicksWayland).
  • Sequence tracking restarts with the stream. RadioModel restarts the sequence tracking of every stream the radio reports removed, in one place, which covers remote_audio_rx (it has no registration in PanadapterStream). Nothing restarts on a status for a stream still flowing, so a real gap is never hidden. A reused id no longer adds a false error to the status-bar and Network Diagnostics counts either.
  • tools/docs/snapshot_stable.py rewrites each Log Analyzer rule link /next/<page> to /<page> when the new Stable snapshot has that page.

Design notes

  • Item 1 uses qCWarning(lcVita49), not aether.protocol. The triage's correction applies: aether.vita49 is declared QtWarningMsg, so a Warning reaches a default log with no LogManager change, and packet loss is a warning. The limiter (VitaSequenceLossReport.h) lives in StreamStats and runs only on the error path; a shared VitaSequenceLossBudget bounds all streams together, because the per-stream limiter lets each new id's first error through at once and the VITA socket does not filter by sender: a clean stream pays one clock read when it is first seen. Its values are read under m_statsMutex; the line is formatted and logged after the lock is released. Teardown (clearRegisteredStreams() on disconnect, unregister{Pan,Wf,Dax,Iq}Stream(), and RadioModel for every stream the radio reports removed) restarts the stream's sequence tracking, because a reconnect can reuse a stream id; the cumulative per-stream totals behind the status bar and Network Diagnostics are kept, so those counters behave as before, except that a reused id no longer adds a false error on its first packet. Errors still held when a stream goes clean are not logged; the live counters (Network Diagnostics) keep them. A sequence error is any gap in the 4-bit count, so reordered packets count too (Remote RX audio dropouts: out-of-order VITA-49 packets counted as drops and concealed instead of reordered in the jitter buffer #6051); the docs say "lost or arrive out of order".
  • Item 2. lcNvAfx had no header declaration; it is now declared in NvidiaAfxFilter.h. aether.nvafx gets a LogManager registry row ("BNR (NVIDIA AFX)") so it can be toggled in the Support dialog; its Warning lines reach a default log either way, since only Debug is filtered. The three exits that called emit finished(false, …) directly (no GPU, install verification, offline archive not found) now go through fail(), which only adds the cleanup it already does and the log line. Redaction: withoutUrlQueries() strips URL query strings and fragments (keeping closing punctuation after them) from logged text, because a redirected download URL can carry a signed query. fail() strips the message it emits as well as the one it logs, so the BNR panel's Failed: text is redacted too. A failed download logs two lines on purpose: transfer failed: per network transfer (the same line as a failed lookup, whose panel message has no reason) and install failed: for the outcome. The manifest and PyPI URLs are source constants with no tokens. could not install into <path> is under the home directory, which the log redactor already collapses to ~, and NvidiaAfxFilter already logs the pack path.
  • Redaction. The pre-existing default-level addr= (VITA bind) and from= (first packet) lines carry no host keyword, but the IPv4/IPv6 shape rules in redactPii() mask them (IPv4 keeps its last octet, IPv6 is replaced, per docs/log-redaction.md); three negative-corpus rows in async_log_writer_test pin that. The analyzer page's privacy note now lists exactly what is masked.
  • Item 4. A CR or LF in the request is folded to a space, so it cannot forge a second log line. main() reads the -platform argument the way QGuiApplication does (one - of -- dropped, the next argument, the last one winning) before QApplication consumes it. The line lives in a small app-shell helper, src/QtPlatformChoice.{h,cpp}, not in src/core/, so main.cpp gains no engine include (the touchpoint manifest stays unchanged). A plugin that fails to load entirely aborts inside the QApplication constructor, before any log handler exists; that can't be logged in-process, and the docs entry says so.

Docs

  • Log Analyzer: five rules with positive samples — vita-sequence-errors, bnr-download-failed, bnr-checksum-mismatch, bnr-install-failed, qt-platform-fallback. There is also a Qt platform plugin row in the analyzer's System summary (a summary entry plus SUMMARY_FIELDS). The plain platform line is in clean.log, and test_log_rules.mjs checks that the summary reads xcb.
  • Troubleshooting (symptom → fix), Next only, so the new rules link /next/…:
    • Low-Bandwidth Connections: Audio drops out or the waterfall has gaps
    • BNR GPU Noise Removal: The download fails with "checksum mismatch" and The download finishes but BNR is not installed. The download fails now mentions the 30 s stall and that downloads resume.
    • GPU Rendering: AetherSDR runs on XWayland in a Wayland session
  • bnr-download-failed links the Stable anchor /bnr-gpu-noise-removal#the-download-fails.
  • snapshot_stable.py moves /next/ rule links to Stable at each snapshot; docs/user/README.md and the /release-prep snapshot step describe it. Tested on a scratch copy of the tree: the 4 /next/ links became Stable links, test_log_rules.mjs passed against the new snapshot, a second run changed nothing, and a link whose page is not in the snapshot stays /next/.
  • test_log_rules.mjs --update also refreshed four existing rules whose cited lines moved in PanadapterStream.cpp and main.cpp.

Proof

Agent automation bridge: an offscreen build, a fresh settings profile, and a private network namespace (unshare -n, as capture_screenshots.py does), so there is no radio and no internet.

Step Before (888ac58bf) After (18a772b75)
Start-up log, QT_QPA_PLATFORM=offscreen no platform line INF default: Platform: Qt platform plugin "offscreen" (QT_QPA_PLATFORM=offscreen, set by the user)
Wayland session on the desktop, no QT_QPA_PLATFORM — INF default: Platform: Qt platform plugin "wayland" (QT_QPA_PLATFORM=wayland;xcb, set by AetherSDR)
QT_QPA_PLATFORM='nosuchplugin;offscreen' — WRN default: Platform: Qt fell back from "nosuchplugin" to "offscreen"; the first plugin in QT_QPA_PLATFORM did not load; Qt's own Could not find the Qt platform plugin "nosuchplugin" went to stderr only
QT_QPA_PLATFORM=offscreen AetherSDR -platform minimal (an intentional override) — INF default: Platform: Qt platform plugin "minimal" (-platform minimal, on the command line), no warning; --platform minimal logs the same
Fake Wayland session (XDG_SESSION_TYPE=wayland, WAYLAND_DISPLAY set, no QT_QPA_PLATFORM) with -platform offscreen / --platform offscreen — no Platform: Wayland session … -> QT_QPA_PLATFORM wayland;xcb line (AetherSDR no longer imposes it); INF default: Platform: Qt platform plugin "offscreen" (-platform offscreen, on the command line)
AetherSDR -platform 'nosuchplugin;offscreen' — INF default: Platform: Qt platform plugin "offscreen" (-platform nosuchplugin;offscreen, on the command line), then WRN default: Platform: Qt fell back from "nosuchplugin" to "offscreen"; the first plugin in -platform did not load
invoke "AetherRX..." trigger → invoke dspMethodBtnBNR click → invoke "Download BNR runtime" click → invoke Accept click (license prompt, throwaway profile), then log mark / log tail no line: the download fails silently W aether.nvafx: NvidiaAfxPack: transfer failed: AFX runtime: Network unreachable, then W aether.nvafx: NvidiaAfxPack: install failed: download failed (AFX runtime): Network unreachable
The analyzer engine (analyze.mjs + rules.json) run on those real log files qrhi-failure, bnr-pack-missing; summary has no platform adds bnr-download-failed (BNR run) and qt-platform-fallback (fallback run); summary Qt platform plugin: offscreen / wayland

(qrhi-failure is expected under offscreen, as its rule says.)

One thing the bridge showed along the way, not changed here: after the failed download, dumpTree reads the BNR status label as Not installed. updateBnrStatus() runs straight after the Failed: <msg> text is set and overwrites it, so before this PR the reason for a failed download appeared nowhere.

Unit tests (socket-free, QT_QPA_PLATFORM=offscreen, ctest --no-tests=error): all pass.

  • vita_sequence_loss_log_test (45 checks): the limiter (first error at once, none for 10 s, held count carried, 7 lines in a minute of continuous errors, the limit held across a restart) and the exact text. It also feeds meter packets to PanadapterStream::processDatagram() on a stream that never binds, through a test-access friend like PcmCompatibilityTestAccess, under the product's aether.*.debug=false rule. In-order packets log nothing, one gap logs the exact warning, and 50 more gaps within 10 s add no line. After clearRegisteredStreams(), a reused stream id's first packet logs and counts nothing, the cumulative total is kept, and the next real gap logs at once with restarted counts; after each of unregister{Pan,Wf,Dax,Iq}Stream() a re-created stream counts no error.
  • remote_audio_rx_sequence_restart_test (4): through RadioModel's status router, a gap across an adoption status still counts, a removed remote_audio_rx id's next packet counts no error, and a later real gap counts.
  • async_log_writer_test: negative-corpus rows for the addr= / from= VITA lines (IPv4 and IPv6).
  • qt_platform_choice_test (31): list parsing, wayland vs wayland-egl, fallback detection, the exact text for each source including -platform, and reading -platform / --platform (last wins, a trailing -platform and -platformtheme ignored), and GpuSelector::requestPicksWayland on the -platform request.
  • nvidia_afx_pack_log_test (11): the offline import of a missing archive logs W aether.nvafx: NvidiaAfxPack: install failed: archive not found without changing the finished() message; it never calls install(). URL query and fragment stripping, closing punctuation kept, any scheme case.

Mutation checks, each restored and confirmed by sha256 against the saved original:

  • Limiter always reports → 4 checks fail.
  • Line downgraded to qCInfo → the integration checks fail (Info on aether.vita49 is dropped by a default log).
  • fellBack() always false → 4 fail.
  • The missing-archive exit reverted to emit finished(…) → the log check fails.
  • withoutUrlQueries() returns its input unchanged → 2 fail; the old greedy tail → the punctuation check fails.
  • start() resetting the last report again → the 4 restart checks fail.
  • clearRegisteredStreams() without the sequence restart → 3 fail; each unregister*Stream() without it → its check fails.
  • platformArgument() never returning a value → 3 fail; without the -- handling → 1 fails; requestPicksWayland not recognising xcb → 2 fail.
  • No shared budget → the burst-of-new-ids check fails (20 lines instead of 8); a refusal that spends the stream's report → 3 fail; a case-sensitive scheme → 1 fails; no CR/LF folding → 1 fails.
  • No restart for a removed stream (RadioModel and unregisterDaxStream()) → 2 fail; stream id unpadded → 1 fails; fellBack() prefix one-way → 1 fails.

Also run: pcm_compatibility_test, nvidia_afx_filter_test, nr_rate_domain_test and flex_backend_lifecycle_test pass. Every Static checks script passes locally, including gen_reference.py --check, test_log_rules.mjs (707 checks, 45 rules) and gen_touchpoint_manifest.py --check. The colour and a11y steps have no src/gui/ files to scan. cd docs/user && npm ci && npm run build passes (links and anchors).

Build: Linux, Qt 6.12.0, lean review configure (AETHER_SHARED_CORE=ON, -O2 -g1); targets AetherSDR plus the tests above.

Not verified

  • Item 1 on a real radio. No live hardware was used for this PR. The PanadapterStream path is exercised by feeding synthetic packets to processDatagram(), not by a lossy network.
  • The stall branch (transfer timed out). I checked with a standalone Qt 6.12 probe that a transfer timeout reports TimeoutError ("Operation timed out"). The app's own stall path was not triggered.
  • The checksum and unpack failures were not triggered in the app; their samples are written from the message literals.
  • Windows and macOS: built and run on Linux only. CI covers the builds.

Agent automation bridge — gaps found

  • Replaying a lossy VITA-49 stream. Driving item 1 end to end needs packets with sequence gaps on a live stream. The bridge has no verb to inject or replay VITA-49 datagrams, and the demo backend is not a Flex PanadapterStream. Proposed: a TX-free sim vita drop <stream> <every-N> fault-injection verb (or a pcap replay) on the Flex receive path.
  • Start-up log lines before the bridge listens. log tail starts at bridge start-up, so the Platform: lines (written earlier in main()) are only in the log file. Proposed: log file [grep], which returns lines of the current log file.

🤖 Generated with Claude Code

…rinciple VIII.

Three user-facing failures never reached the log, so neither the docs Log
Analyzer nor a person reading a support bundle could spot them (#6285):

- Flex VITA-49 sequence errors. PanadapterStream counted them per stream
  but logged nothing. It now writes one aether.vita49 warning per stream
  at most every 10 s ("PanadapterStream: VITA-49 sequence errors on <cat>
  stream 0x<id>: N in the last S s, E of P packets since the stream
  started"). The limiter runs only on the error path, and the line is
  logged after the stats lock is released.
- BNR runtime pack failures. NvidiaAfxPack had no log statements. fail()
  now logs "NvidiaAfxPack: install failed: <reason>" on aether.nvafx (a
  cancel logs at Info), the three exits that bypassed fail() go through
  it, and each network failure adds "transfer failed" or "transfer timed
  out (stall limit 30 s)". URL query strings are stripped from logged
  text, since a redirected download URL can carry a signed query.
- The Qt platform plugin. main.cpp now logs the plugin in use, the
  QT_QPA_PLATFORM value and who set it, and a warning when Qt fell back
  from the first plugin in that list. Qt's own message about a plugin that
  failed to load is printed before the log handler exists, so it stays on
  stderr.

The Hermes-Lite 2 discovery item is #6293 and is not touched here.

Docs: five Log Analyzer rules with samples (vita-sequence-errors,
bnr-download-failed, bnr-checksum-mismatch, bnr-install-failed,
qt-platform-fallback), a "Qt platform plugin" summary row, and
Troubleshooting entries on the Low-Bandwidth, BNR and GPU Rendering pages.

Tests: vita_sequence_loss_log_test (limiter, text, and PanadapterStream
fed meter packets without a socket), qt_platform_choice_test and
nvidia_afx_pack_log_test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 self-assigned this Oct 9, 2026
@ten9876
ten9876 marked this pull request as ready for review October 9, 2026 02:12
@ten9876
ten9876 requested review from a team as code owners October 9, 2026 02:12

@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.

Thanks for this — it is a well-built change, and the drift-checked rule/sample/doc triple per log line is exactly the shape #6285 asked for. I ran node tools/docs/test_log_rules.mjs against the head checkout: 706 checks passed for 45 rules, so every new rule compiles, cites a live source literal, fires on its sample, keeps clean.log silent and resolves its docs anchor. One finding below, in the VITA-49 path.

1. Issue fit

Partially — and the gap is disclosed. #6285 lists four items:

# Item This PR
1 Flex VITA-49 sequence errors ✅ aether.vita49 warning, rate-limited per stream (issue suggested aether.protocol; lcVita49 is the better fit and is already QtWarningMsg in LogManager.cpp:22)
2 BNR runtime pack failures ✅ fail() logs, the three exits that bypassed it now route through it, network failures get their own line
3 HL2 discovery ❌ deferred to PR #6293 (open, by @on8st, covers item 3 only) — stated in the commit message and non-overlapping, so this is a coordinated split, not an unaddressed requirement
4 Qt platform plugin / fallback ✅ plugin + QT_QPA_PLATFORM + who set it, plus a fallback warning

The issue's own acceptance bar ("each new line should be a stable, greppable string, so the analyzer rule can cite its file:line") is met and mechanically enforced.

Test layer is right: all three new tests are socket-free CTests, which is the correct layer for wire/parsing/policy per AGENTS.md. I verified the claim — PanadapterStream's constructor (PanadapterStream.cpp:88-95) only does m_pcmProducer.start() and explicitly defers socket setup to init(), so feeding processDatagram() directly binds nothing.

Socket-test register: this PR adds no socket-owning test and removes none. vita_sequence_loss_log_test names QUdpSocket only indirectly via PanadapterStream.h; nothing in the three new tests binds, listens or connects.

2. Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/VitaSequenceLossReport.h, PanadapterStream.{cpp,h} Rate limiter + line text + the emit site Yes (item 1) In scope
src/core/NvidiaAfxPack.{cpp,h} fail() logging, logTransferFailure, withoutUrlQueries, 3 bypass exits routed through fail() Yes (item 2) In scope
src/core/NvidiaAfxFilter.h adds Q_DECLARE_LOGGING_CATEGORY(lcNvAfx) Not in the body In scope — necessary: lcNvAfx is defined in NvidiaAfxFilter.cpp:12 and the pack TU needs the declaration. Not a new category
src/QtPlatformChoice.{h,cpp}, src/main.cpp, CMakeLists.txt:1691 Item 4 + the one target that compiles main.cpp Yes In scope. src/main.cpp is referenced by exactly one target, so no sibling target needs the new TU
docs/user/docs/*.md, log-samples/**, rules.json, LogAnalyzer/index.js 5 rules, 5 samples, 1 summary row, 3 Troubleshooting entries Yes In scope
rules.json line bumps (221→234, 175→188, 1686→1714, 80→83) Cited line numbers Not in the body In scope — these are this PR's own insertions moving existing cited lines; the drift checker's --update output
clean.log +1 line the new Platform: line Not in the body In scope — required, or check 6's s.qtPlatform === 'xcb' assertion has no data
tests/* + tests.cmake 3 new targets Yes In scope

Nothing in the diff is unexplained. No new public or protocol surface: no settings key, no capability field, no wire verb, no CLI flag — QtPlatformChoice is an internal struct and the log strings are the only new contract. No CHANGELOG.md entry was added (correct). No existing default, UI element or behaviour is modified, so there is no preference-vs-fix question here: this is purely additive diagnostics.

3. Blockers

1. src/core/backends/flex/PanadapterStream.cpp:465-488 — m_streamStats is not cleared at the documented disconnect reset hook, so a reused stream id makes the new warning fire falsely on reconnect.

clearRegisteredStreams() is self-described as the place where everything keyed by stream id gets dropped:

    // Drop PLC state alongside the rest of the per-stream tables — this is
    // the disconnect-time reset hook, so anything keyed by VITA-49 stream
    // id can be cleared safely (#2738).
    m_audioPlc.clear();

It clears 15 per-stream maps plus resetAudioStreamStats(). It does not clear m_streamStats, and neither do unregisterPanStream() (:449, which clears m_frames/m_dbmRanges/m_pendingDbmRanges) nor unregisterWfStream() (:458). This PR puts new state into that very map:

    struct StreamStats {
        int  lastSeq{-1};
        int  errorCount{0};
        int  totalCount{0};
        VitaSequenceLossLimiter lossLog;   // PanadapterStream.h:309
    };

Precondition: a stream id recurs after a teardown (disconnect/reconnect to the same radio, or a pan/DAX stream recreated with the same id). Then, in processDatagram():

  • stats.lastSeq still holds the previous session's last sequence value, so the first packet of the new stream almost always fails vitaSeq == (lastSeq + 1) & 0x0F → a spurious sequenceError.
  • stats.lossLog.m_lastReportMs is more than kIntervalMs in the past, so recordError() returns true → the line is emitted immediately, at warning level, on the first packet after reconnect.
  • stats.totalCount == 1 is never true again, so lossLog.start() never re-runs and reportWindowMs() is measured from the previous session's last report — "in the last N s" can be an arbitrarily large number.
  • errorCount/totalCount carry over, so "E of P packets since the stream started" really means "since this process first saw this stream id".

Before this PR that was an invisible counter artifact. Now it surfaces as vita-sequence-errors → "Packets from the radio were lost or arrived out of order" (rules.json:260) pointing the user at a Troubleshooting entry that tells them to re-cable their network. That is a misdiagnosis in a feature whose whole purpose is accurate diagnosis.

I could not confirm how often FlexRadio reuses stream ids without hardware, so the frequency is uncertain — but the fix is correct regardless and is what the #2738 comment already asks for. Add a stats-side forget and call it from the three teardown paths (keep it outside the m_streamMutex scope so you don't introduce a m_streamMutex → m_statsMutex nesting that processDatagram() doesn't already have — worth a second look at lock order before you place the calls):

void PanadapterStream::forgetStreamStats(quint32 streamId)
{
    QMutexLocker statsLock(&m_statsMutex);
    m_streamStats.remove(streamId);
}

and in clearRegisteredStreams(), alongside resetAudioStreamStats();:

    {
        QMutexLocker statsLock(&m_statsMutex);
        m_streamStats.clear();
    }

A unit test fits the existing socket-free seam: feed a gap, call clearRegisteredStreams(), feed seq 0 on the same id, assert no second warning.

4. Nits (non-blocking)

  • src/core/NvidiaAfxPack.cpp:709 — the redaction covers the log but not the signal. Credit to security-audit (CodeGuard) for the lead; I confirmed it. es is the raw errorString() from :701. fail() logs it stripped (:381, withoutUrlQueries(msg)), but also does emit finished(false, msg) unstripped, and the only consumer renders it verbatim: src/gui/AetherDspWidget.cpp:1850 → m_bnrAfxStatus->setText(QStringLiteral("Failed: %1").arg(msg)). Qt's transfer error strings commonly embed the post-redirect URL, which is the signed query NvidiaAfxPack.h:80-82 exists to defend against. Low severity — a label the operator already sees, every current source is a public GitHub-release/pypi.org asset, and the signature is minutes-lived — but the fix also removes the current double-log of the same cause (:708 plus :381):
              fail(QStringLiteral("download failed (%1): %2").arg(label, withoutUrlQueries(es)));
  • src/core/backends/flex/VitaSequenceLossReport.h:57-58 — "%5 of %6 packets" misreads as a loss count. totalErrors is stats.errorCount, which counts sequence gap events, not packets lost — a single 12-packet gap increments it by 1. The header comment two lines up is accurate about this; the line text is not. Suggest "%5 errors in %6 packets since the stream started".
  • src/core/backends/flex/PanadapterStream.cpp:743 — the comment is slightly ahead of the code. QString sequenceLossLine; // logged after the stats lock is released is true of the qCWarning at :798, but formatVitaSequenceLoss() is called at :770 inside the QMutexLocker scope, so the string formatting still happens under m_statsMutex. Error path only, so the cost is negligible — just worth not claiming otherwise.
  • src/core/NvidiaAfxPack.cpp:334 — the stripper eats a trailing delimiter. [?#][^\s]* is greedy to the next whitespace, so (https://h/x?y) becomes (https://h/x. Cosmetic only; the redaction itself is sound.
  • docs/user/src/components/LogAnalyzer/rules.json — 4 of 45 rules now point at /next/ (the three new-heading ones plus vita-sequence-errors), against 41 on stable. That is forced and correct today, but nothing flips them when version-stable is next snapshotted, and a user on a released build following them lands on Next docs. Worth a note wherever the versioning procedure lives.
  • docs/user/log-samples/rules/vita-sequence-errors.log:9 — "1 in the last 376 s" at 09:20:41 against a stream-start line at 09:14:05 is ~396 s. Cosmetic; the checker doesn't look at it.
  • None of the three new tests are in any CI -R filter. The only filtered ctest steps in ci.yml are the DV, cross_needle_meter_test, mac_nr_filter_test and asr_gpu_probe_test lanes, so PR CI compiles vita_sequence_loss_log_test, qt_platform_choice_test and nvidia_afx_pack_log_test but does not run them — they gate only on the nightly full suite. That matches house practice; noting it so "all green" isn't read as "the new tests passed".

5. What I tried to break (and could not)

Findings below are reasoned from the head checkout at /tmp/aetherclaude/pr-6308; I have no build and no runtime here, so nothing was reproduced by execution except the Node drift checker, which I did run.

  • "The regex escapes are missing." The diff as rendered shows (\bhttps?://...) and \1 — but that is a harness artifact (the same one that turned every & into ${PR_DIFF}). NvidiaAfxPack.cpp:334-336 in the checkout really reads "(\\bhttps?://[^\\s?#]+)[?#][^\\s]*" and "\\1", so the pattern compiles as \b/\s and the replacement is a genuine capture back-reference. No leak. Same for \\S in rules.json:835, which new RegExp() would have thrown on.
  • "A user cancel will be logged as a 30 s stall." logTransferFailure treats OperationCanceledError as a timeout on the stated grounds that "cancel() disconnects before it aborts". Verified: cancel() calls m_reply->disconnect() before abort() (with a comment explaining the close-mid-download crash that motivated it), so the slot never runs on a user cancel. The comment is accurate.
  • "fail() on the three new exits will delete a completed component and break the resume the docs now promise." fail() does QFile::remove(m_tmpFile). But m_tmpFile is cleared on the success path (:728, QFile::remove(dest); m_tmpFile.clear();) and on the checksum path, so by the time assembleAndCommit() can call fail("install verification failed") it is empty. The new BNR doc text ("Components that finished downloading are kept, so the download resumes") holds.
  • "The platform line will mislabel who set QT_QPA_PLATFORM." The only qputenv("QT_QPA_PLATFORM", ...) in main.cpp is at :307, which also sets g_qpaPlatformChoice on the next line, and g_qpaPlatformUserSet is assigned at :288 unconditionally (not inside any platform #ifdef), before anything can modify the variable. The three-way User/AetherSDR/Unset discrimination is exact on every OS.
  • "QGuiApplication::platformName() is read before there is an application." QApplication app(argc, argv) is at main.cpp:384; the logging block is at :638-648, inside if (logManager.startLogging(...)) at :621. Ordering is fine.
  • "fellBack() will false-positive." Walked wayland;xcb→wayland-egl (prefix match, no warning), xcb:foo=1, ";xcb" (empty first → false), a single-entry list (Qt aborts rather than falling back), and case differences (startsWith(..., Qt::CaseInsensitive)). No false warning found.
  • "The test can't reach processDatagram/PCC_METER." PanadapterStream is in namespace AetherSDR (PanadapterStream.h:24) and the test declares VitaSequenceLossLogTestAccess in the same namespace, so the new friend at :220 genuinely grants access to processDatagram (:225) and the private PCC_METER (:256). VITA49_HEADER_BYTES, StreamCategory and categoryStats() are public.
  • "The new BNR test won't compile without HAVE_NVIDIA_AFX." CMakeLists.txt:3241 sets it PUBLIC on aethercore, which the test links, so the guard in NvidiaAfxPack.h opens. And ci.yml does build the AFX arm (-DENABLE_NVIDIA_AFX=ON in the build job at :269), so the green badge really does cover that code — unlike some AFX changes.
  • Rate limiter arithmetic. Hand-checked the 60-second steady-error case: reports at t=0/10000/…/60000 = 7 lines accounting for all 12001 errors, matching the test's assertions. The m_windowStartMs < 0 fallback and the !m_seqLossClock.isValid() guard both hold; m_seqLossClock is only ever touched under m_statsMutex, and processDatagram runs on the single network thread (PanadapterStream.cpp:94), so there is no race.
  • Tests that would pass against the unfixed code. vita_sequence_loss_log_test asserts exact line text and that a 50-gap burst adds no second line while categoryStats().errors > 40 — emptying the limiter body or dropping the qCWarning fails it. Likewise nvidia_afx_pack_log_test asserts the exact aether.nvafx warning string for a path (archive not found) that previously emitted no log at all.
  • Other tools' findings. All 36 CodeGuard hits are false positives on this diff and I refuted them individually: CS-VLN-SSRF-PRIV-10/CS-PII-IP on 10.9.0.34 is the pinned TensorRT version, not an RFC1918 address; every CS-VLN-CMDI-PIPE is | as regex alternation in rules.json (and the Flex wire delimiter R\d+\|0\|); CS-VLN-CMDI-BACKTICK hits backticks inside a regex character class and C++ comments, with no system/QProcess anywhere in the change; CS-VLN-SSRF-HEX hits VITA-49 stream ids and header bit masks; CS-PII-DL-1 is the Flex error code F3000001 in prose; CS-VLN-PATH-UNC is JSX display text showing users where logs live; and every CS-VLN-XSS-EVENT is a React JSX prop (onDrop, onChange, onClick) — the file has no innerHTML/dangerouslySetInnerHTML, and all log-derived text goes through JSX interpolation, which React escapes. The only lead that survived is the nit above. security-audit also flagged addr=/from= host addresses reaching default-level logs at PanadapterStream.cpp:322 and :596 versus LogRedactionPolicy.h:53-56, and the "AetherSDR already redacts addresses…" claim at LogAnalyzer/index.js:135; I checked the hunks and none of those lines are in this diff — pre-existing, and worth its own issue rather than a change here.
  • Could not check: whether FlexRadio actually reuses VITA-49 stream ids across a reconnect (needs hardware or a firmware capture) — that is the one open question in Blocker 1, and it governs frequency, not whether the fix is right. Also could not verify the rendered docs anchors in a built site beyond what the checker's slugifier asserts.

6. Recommendation

Request changes, on Blocker 1 only. Everything else here is solid and the nits are all optional. m_streamStats escaping the clearRegisteredStreams() reset is a small fix in a file this PR already touches, and it matters more than usual because the state is now user-visible: without it, the first packet after a reconnect on a recurring stream id can tell an operator their network is dropping packets when nothing was lost. Next step: add the stats-side forget to the three teardown paths (checking lock order), plus the one reconnect assertion in vita_sequence_loss_log_test. The %5 of %6 packets wording is worth folding into the same push since it changes a cited literal and the drift checker will want the rule text updated with it.


🤖 aethersdr-agent · cost: $12.1264 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (36 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:170
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:176
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:192
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:197
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:200
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:84
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:739
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:998
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:83
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:588
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:325
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:326
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:588
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/NvidiaAfxPack.h /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.h:80
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/NvidiaAfxPack.h /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.h:92
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:251
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:803
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:1461
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:261
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:281
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:131
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:139
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:141
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:150
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:188
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:349
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/main.cpp /tmp/aetherclaude/pr-6308/src/main.cpp:106
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in tests/vita_sequence_loss_log_test.cpp /tmp/aetherclaude/pr-6308/tests/vita_sequence_loss_log_test.cpp:123
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in tests/vita_sequence_loss_log_test.cpp /tmp/aetherclaude/pr-6308/tests/vita_sequence_loss_log_test.cpp:145

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $12.2877 · model: claude-opus-5

@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: Partially, with one correctness defect to fix. At 18a772b75, items 1, 2 and 4 of #6285 gain the promised log lines and analyzer rules. Item 3 is explicitly delegated to #6293 and #6285 correctly stays open. VITA warnings and BNR failure logging work; the Qt fallback diagnosis mistakes an explicit command-line override for a failed plugin.

Scope

Files/group Claimed? Verdict
PanadapterStream.{cpp,h}, VitaSequenceLossReport.h Yes: per-stream sequence-error warnings In scope; routing and sequence-counter semantics retained
NvidiaAfxPack.{cpp,h}, NvidiaAfxFilter.h Yes: failures, timeout detail, URL-query stripping In scope; three bypass exits now use existing cleanup
QtPlatformChoice.{cpp,h}, main.cpp, CMakeLists.txt Yes: actual platform and fallback log In scope; defect below
Log Analyzer index.js, rules.json Yes: five rules and platform summary In scope
Three troubleshooting pages, six log samples Yes In scope
Three tests, tests.cmake, test_log_rules.mjs Yes: coverage and sample/summary checks In scope; socket-free and registered

All 26 files and the single signed commit are explained by the stated scope. No unrelated changes, CHANGELOG entry, new dependencies, radio commands, settings writes, or engine-boundary growth found.

Blockers

  1. [P2] Treat Qt's command-line platform override as the effective request before diagnosing a fallback — src/main.cpp:643, src/QtPlatformChoice.cpp:35. Qt accepts -platform and gives it precedence over QT_QPA_PLATFORM. This code compares the plugin actually loaded with the environment value alone, then asserts the environment's first plugin failed to load. Launching the exact PR build with QT_QPA_PLATFORM=offscreen AetherSDR -platform minimal produced:

    INF default: Platform: Qt platform plugin "minimal" (QT_QPA_PLATFORM=offscreen, set by the user)
    WRN default: Platform: Qt fell back from "offscreen" to "minimal"; the first plugin in QT_QPA_PLATFORM did not load
    

    Offscreen did not fail: the control launch without -platform minimal successfully loaded it. The analyzer nevertheless emits qt-platform-fallback and tells the user a plugin could not load. This also affects an ordinary Wayland user launching with -platform xcb, a useful diagnostic escape hatch. Capture the effective -platform request before QApplication consumes its arguments, distinguish that source in the startup summary, and compare against that request. Add coverage for an intentional override alongside a genuine fallback; keep the genuine warning working.

    Bridge evidence from that same instance: get radio returned {"ok":true,"radio":{"model":"AetherSDR Demo","serial":"DEMO-0001","connected":true,"transmitting":false}} (abridged). I also reproduced the false line with a small Qt 6.12 QGuiApplication harness using this PR's helper.

Nits — non-blocking

  • src/core/NvidiaAfxPack.cpp:378: narrow the body's claim that a user cancel always logs Info. The line is in fail("cancelled"), whereas cancel() at line 348 disconnects an active reply before aborting it and emits no log itself; cancellation during a transfer therefore bypasses this new line. The asynchronous unpack callbacks do reach it. This is a documentation precision issue, not a request to log every destructor/idle cancel.
  • src/core/NvidiaAfxPack.cpp:73: the stall-timeout and checksum/unpack failures remain app-level coverage gaps, correctly disclosed by the author. I independently verified the no-network download failure and missing-archive failure, but did not reproduce a 30-second stall, corrupt download, native Windows/macOS execution, or real-radio packet loss.

What I tried to break

  • Built the exact head with Qt 6.12 on Linux, shared core, ASR/RADE/D-STAR/DFNR disabled. All three newly registered CTests passed. Preflight inspected their sources: meter packets are injected directly without init()/bind, BNR imports only a nonexistent local archive, and the platform test is pure QString logic. No new socket test was built or run.
  • Mutation checks: forcing fellBack() false fails four checks; downgrading the VITA warning to Info fails three integration checks; reverting the missing-archive exit fails its warning check. Restored the source after each mutation. Burst limiting, held-error accounting, default category visibility, signed-query/fragment stripping, and unchanged offline-failure signal text survived the positive checks.
  • Drove three private-network-namespace app sessions through explicit, unique bridge sockets, fresh settings and AETHER_AUTOMATION_NO_TX=1. Each connected only to DEMO-0001: normal offscreen launch logged no fallback, nosuchplugin;offscreen logged a genuine fallback, and the command-line override reproduced blocker 1. All review instances were stopped.
  • A fourth demo session opened AetherRX, selected BNR, clicked Download and accepted the license in the throwaway profile. log tail returned both W aether.nvafx: NvidiaAfxPack: transfer failed: AFX runtime: Network unreachable and W aether.nvafx: NvidiaAfxPack: install failed: download failed (AFX runtime): Network unreachable.
  • Analyzer validation passed 706 checks / 45 rules. Test registration and engine-boundary checks passed (existing boundary warnings only). Required CI checks pass; current main 888ac58bf was committed 2026-10-09T00:35:52Z, before this head's pull-request CI run creation at 02:09:16Z, so the checks are not stale relative to main. GitHub verifies the commit signature. A separate model-invocable automated-review skill was unavailable; this was a manual adversarial pass.

Recommendation: Request changes. Fix the reproduced false diagnosis, then rerun the override and genuine-fallback cases. The VITA and BNR changes otherwise survived the focused tests and demo checks. Formal approval remains the maintainer's decision.

GitHub rejected the formal REQUEST_CHANGES event because the authenticated reviewer account is also this PR's author. This single COMMENT review carries the request-changes recommendation instead.

@ten9876 ten9876 assigned ten9876 and unassigned ten9876 Oct 9, 2026
…og false failures. Principle VIII.

Review follow-up for #6285 (items 1, 2 and 4).

VITA-49: clearRegisteredStreams() and unregister{Pan,Wf,Dax,Iq}Stream()
restart the stream's sequence tracking, so a reconnect or a re-created
stream that reuses an id no longer measures its first packet against the
previous instance's last count and logs a sequence error. The cumulative
per-stream totals behind the status bar and Network Diagnostics are kept.
The line's counts restart with the instance and now read "E since the
stream started (P packets received)": E counts gaps, not lost packets. Its
values are read under m_statsMutex and formatted after it is released.
The 10 s rate limit holds across a restart, so a stream torn down and
re-created over and over logs no more often.

Qt platform: Qt gives -platform (or --platform) precedence over
QT_QPA_PLATFORM. main() now reads it before QApplication consumes it, logs
it as the request ("-platform <value>, on the command line") and compares
the plugin in use with it, so an intentional override is not reported as
a failed plugin.

BNR: fail() strips URL queries from the message it emits as well as the
one it logs, so the panel's "Failed:" text is redacted like the log; the
stripper keeps closing punctuation after a query.

Docs: the vita-sequence-errors sample uses the new wording and consistent
windows; qt-platform-fallback's explanation names -platform; the summary
cites both platform line forms. docs/user/README.md and the /release-prep
snapshot step say to move /next/ rule links to Stable once a snapshot
carries their pages.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up, pushed as d87678cdc (one appended signed commit on 18a772b75). Every finding from both reviews is answered below. Line numbers refer to d87678cdc.

@ten9876's review

Blocker 1: -platform override reported as a fallback. Fixed. Before QApplication consumes the arguments, main() reads -platform / --platform the way QGuiApplicationPrivate does: one - of -- is dropped, the value is the next argument, and the last occurrence wins (src/QtPlatformChoice.cpp:25, src/main.cpp:293). Because Qt prefers that argument to QT_QPA_PLATFORM, it becomes the request that fellBack() compares against. The new QpaRequestSource::CommandLine gets its own summary text (src/QtPlatformChoice.cpp:47, src/main.cpp:645). The QT_QPA_PLATFORM lines are byte-identical. The analyzer summary cites both forms, and the qt-platform-fallback explanation now names -platform.
Proof: offscreen runs of the PR build in a private netns (unshare -n -u --map-current-user) with a fresh profile and no radio, with lines quoted from each run's log file:

QT_QPA_PLATFORM=offscreen  -platform minimal
INF default: Platform: Qt platform plugin "minimal" (-platform minimal, on the command line)
QT_QPA_PLATFORM=offscreen  --platform minimal
INF default: Platform: Qt platform plugin "minimal" (-platform minimal, on the command line)
QT_QPA_PLATFORM='nosuchplugin;offscreen'   (genuine fallback still warns)
INF default: Platform: Qt platform plugin "offscreen" (QT_QPA_PLATFORM=nosuchplugin;offscreen, set by the user)
WRN default: Platform: Qt fell back from "nosuchplugin" to "offscreen"; the first plugin in QT_QPA_PLATFORM did not load
-platform 'nosuchplugin;offscreen'   (genuine fallback via argv)
INF default: Platform: Qt platform plugin "offscreen" (-platform nosuchplugin;offscreen, on the command line)
WRN default: Platform: Qt fell back from "nosuchplugin" to "offscreen"; the first plugin in -platform did not load
QT_QPA_PLATFORM=offscreen   (control)
INF default: Platform: Qt platform plugin "offscreen" (QT_QPA_PLATFORM=offscreen, set by the user)

qt_platform_choice_test has 22 checks and passes. They cover the override (one line, no warning), a command-line fallback, -platform, --platform, the last occurrence winning, and that a trailing -platform and -platformtheme are ignored. Mutations: platformArgument() never returning a value makes 3 checks fail, and dropping the -- handling makes 1 fail.

Nit: cancel claim. The PR body now says this: a cancel that reaches fail("cancelled") (during unpacking) logs install cancelled at Info, and cancelling an active transfer logs nothing, because cancel() disconnects the reply first. No code change, as you asked.

Nit: stall, checksum and unpack coverage gaps. Acknowledged. They are still listed under Not verified in the body.

@aethersdr-agent's review

Blocker 1: m_streamStats survives teardown. Fixed, using a different shape from the one suggested. clearRegisteredStreams() and all four per-stream teardowns, unregister{Pan,Wf,Dax,Iq}Stream(), restart the stream's sequence tracking (PanadapterStream.cpp:458, 468, 515, 1628, 1886; helpers at :471). The restart sets lastSeq to -1 and marks a new instance, so the next packet is not compared with the old count, and the line's counts and window start over. I did not clear the map. packetErrorCount()/packetTotalCount() are summed from it and feed the status-bar seq errors x/y and Network Diagnostics, so clearing it would have made those cumulative totals drop whenever a pan closed. Those totals are kept; the only difference is that a reused id no longer adds a false error on its first packet. Lock order: the per-stream paths take m_statsMutex after m_streamMutex is released. clearRegisteredStreams() nests stream→stats, which is the order resetAudioStreamStats() already uses there, and processDatagram() closes its m_streamMutex scope before it takes m_statsMutex. That is why no new ordering was introduced. The 10 s limit also holds across a restart (VitaSequenceLossReport.h start() no longer resets the last report), so a stream torn down and re-created repeatedly cannot get around the limit.
Proof: vita_sequence_loss_log_test has 33 checks and passes. Each case uses a fresh stream, so no recent report can hide a false line. After clearRegisteredStreams(), a reused id's first packet logs nothing and counts no error, the cumulative total is kept, and the next real gap logs …: 1 in the last 0 s, 1 since the stream started (3 packets received). After each of the four unregister*Stream() calls, a re-created stream counts no error. In the limiter unit, a restart inside the interval is held and the next report counts only the new instance. Mutations: removing the restart from clearRegisteredStreams() makes 3 checks fail; removing it from each unregister* makes that path's check fail; start() resetting the last report again makes 4 fail.

Nit: unredacted finished() text. Fixed in fail() itself, not at one call site (NvidiaAfxPack.cpp:384). The message it emits is stripped the same way as the one it logs, so this also covers the other messages that can carry a URL or a raw error (the no … wheel … at <index> text and the extract and worker errors). The "removes the double-log" aside does not hold: the two lines are deliberate. transfer failed: is one line per network transfer, the same as for a failed lookup, whose panel message has no reason. install failed: reports the outcome. bnr-download-failed and its sample use both. Not covered by a test: no socket-free path puts a URL into fail(). The stripper itself is tested.

Nit: "%5 of %6 packets" misread as a loss count. Fixed (VitaSequenceLossReport.h:59). The line now reads …: N in the last S s, E since the stream started (P packets received), and the comment says that one gap counts as one error. The exact-text tests and the sample were updated. The analyzer rule matches the unchanged prefix.

Nit: comment ahead of the code (:743). Fixed by changing the code to match the comment. The values are read under m_statsMutex, and formatVitaSequenceLoss() now runs after the lock is released (PanadapterStream.cpp:775, :839).

Nit: the stripper eats a trailing delimiter. Fixed (NvidiaAfxPack.cpp:337). Closing punctuation after a query is kept, and brackets inside a query are still removed. There are 3 new checks in nvidia_afx_pack_log_test (10 checks, passes); the old greedy tail fails the punctuation check.

Nit: /next/ links after a snapshot. A procedure note was added in the two places it runs. docs/user/README.md (Log Analyzer) and the /release-prep snapshot step (.claude/skills/release-prep/SKILL.md) both say to drop the /next prefix from rule links in the snapshot commit. Automating that inside snapshot_stable.py is release tooling, so it is left to the maintainer.

Nit: sample window. Fixed. The sample now says 396 s, consistent with the 09:14:05 start line. The audio stream line says 407 s.

Nit: the new tests are not in a CI -R filter. No change, by design. New tests register in tests/tests.cmake, PR CI builds them, and the sanitizer sweep runs the unfiltered tree. A ci.yml edit is out of scope for this PR. All three tests pass locally (above).

Pre-existing addr=/from= host addresses in default logs (PanadapterStream.cpp startup lines, LogAnalyzer/index.js redaction claim). No change: none of those lines are in this diff. Whether to file a separate issue is left to the maintainer.

CodeGuard (36 findings). All false positives, as the bot's review already showed one by one: version strings, regex |, backticks in comments, VITA hex ids, and React JSX props. Nothing to change.

Also checked

  • Tests: vita_sequence_loss_log_test, qt_platform_choice_test, nvidia_afx_pack_log_test, pcm_compatibility_test, flex_backend_lifecycle_test and nvidia_afx_filter_test, run with QT_QPA_PLATFORM=offscreen and ctest --no-tests=error: 6/6 pass. Build: Linux, Qt 6.12.0, the lean review configure.
  • node tools/docs/test_log_rules.mjs: 707 checks pass for 45 rules (cited lines refreshed with --update). gen_reference.py --check passes, as does every other Static checks script. Engine-boundary, command-plane and capability findings at the merge base and at the head differ only by a one-line shift of an existing main.cpp warning. cd docs/user && npm ci && npm run build succeeds.
  • An automated code-review pass on this diff raised two points outside this PR's scope. Both are left to the maintainer: whether -platform should also drive AetherSDR's own Wayland choice and GpuSelector, which ignore it today as they did before this PR; and whether the remote_audio_rx stream, which has no unregister path into PanadapterStream, needs a restart hook.

@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.

1. Issue fit

Yes, for the scope the PR claims. #6285 lists four gaps; this PR takes 1 (VITA-49 sequence errors), 2 (BNR pack failures) and 4 (Qt platform plugin), and the body and the issue thread both carve item 3 out to #6293 — so the partial coverage is declared, not quiet. I checked the three specific rulings from the triage thread and all three are honoured:

  • "use qCWarning on the semantically right category" — aether.vita49 is Q_LOGGING_CATEGORY(lcVita49, "aether.vita49", QtWarningMsg) at src/core/LogManager.cpp:22 and is registered at :77. applyFilterRules() emits no aether.*.info=false, so a Warning reaches a default bundle with no LogManager change. ✔
  • "lcNvAfx has no header declaration, which is the only thing blocking NvidiaAfxPack.cpp" — now declared in src/core/NvidiaAfxFilter.h:13, inside the same HAVE_NVIDIA_AFX guard as the Q_LOGGING_CATEGORY. ✔
  • "three exits bypass fail() and emit finished(false, …) directly" — all three are rerouted: NvidiaAfxPack.cpp:479 (no GPU), :855 (install verification failed), :879 (archive not found). ✔

The one place the PR usefully disagrees with the triage is item 4. Triage ruled the plugin-load failure "structurally unloggable" because QApplication is built at main.cpp:377 and the handler installed at :625. That is true of Qt's own diagnostic, but the PR notices that the fallback is still inferable after the fact by comparing the request to QGuiApplication::platformName(), and logs that. I verified the ordering: platformArgument() is read at main.cpp:293, before QApplication; the log block sits at :642-658, inside if (logManager.startLogging(...)) after qInstallMessageHandler. That is a real advance on the triage's framing, not a workaround.

2. Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/VitaSequenceLossReport.h, PanadapterStream.{h,cpp} New rate limiter + log line; restartStreamSequence() on all five teardown paths Yes In scope
↳ side effect: a reused stream id no longer bumps stats.errorCount / m_catStats[cat].errors Changes the status-bar and Network Diagnostics counters, not just the log Yes — body: "except that a reused id no longer adds a false error on its first packet" In scope, disclosed
src/core/NvidiaAfxPack.{h,cpp}, NvidiaAfxFilter.h Log lines, withoutUrlQueries(), three exits rerouted through fail() Yes In scope
src/QtPlatformChoice.{h,cpp}, src/main.cpp, CMakeLists.txt New app-shell helper + the start-up platform lines Yes In scope
docs/user/src/components/LogAnalyzer/{rules.json,index.js} 5 new rules, 1 summary field, 4 refreshed line citations Yes In scope
docs/user/docs/*.md, docs/user/log-samples/** 3 new troubleshooting sections + 5 samples Yes In scope
.claude/skills/release-prep/SKILL.md, docs/user/README.md The /next/ → Stable link obligation at snapshot time Yes In scope — this PR is the first to use /next/ links (git show 888ac58bf:…/rules.json | grep -c '/next/' → 0; head → 4), so documenting the obligation belongs here

Nothing in the diff is unexplained. CHANGELOG.md correctly untouched. withoutUrlQueries() is new public surface on NvidiaAfxPack but it is a pure static helper, not protocol or config — no maintainer ruling needed.

Socket tests: none added. vita_sequence_loss_log_test constructs a bare PanadapterStream and calls processDatagram() directly via a friend test-access class — init() is never called, nothing binds. Recorded for the §2 obligation, not a finding.

CI: build, check-windows and check-macos were still in_progress at head d87678c when I looked; Static checks, Build docs site, Sanitizer option configures and the three CodeQL lanes are green. I am not reporting this as green. None of the three new tests appear in any ctest -R filter in .github/workflows/ci.yml, so they are compile-gated only on this PR; nvidia_afx_pack_log_test is additionally inside the ENABLE_NVIDIA_AFX AND ((UNIX AND NOT APPLE) OR WIN32) AND x86_64 block, so it never builds on macOS. That matches house practice (full-suite is its own lane), so it is an observation, not an ask.

3. Blockers

None.

4. Nits (none blocking)

  • docs/user/log-samples/rules/vita-sequence-errors.log:11 — this line cannot be produced by the code:

    ... VITA-49 sequence errors on audio stream 0x04000008: 7 in the last 407 s, ...
    

    formatVitaSequenceLoss() formats the id with QString::number(streamId, 16) (VitaSequenceLossReport.h:66), which is unpadded — 0x04000008 would really print as 0x4000008. The analyzer rule matches on the fixed prefix, so nothing fails, but the sample is advertised as real output. Either fix the sample, or pad — padding also matches the 8-digit form Flex's own stream listings use:

            .arg(category, QString::number(streamId, 16).rightJustified(8, QLatin1Char('0')))

    (If you pad, tests/vita_sequence_loss_log_test.cpp:131 and :152/:181 and docs/user/log-samples/rules/vita-sequence-errors.log:9-10 need the same treatment.)

  • src/core/backends/flex/VitaSequenceLossReport.h:13 — kIntervalMs rate-limits per stream, with no global cap. A Flex session routinely carries 10–14 tracked streams (2 pan + 2 waterfall + meter + up to 4 DAX + per-slice audio), so a genuinely lossy link can sustain ~1 warning/second indefinitely. That is the shape that has previously flushed the real cause out of the 100-line bundle tail (the PerfStall frameAge precedent). Worth considering a global floor, or one aggregated line listing the affected streams, rather than N independent limiters.

  • src/core/NvidiaAfxFilter.h:13 / src/core/LogManager.cpp:77 — the body is right that aether.nvafx needs no registry row for visibility (only Debug is wildcard-filtered). But it is still absent from m_categories, so it is the one new line a user cannot see or toggle in the Support dialog's category list, unlike aether.vita49. The triage comment called the row "still worth doing"; adding it is a one-line change if you agree.

  • src/QtPlatformChoice.cpp:14-22 — fellBack() is a one-directional prefix test. It correctly treats wayland → wayland-egl as not a fallback, but the reverse (request wayland-egl, Qt reports wayland) would be logged as a fallback that did not happen. Nobody writes QT_QPA_PLATFORM=wayland-egl in practice, so this is cosmetic; a startsWith in either direction would close it.

  • docs/user/src/components/LogAnalyzer/rules.json:766-773 — bnr-download-failed points at the Stable anchor /bnr-gpu-noise-removal#the-download-fails, but its explanation cites the 30 s stall limit and resume-on-retry, which this PR adds to the Next copy of that section only. A user on the released docs gets a correct link to a page that does not yet say what the rule just told them. Inherent to the snapshot model and self-correcting at the next release — noting it only because it is the one rule whose text and target are out of step.

5. What I tried to break

All of the below is reasoned from reading the code at /tmp/aetherclaude/pr-6308. I did not build or run anything in this repo (the one exception: I ran node tools/docs/test_log_rules.mjs, which passed — 707 checks, 45 rules, confirming the body's figure and that all four /next/ anchors and the Stable one resolve).

  • The redaction regex, read from the raw file rather than the diff. The diff I was handed is sed-mangled (& → a placeholder), so the literal looked like it had single backslashes — which in C++ would make \b a backspace byte and \1 a 0x01, i.e. a redactor that silently does nothing. sed -n '332,340p' src/core/NvidiaAfxPack.cpp shows "(\\bhttps?://[^\\s?#]+)[?#]\\S*?([)\\]}>\"',.;:]*)(?=\\s|$)" with "\\1\\2" — correctly escaped. I then hand-walked the lazy-match backtracking for ?a=(1)&sig=abc (the bracket-inside-query case) and (…?sig=abc). Retry (punctuation-keeping) and both land where the tests assert. The security-audit subagent independently confirmed this and that QString::replace expands \1 (same idiom already at AsyncLogWriter.cpp:310).
  • Whether a signed URL can still reach a log or the panel. All three QNetworkReply::errorString() sinks funnel through logTransferFailure() (strips at :82) or fail() (strips at :383, before both the qCWarning at :387 and the emit finished(false, shown) the panel renders). The file has exactly four logging statements, none of which prints a URL. My one remaining idea — userinfo credentials (https://user:tok@host/) survive withoutUrlQueries() — is covered anyway by urlAuthorityRe in AsyncLogWriter.cpp:314, which already redacts userinfo and host. Refuted.
  • Lock inversion from the new nesting. clearRegisteredStreams() holds m_streamMutex and now calls restartAllStreamSequences(), which takes m_statsMutex — a nested acquisition. I checked every m_statsMutex holder (packetErrorCount/packetTotalCount/categoryStats/audioStreamDiagnostics/recordAudioStreamPacketLocked/the processDatagram block) and none takes m_streamMutex. The order is also pre-established: resetAudioStreamStats() already did exactly this nesting at the same call site before the PR. No deadlock. The four unregister* functions were deliberately restructured to release m_streamMutex before calling restartStreamSequence(), which is the right shape.
  • The three rerouted fail() exits deleting state they shouldn't. fail() runs QFile::remove(m_tmpFile), so rerouting the early-return exits through it could, in principle, destroy a partial download a user meant to resume. It can't: cancel() clears m_tmpFile (:370), it is only ever set at :661 inside an in-flight download, and all three new call sites are early returns before any transfer starts. Harmless.
  • Whether fail()'s new log line fires a wrong analyzer rule. install()'s "no supported NVIDIA GPU found" and installFromFile()'s "archive not found" now emit install failed: lines. Neither matches bnr-download-failed nor bnr-install-failed (both regexes enumerate their suffixes), so no false docs link. The cancelled sentinel compare at :384 matches the three literal fail(QStringLiteral("cancelled")) sites.
  • platformArgument() on hostile argv. - alone (reads arg[1] only after confirming arg[0]=='-', so in bounds), -- (becomes -, no match), -platform as the final argument (i+1 < argc guard), -platformtheme (exact strcmp, not a prefix), and two -platform arguments (last wins, matching the documented Qt behaviour). The test covers all six.
  • Whether nvidia_afx_pack_log_test can even compile. NvidiaAfxPack.h is entirely inside #ifdef HAVE_NVIDIA_AFX, and the new target doesn't set the define itself — but it links aethercore, which declares it PUBLIC at CMakeLists.txt:3241 under a condition character-for-character identical to the tests.cmake guard. Fine. src/QtPlatformChoice.cpp is also pulled into the opt-in spectrum_confirmed_geometry_test (it copies AetherSDR SOURCES); self-contained, so harmless.
  • Attacking the tests. These would not pass against the unfixed code: the reconnect block asserts packetErrorCount() == 0 and the exact warning text after clearRegisteredStreams() with a reused id, and each unregister* has its own assertion — remove restartStreamSequence() from any one of them and that check fails. The limiter block pins the held-count arithmetic (reportErrors() == 1001, lines == 7 over a minute) rather than asserting the implementation back at itself. logLines() is checked against full literal strings, so a wording drift breaks the test and the analyzer rule together. The body's mutation-check list matches what I'd expect these assertions to catch.
  • Doc claims against code. kTransferTimeoutMs = 30000 (:39) backs "stalled for 30 seconds"; unzip (:795) and tar --zstd (:808) are really what unpacks the pack; the menu label is "Network Diagnostics..." (MainWindow_Menus.cpp:376), matching the docs text exactly; AsyncLogWriter::redactPii really does collapse QDir::homePath() to ~ (:198-206), backing the could not install into <path> claim.
  • CodeGuard's 39 findings. All refuted, none folded in. CS-PII-IP/CS-VLN-SSRF-PRIV-10 on NvidiaAfxPack.cpp:49/:272 are the pinned TensorRT/CUDA version strings (10.9.0.34, 12.8.90) read as RFC-1918 addresses; CS-VLN-SSRF-HEX and CS-PII-DL-1 are SHA-256 integrity pins and a file/line/text citation block; CS-VLN-CMDI-PIPE/-BACKTICK are | alternations and backticks inside rule regexes and C++ comments, never shell input; CS-VLN-XSS-EVENT are React onChange/onDrop props (the component has no innerHTML, dangerouslySetInnerHTML, eval or new Function anywhere, and index.js gained exactly one line in this PR); CS-INJ-role_override is ARIA role="note"/"status"/"alert" plus the comment "an explicit override is not a fallback". Credit to security-audit (CodeGuard) for independently walking the leak paths and the regex.
  • What I could not check. Item 1 against real lossy hardware (the PR says so itself under "Not verified"), the stall-timeout branch, Windows/macOS behaviour, and whether Qt 6.12's -platform parser really takes the last occurrence — I verified only that the helper matches the comment describing it. CI's three compile lanes had not finished.

6. Recommendation

Approve with nits. This is unusually well-made work: the body is a verifiable artifact rather than a summary, every triage ruling is honoured with the reason recorded, the one place it departs from the triage (item 4's fallback line) is a genuine improvement on that ruling, the user-visible counter side effect is disclosed rather than buried, and the tests fail for the right reasons. I went after the redaction regex, the lock nesting, the rerouted failure exits and the test strength specifically, and all four held.

Concrete next step: fix the 0x04000008 sample (or pad the hex), and say whether you want the per-stream limiter to grow a global cap now or in a follow-up — neither needs to hold the merge. Thank you for the mutation-check list and the honest "Not verified" section; they made this much faster to review.


🤖 aethersdr-agent · cost: $15.8778 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (39 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:170
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:176
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:192
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:197
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:200
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:594
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:339
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:325
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:326
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:594
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/NvidiaAfxPack.h /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.h:80
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/NvidiaAfxPack.h /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.h:92
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:251
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:844
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/backends/flex/PanadapterStream.cpp /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.cpp:1502
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:261
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:281
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:131
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:139
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:141
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:150
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:188
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/backends/flex/PanadapterStream.h /tmp/aetherclaude/pr-6308/src/core/backends/flex/PanadapterStream.h:361
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/main.cpp /tmp/aetherclaude/pr-6308/src/main.cpp:107
  • [CRITICAL] CS-INJ-role_override — Prompt injection: role override attempt in tests/qt_platform_choice_test.cpp /tmp/aetherclaude/pr-6308/tests/qt_platform_choice_test.cpp:69
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in tests/vita_sequence_loss_log_test.cpp /tmp/aetherclaude/pr-6308/tests/vita_sequence_loss_log_test.cpp:137
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in tests/vita_sequence_loss_log_test.cpp /tmp/aetherclaude/pr-6308/tests/vita_sequence_loss_log_test.cpp:159
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in tests/vita_sequence_loss_log_test.cpp /tmp/aetherclaude/pr-6308/tests/vita_sequence_loss_log_test.cpp:197
  • CodeGuard could not scan 1 file(s): exit 1: Error: failed to init audit store: audit: verify event timestamp retention infrastructure: replace exact retention timestamp triggers: database disk image is malformed (11)

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $16.0395 · model: claude-opus-5

…estarts, and Stable snapshots move rule links. Principle VIII.

Maintainer follow-ups on #6285's review.

-platform: a -platform (or --platform) argument now counts like a
user-set QT_QPA_PLATFORM. AetherSDR no longer imposes wayland;xcb over
it, and GpuSelector's GLX/EGL choice reads the request Qt will actually
use (GpuSelector::requestPicksWayland, tested).

remote_audio_rx: it has no registration in PanadapterStream, so
RadioModel restarts its sequence tracking when the stream is created,
adopted or removed by us. A removal the radio reports already restarts it
through handleDaxRxStreamRegistry's unregisterDaxStream(). New socket-free
remote_audio_rx_sequence_restart_test.

Redaction: the default-level `addr=` and `from=` VITA lines are already
masked by the IPv4/IPv6 shape rules; three negative-corpus rows in
async_log_writer_test pin that. The Log Analyzer's privacy note now says
exactly what the redactor masks.

snapshot_stable.py rewrites each Log Analyzer rule link /next/<page> to
/<page> when the new Stable snapshot has the page; the README and the
/release-prep step drop the manual step.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

The maintainer asked for the four open follow-ups to be folded into this PR. They are in 43ed8bff2, one signed commit appended to d87678cdc.

1. -platform drives AetherSDR's own platform logic.

  • main.cpp now skips the Wayland wayland;xcb override when a -platform/--platform argument is present, just as it does for a user-set QT_QPA_PLATFORM.
  • GpuSelector::applyAtStartup(platformArgument) decides GLX vs EGL from the request Qt will actually use. That decision is the new pure GpuSelector::requestPicksWayland (src/core/GpuSelector.h).

Proof:

  • Offscreen runs in a private netns in a fake Wayland session (XDG_SESSION_TYPE=wayland, WAYLAND_DISPLAY set, no QT_QPA_PLATFORM), with -platform offscreen and with --platform offscreen. Both logs have no Platform: Wayland session … -> QT_QPA_PLATFORM wayland;xcb line, only INF default: Platform: Qt platform plugin "offscreen" (-platform offscreen, on the command line).
  • qt_platform_choice_test has 29 checks and passes; 6 of them are new requestPicksWayland cases (for example, -platform xcb in a Wayland session counts as X11).
  • Mutation: when xcb is not recognised, 2 checks fail.

2. Host addresses in default-level logs.

  • The addr= (VITA bind) and from= (first packet) lines are already redacted on every line by redactPii()'s IPv4/IPv6 shape rules. IPv4 keeps its last octet and IPv6 is replaced, which is what docs/log-redaction.md specifies. The host-keyword table the bot pointed at only covers host names.
  • Three new negative-corpus rows in tests/async_log_writer_test.cpp pin that, using the exact line shapes (IPv4 addr=, IPv4 from=ip:port, IPv6 from=). The test passes.
  • The analyzer's privacy note (docs/user/src/components/LogAnalyzer/index.js) now says exactly what is masked: IPv4 and MAC addresses (all but the last part), IPv6 addresses, serial numbers (all but the last group), passwords, tokens, email addresses and the home directory.

3. remote_audio_rx restart.

  • RadioModel::restartRxAudioSequence() restarts the stream's sequence tracking in three places: the create response (and the replaced old id), status adoption, and removeRxAudioStream().
  • A removal reported by the radio already restarted it: handleDaxRxStreamRegistry() sends every removed stream id to unregisterDaxStream(), and that now restarts.

Proof:

  • The new remote_audio_rx_sequence_restart_test has 4 checks and passes. It drives RadioModel's status router with a socket-free backend and an unopened PanadapterStream.
  • Mutations: with no restart on adoption, 3 checks fail; with no restart in unregisterDaxStream(), the removal check fails.

4. snapshot_stable.py rewrites the rule links. Each "docs": "/next/<page>[#anchor]" whose page is in the new Stable snapshot becomes /<page>[#anchor]. The rewrite edits the text in place, so the file layout is kept. The README and /release-prep notes describe this; the manual step is gone.

Proof:

  • Ran on a scratch copy of the tree: all 4 /next/ links became Stable links, test_log_rules.mjs passed against the new snapshot, and a second run changed nothing.
  • A unit check confirmed that a page missing from the snapshot keeps its /next/ link and that a front-matter slug is honoured.

Also run on 43ed8bff2:

  • These 8 tests pass: vita_sequence_loss_log_test, qt_platform_choice_test, nvidia_afx_pack_log_test, remote_audio_rx_sequence_restart_test, async_log_writer_test, pcm_compatibility_test, flex_backend_lifecycle_test and nvidia_afx_filter_test.
  • Every Static checks script passes.
  • test_log_rules.mjs: 707 checks pass.
  • gen_reference.py --check passes.
  • The engine-boundary, command-plane and capability findings are identical to the merge base.
  • The docs build passes.

…ter aether.nvafx. Principle VIII.

Review of d87678c and the automated pass on 43ed8bf.

RadioModel restarts the VITA-49 sequence tracking of every stream the
radio reports removed, in one place (handleDaxRxStreamRegistry), instead
of special-casing remote_audio_rx on create and adoption: a restart while
a stream is still flowing could hide a real gap. The test now also proves
a gap across an adoption status still counts.

The sequence-error line pads the stream id to 8 hex digits, as the radio
lists it, so the log sample is real output. fellBack() matches the
platform prefix both ways (wayland-egl reports itself as wayland).
aether.nvafx gets a Support-dialog registry row (generated log category
page refreshed). The analyzer's privacy note lists every class the
redactor masks; the ORDER note in main.cpp names the -platform input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Answers to @aethersdr-agent's review of d87678cdc and to the automated code-review pass on 43ed8bff2. The fixes are in e23a4074c, which is signed and appended. The bot had not re-reviewed 43ed8bff2 within 60 minutes of that push.

Bot review of d87678cdc (no blockers)

  • Sample stream id 0x04000008 cannot be printed by the code. Fixed. formatVitaSequenceLoss() now pads the id to 8 hex digits, which is the form the radio lists (VitaSequenceLossReport.h:65), so the sample is real output. A new check covers it, and an unpadded mutation fails it.
  • No global cap on the per-stream limiter. Not changed here. Capping or aggregating changes the line's design, so it has been put to the maintainer. Worst case today is one line per stream per 10 s.
  • aether.nvafx missing from the Support dialog registry. Fixed. The row is in LogManager.cpp as "BNR (NVIDIA AFX)", and log-categories.md was regenerated (gen_reference.py --check passes).
  • fellBack() matched the plugin name only one way. Fixed. It now matches a prefix in either direction (QtPlatformChoice.cpp:14), so wayland-egl → wayland is not reported as a fallback. There is a new check, and a one-way mutation fails it.
  • bnr-download-failed links Stable text that predates this PR. Refuted as inherent to versioned docs. It corrects itself at the next snapshot, and snapshot_stable.py now moves rule links to Stable when that happens.
  • The new tests are in no CI -R filter, and CodeGuard reported 39 findings. The review itself called the first an observation and refuted all 39 findings. No change.

Automated code-review pass on 43ed8bff2

  • The remote_audio_rx test does not guard the create and remove paths, and the restart at create or adoption can hide a gap mid-flow. Fixed by redesigning the restart. RadioModel now restarts sequence tracking only for streams the radio reports removed, in one place (handleDaxRxStreamRegistry, RadioModel.cpp:10696). It no longer restarts at create or adoption. remote_audio_rx_sequence_restart_test checks three things: a gap across an adoption status still counts, a re-created id after removal counts nothing, and a later real gap counts. With the removal restart mutated out, 2 checks fail.
  • The helper restartRxAudioSequence() was public. Removed.
  • The IPv6 corpus comment claimed the port is kept. Corrected: an unbracketed IPv6 addr:port is masked whole, while an IPv4 port stays readable.
  • The privacy note was not exhaustive. Fixed. It now lists every class that docs/log-redaction.md covers.
  • main.cpp's ORDER note named only QT_QPA_PLATFORM. Fixed: it now names the -platform input as well.
  • page_slugs() duplicates readPages(), and only top-level pages count. Refuted. Both read the same top-level tree with the same slug rule, and every rule link targets a top-level page. If they ever disagreed, test_log_rules.mjs would fail the snapshot PR.
  • requestPicksWayland() defers to the session for eglfs and linuxfb. Refuted as unchanged behaviour. This is the same rule willUseWayland() applied before. Only the input changed: it now reads -platform first, and a desktop session never runs eglfs.

Re-run on e23a4074c:

  • These 8 tests pass: vita_sequence_loss_log_test (34 checks), qt_platform_choice_test (30), nvidia_afx_pack_log_test (10), remote_audio_rx_sequence_restart_test (4), async_log_writer_test, pcm_compatibility_test, flex_backend_lifecycle_test and nvidia_afx_filter_test.
  • All 18 Static checks scripts pass.
  • test_log_rules.mjs passes 707 checks.
  • gen_reference.py --check passes.
  • Engine boundary, command plane, capability records and the touchpoint manifest are identical to the merge base.
  • The docs build passes.

@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

Solves #6285 for the three items it claims. The issue lists four gaps; the PR takes 1 (Flex VITA-49 sequence errors), 2 (BNR pack failures) and 4 (Qt platform plugin), and explicitly leaves 3 (HL2 discovery) to #6293 — which matches @ten9876's own comment on the issue and @on8st's claim of that item. Each new string is greppable, cited from rules.json, and node tools/docs/test_log_rules.mjs passes locally (707 checks / 45 rules), which is the sync net the issue asked for.

The PR also follows the triage rulings rather than the issue text: qCWarning(lcVita49) instead of the issue's aether.protocol suggestion (the second triage comment's revised recommendation — and correct, since lcVita49 is declared QtWarningMsg at src/core/LogManager.cpp:22 so a warning passes applyFilterRules() with no defaultOn change), and all three fail()-bypassing exits the triage enumerated are rerouted (NvidiaAfxPack.cpp:479, 855, 879).

One place it goes beyond the triage, correctly: the triage concluded item 4's plugin-load failure was "structurally unloggable" because QApplication is constructed ~240 lines before the handler. The PR instead infers the fallback after the fact by comparing the request to QGuiApplication::platformName(), and is honest in the docs and the rule explanation that Qt's own reason stays on stderr. That is a better answer than the issue got.

Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/{PanadapterStream.*,VitaSequenceLossReport.h} Rate-limited VITA-49 sequence-error line + per-instance restart Yes (item 1) In scope
src/core/{NvidiaAfxPack.*,NvidiaAfxFilter.h} fail() logging, transfer-failure lines, withoutUrlQueries(), Q_DECLARE_LOGGING_CATEGORY Yes (item 2) In scope
src/{QtPlatformChoice.*,main.cpp}, CMakeLists.txt Platform-plugin + fallback lines Yes (item 4) In scope
src/core/LogManager.cpp, docs/user/docs/generated/log-categories.md aether.nvafx registry row Yes In scope. New user-visible surface (a Support-dialog toggle), but the triage explicitly said it was "still worth doing"; python3 tools/docs/gen_reference.py --check confirms the hand-edited generated page matches the generator exactly
src/core/GpuSelector.{h,cpp} willUseWayland() now reads -platform first; requestPicksWayland() extracted to the header Partly Not pure logging — see nit 1. Related, undisclosed behaviour fix
src/models/RadioModel.cpp restartStreamSequence() on every removed stream id Yes (commit 43ed8bf) In scope; covers remote_audio_rx, which has no registration in PanadapterStream
tools/docs/snapshot_stable.py, .claude/skills/release-prep/SKILL.md, docs/user/README.md Release-time rewrite of /next/ rule links to Stable Not in #6285; only in commit 43ed8bf's message Related tooling, not a blocker — see nit 2
docs/user/docs/*.md, log-samples/**, LogAnalyzer/{index.js,rules.json}, test_log_rules.mjs Rules, samples, Troubleshooting entries, summary row Yes In scope
tests/async_log_writer_test.cpp Three negative-corpus rows for pre-existing PanadapterStream address lines No (comment says "#6285 review") Tiny, defensible hardening; not worth unbundling
tests/{vita_sequence_loss_log,remote_audio_rx_sequence_restart,qt_platform_choice,nvidia_afx_pack_log}_test.cpp, tests.cmake Four new tests Yes In scope

No CHANGELOG.md entry — correct. No settings keys, no capability fields, no protocol verbs, no UI defaults touched, so Constitution I/II/III/V/VI and the AGENTS.md persistence/credential rules are not engaged. No preference change: every behavioural delta points at a named issue item or a triage ruling.

Socket-test disclosure (per the reporting rule): this PR adds four tests and none of them open, bind, listen on or connect a socket. vita_sequence_loss_log_test and remote_audio_rx_sequence_restart_test feed hand-built meter packets straight to PanadapterStream::processDatagram() on a stream that never binds; remote_audio_rx_sequence_restart_test uses an IRadioBackend stub with no transport (state-machine injection, not a synthetic firmware peer); qt_platform_choice_test is pure; nvidia_afx_pack_log_test imports a missing archive and never calls install(). I grepped all four for QTcpServer/QUdpSocket/bind/listen/connectToHost/Fake* — the only hits are the word "socket-free" in the header comments. Layer placement matches AGENTS.md ("wire encoding, parsing… a socket-free CTest"). None of the four is in any ctest -R filter in ci.yml, so they compile in the build lane but are exercised only by the full suite — a coverage caveat, not a defect.

Blockers

None.

Nits (all non-blocking)

1. src/core/GpuSelector.cpp:136-145 — the -platform fix is a real behaviour change the body doesn't state.

    const QByteArray request = platformArgument.isEmpty()
        ? qgetenv("QT_QPA_PLATFORM")
        : platformArgument.toLocal8Bit();

Before this PR, a Linux user in a Wayland session who passed -platform xcb got willUseWayland() == true (we read our own wayland;xcb env, which main.cpp set because it had no way to see -platform), so __GLX_VENDOR_LIBRARY_NAME was never applied and PRIME/GLX vendor selection was silently skipped on an XWayland session. After it, main.cpp:296 skips the env override when -platform is present and GpuSelector reads the argument, so that user now gets the GLX hint. That's a correct fix, but it is NVIDIA multi-GPU behaviour, not logging. Worth one line in the body so it isn't discovered later as an unexplained delta.

2. tools/docs/snapshot_stable.py:92-130 — the /next/→Stable rewriter is new release-tool behaviour; please state it in the PR body. It's a genuine consequence of the new rules linking /next/ pages (and README.md + SKILL.md document it), but today it appears only in commit 43ed8bf's message. Two small observations while I was in there, neither needing a change: page_slugs() uses docs_dir.iterdir(), so a rule linking a nested page (/next/foo/bar) is left as /next/ — fail-safe, which is the right direction; and since rules.json is one shared component, after a release the Log Analyzer served under /next/ also links to Stable pages. Both are reasonable trade-offs, just undocumented ones.

3. docs/user/src/components/LogAnalyzer/index.js:134-138 — the privacy paragraph overstates three of its classes.

        AetherSDR already masks personal details in its logs: IPv4 and MAC addresses (all but
        the last part), IPv6 addresses, host names it connects to, radio serial numbers (all
        but the last group), passwords, tokens and keys, names, email addresses, locations and
        grid squares, and your home directory.

The shape-based rules (IPv4/IPv6/MAC/serial/home dir/email) hold unconditionally. But names, locations and grid squares are redacted only as keyword-labelled field values — src/core/LogRedactionPolicy.h:36-42 matches first_name|last_name|full_name|user_name|owner, lat|lon, grid|locator|maidenhead, and docs/log-redaction.md opens by warning that "the guarantee is narrower than 'logs are safe'" for exactly this reason. A bare surname or grid square in prose survives. docs/log-redaction.md also lists callsign as deliberately kept, which this paragraph's framing invites a user to assume otherwise. Suggested wording:

        AetherSDR already masks personal details in its logs: IPv4 and MAC addresses (all but
        the last part), IPv6 addresses, host names it connects to, radio serial numbers (all
        but the last group), passwords, tokens and keys, your home directory, email addresses,
        and names, locations and grid squares where they are labelled as such. Your callsign is
        kept on purpose, so a report can be identified.

4. src/core/NvidiaAfxPack.cpp:715 — a bare hostname escapes both strippers. download failed (%1): %2 can carry a Qt error string shaped Host <name> not found, where the name has no scheme:// prefix and so is touched by neither withoutUrlQueries() nor AsyncLogWriter's URL-authority rule, and sits after no kHostContextKeywords keyword. In practice the reachable names are fixed public infrastructure (github.com, pypi.org, files.pythonhosted.org, pypi.nvidia.com); the only user-specific one is an HTTP proxy host. Strictly better than main, which showed the same message in the panel with no stripping at all — noted for completeness, not asking for a change. (Credit: security-audit (CodeGuard), which flagged this as a residual and declined to raise it as a finding.)

5. Log-volume shape of the VITA line. VitaSequenceLossReport.h:13 rate-limits to one line per stream per 10 s. A multi-slice Flex with DAX can have 6–8 tracked stream ids, so a sustained-loss session can emit ~2,900 lines/hour. That can push the actual cause out of a support bundle's tail (the same failure mode as the frameAge PerfStall floods). A per-stream backoff after the first few reports would keep it diagnosable without crowding; entirely your call.

6. Cosmetic: clean.log:3 reads (QT_QPA_PLATFORM=, unset, Qt default) — the empty value plus the comma is awkward. (QT_QPA_PLATFORM unset, Qt default) would read better on the platform most users are on. Also, the commit message describes the line as "E of P packets since the stream started" while the format is "E since the stream started (P packets received)".

7. src/core/GpuSelector.h:3-4 adds #include <QList>; I couldn't find a QList use the header needs.

What I tried to break

  • Lock ordering on the new m_statsMutex acquisitions. clearRegisteredStreams() (PanadapterStream.cpp:492) calls the new restartAllStreamSequences() while still holding m_streamMutex, whereas all four unregister*() sites deliberately release it first — I expected an ABBA. It isn't one: resetAudioStreamStats() (:1410-1412) already takes m_statsMutex from inside that same block, so streamMutex → statsMutex is the pre-existing order, and I walked the m_statsMutex scope in processDatagram() (:781-836, including recordAudioStreamPacketLocked) to confirm nothing acquires m_streamMutex under it. The reverse order does not exist.
  • The withoutUrlQueries() regex. The diff I was handed showed single-backslash escapes; the real file at NvidiaAfxPack.cpp:337 has "(\\bhttps?://[^\\s?#]+)[?#]\\S*?([)\\]}>\"',.;:]*)(?=\\s|$)" and the replacement "\\1\\2", i.e. correct regex escapes and correct Qt backreferences. I hand-traced five under-strip attempts — query followed by non-whitespace (...?sig=1>rest), two URLs on one line, a query containing brackets, a query at end-of-string, a quote-delimited URL — and the lazy \S*? plus the closing-punctuation class plus the (?=\s|$) anchor strips in all of them; the failure direction is over-stripping, not leaking. tests/nvidia_afx_pack_log_test.cpp:76-91 pins the S3-signature case. Independently confirmed by security-audit (CodeGuard).
  • OperationCanceledError mislabelled as a stall timeout. logTransferFailure() maps it to "transfer timed out (stall limit 30 s)", so a user-initiated cancel could log a bogus timeout. It can't: cancel() (:360) calls m_reply->disconnect() before abort(), so the finished lambda never runs; and the write-failure abort is handled by the m_dlWriteFailed branch at :712, ahead of the error branch. The comment at :67-70 says exactly this and is accurate. kTransferTimeoutMs really is 30000 (:39), matching the docs text.
  • The three emit finished → fail() reroutes (:479 no GPU, :855 verification, :879 archive not found). fail() closes m_dlFile, removes m_tmpFile and clears m_busy — at all three sites those are already in exactly that state, so the only new effect is the log line. The shown == "cancelled" Info branch is live (three callers at :709/735/906).
  • -platform argv parsing vs Qt. Dangling -platform as the last token (ignored, falls back to env — same as Qt), --platform (leading - dropped, matches), -platform xcb:nograb (firstRequested() strips at :), repeated -platform (last wins, matches QGuiApplicationPrivate), -platform "" (treated as "no argument requested", so fellBack() returns false rather than crying fallback). I could not construct an argv where our read and Qt's diverge in a way that produces a wrong log line. The one form I could not verify from here is whether Qt also accepts -platform=xcb; if it does, that spelling would be missed and we'd log the env value as the request.
  • fellBack() false positives. wayland → wayland-egl and wayland-egl → wayland both pass via the bidirectional prefix match; xcb;wayland → xcb is not a fallback (the old substring bug the GpuSelector comment warns about); windows:darkmode=2 → windows passes because of the : strip. source == Unset correctly skips the check entirely, so macOS/Windows default launches never emit a spurious fallback warning.
  • Stream-id reuse, the thing commits d87678c/e23a407 are about. restartStreamSequence() resets only lastSeq/started, not m_lastReportMs, so a stream torn down and recreated in a loop cannot be used to defeat the 10 s rate limit; start() drops errors held from the previous instance; a missing m_streamStats entry is a no-op, which is right because a fresh entry already starts at lastSeq = -1. The restart is on removal only, so it cannot hide a gap in a stream that is still flowing — the RadioModel.cpp:10691 comment makes that claim and the call site matches it.
  • Dead additions / build scope. src/QtPlatformChoice.cpp is referenced by CMakeLists.txt:1691 and tests.cmake:2894; main.cpp is in exactly one target. Q_DECLARE_LOGGING_CATEGORY(lcNvAfx) is at global scope in NvidiaAfxFilter.h:13, matching the global Q_LOGGING_CATEGORY at NvidiaAfxFilter.cpp:12 (no namespace mismatch → no link error), both inside the HAVE_NVIDIA_AFX guard. nvidia_afx_pack_log_test sets no HAVE_NVIDIA_AFX of its own, which looked like a compile break given NvidiaAfxPack.h:3's guard — but CMakeLists.txt:3241 defines it PUBLIC on aethercore, which the test links.
  • CodeGuard leads, refuted. The ~30 CS-VLN-CMDI-AMP/BACKTICK/PIPE criticals land on &&, | and backticks inside C++ comments and on LogManager.cpp's category table — the same harness mangling that rendered the regex above with single backslashes. CS-VLN-XSS-EVENT at index.js:173-203 are React handlers on lines this diff does not touch, rendering JSX text nodes with no dangerouslySetInnerHTML. CS-PII-* and CS-VLN-SSRF-* are the sample logs, screens.json fixtures and redaction test vectors — fixture data by design. CS-SEC-BEARER/BASIC-AUTH/PASSWORD land on LogRedactionPolicy.h's keyword tables, i.e. the scrubber, not a secret. CS-VLN-PATH-UNC in rules.json is a Windows path in a rule's sample text. I confirmed none of these against the change. The security-audit subagent returned no findings after checking seven CodeGuard rule families.
  • Docs/tooling claims I could actually execute: node tools/docs/test_log_rules.mjs → OK: 707 checks passed for 45 rules (this validates every cited file:line + text, every rule's docs anchor, and every sample log, including the five new ones and the new qtPlatform summary row); python3 tools/docs/gen_reference.py --check → up to date, so the hand-edited generated/log-categories.md matches the generator.

Not verified, and why: everything above is reasoned from the code at /tmp/aetherclaude/pr-6308, not reproduced at runtime — I have no build and cannot drive the GUI, so I have not seen a single one of these log lines emitted. At review time CI is incomplete: Static checks, Build docs site, Sanitizer option configures and the three CodeQL analyses are green, but build, check-macos and check-windows were all still in_progress on e23a407. Nothing here says the compile is clean on any platform. I also could not check Qt's -platform= spelling (see above), and the ~2,900-lines/hour volume estimate in nit 5 is arithmetic on the 10 s interval, not a measurement.

Recommendation

Approve with nits. This is careful work: the central choke points the triage identified are all used, the three fail() bypasses are all closed, the two follow-up commits fix exactly the two false-positive classes (reused stream ids, -platform override) that I would otherwise have raised, and the sanitizer is both correct and tested. The new tests are socket-free, correctly layered, and would fail against the unfixed code. Nothing here blocks a merge.

Next step: fold nits 1 and 2 into the PR body (the GpuSelector -platform behaviour change and the snapshot_stable.py link rewriter are the two deltas a reader of the body today would not expect), and take or leave nit 3's privacy wording — that one is the only user-facing accuracy issue. Then wait for build/check-macos/check-windows to land before merging. Thanks for the thorough commit messages; tracing the restart semantics against RadioModel.cpp:10691 was quick because of them.


🤖 aethersdr-agent · cost: $15.3843 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (193 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:80
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:173
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:179
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:195
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:200
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:203
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:216
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:196
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:53
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:155
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:162
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:355
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:128
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:177
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:594
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:339
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:325

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $15.5504 · model: claude-opus-5

… note is exact. Principle VIII.

Review of e23a407. The platform line for an unset QT_QPA_PLATFORM is
"(QT_QPA_PLATFORM unset, Qt default)" instead of "(QT_QPA_PLATFORM=,
unset, Qt default)"; the analyzer summary cites the shared format. The
Log Analyzer's privacy note says names, locations and grid squares are
masked where they are labelled, and that the callsign is kept on purpose.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

These are the answers to @aethersdr-agent's re-review of e23a4074c. The fixes are in 410e5e293 (signed, appended).

  1. The GpuSelector -platform behaviour change was not in the body. Refuted. The PR body has had a section, Behaviour changes beside the log lines, since 43ed8bff2. Its first bullet covers exactly this: -platform drives AetherSDR's Wayland override and GpuSelector's GLX/EGL choice.
  2. The snapshot_stable.py rewriter was not in the body. Refuted. The same section's third bullet covers it, and the Docs list describes the scratch-copy test. Accepted as stated: a nested page is left on /next/, which is fail-safe, and after a release the Next analyzer links to Stable pages.
  3. The privacy note overstates. Fixed with your wording. Names, locations and grid squares are masked "where they are labelled as such", and the note now says the callsign is kept on purpose (docs/user/src/components/LogAnalyzer/index.js:134). The docs build passes.
  4. A bare hostname in a Qt error escapes the strippers. Accepted as a residual, with no change. The names are public infrastructure plus, at most, a proxy host. The text is already better than main, which did no stripping.
  5. Log volume with many streams. Not changed here. A global cap, aggregation or a backoff changes the design of the line, so it has gone to the maintainer as a decision.
  6. (QT_QPA_PLATFORM=, unset, Qt default) reads awkwardly. Fixed. The line is now (QT_QPA_PLATFORM unset, Qt default) (src/QtPlatformChoice.cpp), and clean.log, the unit test and the analyzer summary cite are updated. qt_platform_choice_test (30 checks) passes and test_log_rules.mjs passes 707 checks. The "E of P" wording is only in the original 18a772b75 commit message, which I won't rewrite. The squash commit uses this PR body, which has the current format.
  7. #include <QList> in GpuSelector.h looks unused. Refuted. requestPicksWayland() calls QByteArray::split(), which returns QList<QByteArray>, and then constFirst() on it, so the header needs the type.

-platform=xcb: refuted. QGuiApplicationPrivate's argument loop compares the whole argument against -platform and takes the value from the next argument, so Qt does not accept the = form. Our reader matches. Checked at runtime: with QT_QPA_PLATFORM=offscreen AetherSDR -platform=minimal (offscreen, private netns), Qt loaded offscreen, and the log reads Platform: Qt platform plugin "offscreen" (QT_QPA_PLATFORM=offscreen, set by the user).

Re-run on 410e5e293:

  • The 8 affected tests pass.
  • All 18 Static checks scripts pass.
  • gen_reference.py --check passes.
  • The docs build passes.

@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

Partially, by design — and the partial part is correctly disclosed. #6285 has four items; this PR takes 1 (VITA-49 sequence errors), 2 (BNR pack) and 4 (Qt platform), uses Refs rather than Fixes, and names #6293 as the owner of item 3. I confirmed item 3 is still unlogged at this head (src/core/backends/hl2/Hl2Discovery.cpp — zero qC*/q* calls across 315 lines), so leaving #6285 open is right.

It also follows the triage thread's rulings rather than the issue body's original suggestions, which is the correct reading:

  • Triage comment 2 revised item 1 from lcProtocol to qCWarning(lcVita49) ("packet loss is a warning, so the level is honest rather than a workaround"). The PR uses exactly that; LogManager.cpp:22 confirms lcVita49 is QtWarningMsg, and applyFilterRules() suppresses only .debug, so the line reaches a default bundle with no registry change. ✓
  • Item 2's ruling was: add Q_DECLARE_LOGGING_CATEGORY(lcNvAfx) to a header, qCWarning in fail(), plus the three exits that bypass fail(). All three named bypasses are rerouted — NvidiaAfxPack.cpp:479 (no GPU), :855 (install verification failed), :879 (archive not found). ✓
  • Item 4's ruling was "scope to logging the plugin actually selected; a plugin that fails to load entirely aborts inside the QApplication constructor and is structurally unloggable". The PR does the former and says the latter in docs/user/docs/gpu-rendering.md:124 ("Qt prints why the plugin did not load there, not in AetherSDR's log"). ✓

Coverage: four new registered socket-free CTest targets, and the mutation table in the body is the kind of evidence that makes "the test would fail unfixed" checkable. I read tests/vita_sequence_loss_log_test.cpp line by line — the reconnect/unregister* blocks assert counts that the pre-PR code cannot produce, so they are not implementation-echo tests. No coverage request.

Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/{PanadapterStream.cpp,.h}, VitaSequenceLossReport.h item 1: limiter, log line, per-instance restart yes in scope
src/core/{NvidiaAfxPack.cpp,.h,NvidiaAfxFilter.h,LogManager.cpp} item 2: lcNvAfx header decl, fail() logging, URL stripping, registry row yes in scope
src/QtPlatformChoice.{h,cpp}, src/main.cpp, CMakeLists.txt:1691 item 4: platform line + fallback line yes in scope
src/core/GpuSelector.{cpp,h} -platform now feeds willUseWayland(); requestPicksWayland() moved inline to the header so the test needs no GpuSelector.cpp yes, "Behaviour changes" §1 in scope — and a real fix, not a refactor: pre-PR willUseWayland() read QT_QPA_PLATFORM even when a -platform argument made Qt ignore it, so -platform xcb in a Wayland session dropped the GLX/PRIME hint
src/models/RadioModel.cpp:10691-10696 restart sequence tracking for every removed stream id (covers remote_audio_rx) yes, "Behaviour changes" §2 in scope
docs/user/docs/*, log-samples/*, LogAnalyzer/{index.js,rules.json} 5 rules + samples + 3 troubleshooting sections + summary row + privacy note yes in scope
tests/async_log_writer_test.cpp (+10 lines) 3 negative-corpus rows pinning IPv4/IPv6 masking on the addr=/from= VITA lines yes, "Redaction" § in scope — substantiates the rewritten privacy note rather than asserting it
tools/docs/snapshot_stable.py (+50), docs/user/README.md, .claude/skills/release-prep/SKILL.md, test_log_rules.mjs (+1) /next/<page> rule links become Stable links at snapshot time yes, "Behaviour changes" §3 separable, disclosed — see note below

No undisclosed files, no deleted guards (I read the - lines: the only removals are the inlined willUseWayland() body, now requestPicksWayland(), and the three emit finished(false, …) the triage asked to reroute), no new protocol verb or settings key, no CHANGELOG.md entry, no UI default changed. The release-prep/snapshot_stable.py row is the one thing #6285 does not ask for; it is a consequence of the new rules pointing at /next/ (without it, readers of a release never reach the fixes), and the PR body states it plus its scratch-tree test. Not a blocker, and I would not ask for an unbundle at this size — flagging it so the maintainer can rule.

Socket tests (disclosure, not a finding): the PR adds four test targets and none opens, binds, listens or connects. vita_sequence_loss_log_test and remote_audio_rx_sequence_restart_test construct PanadapterStream with no init() and feed processDatagram() directly; nvidia_afx_pack_log_test imports a missing archive and never calls install(); qt_platform_choice_test is pure. All four are registered in tests/tests.cmake (2877, 2885, 2892, and 2868 inside the ENABLE_NVIDIA_AFX guard) — no # not registered: markers, nothing dead.

Blockers

1. src/core/backends/flex/PanadapterStream.cpp:836-840 — the new warning is rate-limited per stream id, with no global cap, on a socket that does not filter its sender. Credit: security-audit (CodeGuard), codeguard-0-logging; I confirmed each precondition against the head checkout.

    if (reportSequenceLoss) {
        qCWarning(lcVita49).noquote()
            << formatVitaSequenceLoss(streamCategoryName(cat), streamId, lossErrors,
                                      lossWindowMs, lossStreamErrors, lossStreamPackets);
    }

The limiter lives in m_streamStats[streamId] (:785), and m_lastReportMs starts at -1 (VitaSequenceLossReport.h:48), so the first sequence error on every new stream id logs immediately. The 10 s interval only ever throttles an id that has already reported.

Three facts combine:

  • streamId is read straight from the datagram (:660) and nothing in readyRead()/processDatagram() compares dg.senderAddress() against the radio — the only senderAddress() uses are the two diagnostic lines at :625 and :632.
  • Categorisation does not require registration. Your own test proves this: vita_sequence_loss_log_test feeds PCC_METER packets on never-registered id 0x7000000A and gets the warning. So an id nobody registered still lands in CatMeter/CatFFT and still owns a limiter.
  • lcVita49 is QtWarningMsg and applyFilterRules() disables only .debug, so these lines are on by default and go through the rotating log and into the support bundle.

Failure scenario (reasoned from code, not reproduced — I cannot run the app here): any host that can reach the bound VITA UDP port sends pairs of packets with a fabricated streamId and a non-consecutive 4-bit count. Two packets per id ⇒ one unthrottled qCWarning per id, at whatever rate it can send. That is the frameAge-flood shape: the flood is the aftermath, and it pushes the real cause out of the bundle tail. It also grows m_streamStats without bound (one StreamStats, now including a 40-byte limiter, per fabricated id) and the static QSet<quint32> seenIds at :667 alongside it.

The unfiltered socket and the unbounded m_streamStats/seenIds are pre-existing; what this PR adds is the default-on, per-id-unthrottled amplification on top of them. The minimal fix inside this PR's seam is a global bound beside the per-stream one — a shared token bucket for the whole qCWarning site, and/or a cap on how many distinct ids may own a limiter, the way m_orphanStreams already uses kMaxOrphanStreams. A maintainer may reasonably rule that the right fix is the source-address filter instead and downgrade this to a follow-up; I am listing it as a blocker because the merged result is remotely floodable by default and the cap is small.

Nits (non-blocking)

  • src/core/LogManager.cpp:138 — the aether.nvafx registry row is unconditional, but lcNvAfx is declared and defined only under HAVE_NVIDIA_AFX (NvidiaAfxFilter.h:13, NvidiaAfxFilter.cpp:12). On macOS and in -DENABLE_NVIDIA_AFX=OFF builds the Support → Diagnostics list gains a toggle for a category that can never emit. Harmless and consistent with other rows; worth a word in the description if you care.
  • docs/user/src/components/LogAnalyzer/index.js:135-139 — two precision gaps in the otherwise-accurate new privacy note, both from security-audit: opaque tokens keep a 4-character prefix (kOpaqueValueFields), and host masking is keyword-anchored, so a bare hostname in prose is not masked. "passwords, tokens and keys" reads more absolute than the implementation; consider qualifying it the way the names/locations clause already is.
  • src/core/backends/flex/PanadapterStream.cpp:471-479 — restartStreamSequence() runs on the main thread while the UDP thread may still be draining in-flight packets from the old instance. Those re-prime lastSeq after the restart, so the genuinely-new instance's first packet can count one sequence error — the inverse of the false positive you fixed. Rare on a LAN and a clear net improvement; just noting it so "no false failures" is not read as absolute.
  • src/core/NvidiaAfxPack.cpp:337 — the regex is case-sensitive (https?), so an uppercase scheme in a Qt error string would keep its query. No Qt error string I can find produces one; cosmetic.
  • src/core/GpuSelector.h:4 — #include <QList> appears unused (QVector is what the file uses).

What I tried to break

  • Lock ordering / deadlock. restartStreamSequence() takes m_statsMutex; I checked every caller. unregister{Pan,Wf,Dax,Iq}Stream() were all restructured to release m_streamMutex first (:448-469, :1628, :1877-1886) — good. clearRegisteredStreams() does nest m_streamMutex → m_statsMutex at :515, but that nesting already exists at :511 via resetAudioStreamStats(), so no new order is introduced. For a deadlock you would need the reverse; I read the m_statsMutex block in processDatagram() (:782-836) and it acquires nothing else, and :1484/1493/1505/1511 are stats-only. No finding.
  • The limiter arithmetic. startTotalCount = stats.totalCount - 1 after the increment, so lossStreamPackets counts this instance including the current packet — your test's "5 packets received" is right. Traced the steady-error case by hand: 7 lines per 60 s, each carrying the held count, no error silently dropped except those still pending when the stream goes clean (documented, and the live counters keep them). start() deliberately not resetting m_lastReportMs is what stops a teardown/recreate loop from logging every 10 ms; the restart block of the test pins it.
  • platformArgument() vs Qt's real loop. Checked the one-leading-dash drop, -platform taking the next argument, last-occurrence-wins, the trailing--platform guard (i + 1 < argc && argv[i + 1]), and -platformtheme not matching. char** → const char* const* is a valid qualification conversion. Matches QGuiApplicationPrivate's behaviour; -platform=xcb is not a form Qt accepts, so not handling it is correct.
  • The withoutUrlQueries() regex. The diff I was handed had its backslashes stripped (\b, \s, \1 appearing as single-escape), which would have made it ill-formed C++. The real file at :337 is correctly double-escaped and yields a valid PCRE with Qt's \1\2 replacement (not $1$2). I traced it against the realistic shape Error transferring https://host/x.whl?X-Amz-Signature=… - server replied: Forbidden: group 1 stops at ?, the lazy \S*? consumes the query, the greedy punctuation class re-attaches a trailing ).", and the lookahead lands on the space. Not a finding — flagging it because the mangled diff makes it look like one.
  • Leak paths on the new BNR lines. Every fail() argument is self-constructed; the only server-influenced inputs are m_reply->errorString() (:584, :632, :707) and the index URL, and all pass through withoutUrlQueries() before both the log and emit finished(false, shown), so the panel's Failed: text is stripped too. No raw URL is logged anywhere in the file. logTransferFailure() folding OperationCanceledError into the timeout branch is safe because cancel() calls m_reply->disconnect() before abort() (:360-361), so a user cancel never reaches it.
  • The three rerouted fail() exits. install() calls fail() before m_busy = true; fail() only closes m_dlFile, removes m_tmpFile and clears m_busy, all no-ops there, and it deliberately keeps staging so a retry resumes. assembleAndCommit() sets m_busy = false then calls fail(), which sets it again — harmless. The emitted message text is unchanged for all three (no URL to strip).
  • Dead additions / sibling call sites. src/QtPlatformChoice.cpp is compiled in both CMakeLists.txt:1691 and tests/tests.cmake:2894; requestPicksWayland() is header-inline, which is why the test needs no GpuSelector.cpp. applyAtStartup() has exactly one caller (main.cpp:324) and the new parameter defaults, so no sibling is left behind. setBackendForTest/handleStatusForTest already exist (RadioModel.h:1712, :1682). The double restartStreamSequence() on the DAX-removed path (unregisterDaxStream() then RadioModel.cpp:10696) is idempotent.
  • The one thing I actually executed: node tools/docs/test_log_rules.mjs on the head checkout → OK: 707 checks passed for 45 rules, with no "NOTE: cited line number(s) moved". That independently confirms the five new rules compile, fire on their samples, stay silent on clean.log, cite real src/ literals at the exact cited lines, and resolve their docs anchors — including that bnr-download-failed's Stable link /bnr-gpu-noise-removal#the-download-fails exists in the snapshot and that the summary row reads qtPlatform === 'xcb'.
  • CodeGuard leads I refuted (one line each): the CS-PII-IP hits at NvidiaAfxPack.cpp:49/272/311/312/325 are CUDA version strings (12.8.4.1, 11.3.3.83), not addresses; CS-VLN-CMDI-AMP/BACKTICK/PIPE in GpuSelector.*, LogManager.cpp and index.js are &&, backticks and | in comments, C++ expressions and JS template literals with no shell anywhere; CS-VLN-XSS-EVENT in index.js is JSX event props with no innerHTML/eval; CS-VLN-SSRF-*, CS-PII-EMAIL/DL-1 and the CS-INJ-*/CS-SEC-* hits land on screens.json, rules.json, aetherd-touchpoint-tags.json and async_log_writer_test.cpp, i.e. redaction fixtures and detection patterns that must contain the literal shapes they match; CG-EXEC-001 on snapshot_stable.py:72 is list-form argv with no shell=True; the SHA-256 pins at NvidiaAfxPack.cpp:50 are integrity controls, not credentials.

What I could not check. I am headless: no build, no test execution beyond the Node rules harness, no GUI. Every finding above except that harness run is reasoned from the head checkout, not observed at runtime. I cannot reproduce the bridge-automation table in the body, the real-radio VITA-49 path (the PR says so itself), the stall-timeout branch, or Windows/macOS behaviour. CI at review time: Static checks, Sanitizer option configures, Build docs site and the three CodeQL analyses are green; build, check-macos and check-windows were still in progress — the compile of item 2 is only exercised where ENABLE_NVIDIA_AFX is on (ci.yml:269, :688), so the macOS lane does not cover NvidiaAfxPack.cpp at all.

Recommendation

Request changes — on finding 1 alone. Everything else here is unusually strong work: it implements the triage's rulings rather than the issue's first draft, discloses its three behaviour changes, ships a mutation table, and the one check I could run passed 707/707. The single thing I would not merge as-is is the per-id-only rate limit on a default-on warning fed by an unfiltered UDP socket; adding a global cap (or a bound on how many ids may own a limiter) beside the existing per-stream one is a small change in the seam you already built. If the maintainer prefers to treat that as a follow-up against the pre-existing missing sender filter, this becomes Approve with nits. Thanks for the thorough body and the honest "Not verified" section — it made the review much faster.


🤖 aethersdr-agent · cost: $12.9375 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (264 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:80
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:174
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:180
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:196
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:201
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:204
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:216
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:196
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:53
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:155
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:162
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:355
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:128
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:177
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:594
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:339
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:325

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $13.1032 · model: claude-opus-5

…ogether. Principle VIII.

Review of 410e5e2. The per-stream limiter lets the first error of every
stream id log at once, and the VITA socket does not filter by sender, so a
burst of new ids could log one warning per id. VitaSequenceLossBudget caps
all streams together at 8 lines per 10 s; lines over it are counted, and
the next line logged is preceded by "N VITA-49 sequence-error line(s) held
back". The error counters are untouched.

Also: the URL query stripper ignores scheme case, and the Log Analyzer's
privacy note says tokens and keys keep four characters and host names are
masked where a log line labels them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

This answers @aethersdr-agent's review of 410e5e293. The fixes are in 431991323 (signed, appended).

Blocker 1: the warning is rate-limited per stream id only, with no global cap. Fixed.

  • VitaSequenceLossBudget (VitaSequenceLossReport.h:59) is a single limit shared by every stream: at most 8 lines per 10 s. It is checked after the per-stream limiter, under m_statsMutex (PanadapterStream.cpp:809).
  • Lines over the limit are counted. The next line that is logged comes after PanadapterStream: N VITA-49 sequence-error line(s) held back: more than 8 in 10 s across all streams.
  • The error counters (status bar and Network Diagnostics) are unchanged.

Proof (vita_sequence_loss_log_test has 41 checks and passes):

  • The budget unit allows 8 lines in an interval, allows again in the next interval, reports 12 held back and then resets. The held-back line's exact text is also checked.
  • An integration case feeds 20 new stream ids, each with a gap, to processDatagram(). That logs exactly 8 warnings and still counts 20 errors.
  • Mutation: without the budget, the burst check fails (20 lines are logged instead of 8).

What this PR does not change: the socket's missing sender filter and the unbounded m_streamStats/seenIds maps. The review notes these are older than this PR. Each map entry already existed; this PR adds a limiter to each one, not new entries.

Nits:

  • aether.nvafx row exists in builds without AFX. Accepted, no change. The row is harmless, and like other rows in the registry it is not conditional: the generated category page lists it on every platform.
  • Privacy note: tokens keep a 4-character prefix, and host-name masking is keyword-anchored. Fixed. The note now says tokens and keys keep their first four characters, and that host names are masked where a log line labels them (LogAnalyzer/index.js:134). The docs build passes.
  • In-flight old packets can re-prime lastSeq after a restart. Acknowledged as a residual and not changed. It can cost at most one error on the first packet of the new instance, and only on a reconnect race. The test proves the case that counts.
  • The URL regex is case-sensitive. Fixed with CaseInsensitiveOption (NvidiaAfxPack.cpp:338). There is a new check that HTTPS://h/x?sig=abc is stripped, and the case-sensitive mutation fails it.
  • #include <QList> in GpuSelector.h is unused. Refuted, as before. requestPicksWayland() calls QByteArray::split(), which returns QList<QByteArray>, and then calls .constFirst() on it.

CodeGuard reported 264 findings. Same families as before (backticks, &, | and && in comments and regexes; JSX handlers; version strings; fixtures). Refuted; your security-audit pass also returned none.

Re-run on 431991323:

  • These 8 tests pass: vita_sequence_loss_log_test, qt_platform_choice_test, nvidia_afx_pack_log_test, remote_audio_rx_sequence_restart_test, async_log_writer_test, pcm_compatibility_test, flex_backend_lifecycle_test and nvidia_afx_filter_test.
  • All 18 Static checks scripts pass.
  • test_log_rules.mjs passes 707 checks.
  • gen_reference.py --check passes.
  • The docs build passes.

@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.

1. Issue fit

Partially — by design, and the body says so. #6285 lists four unlogged failures; this PR covers items 1 (VITA-49 sequence errors), 2 (BNR pack download/install) and 4 (Qt platform plugin), and leaves item 3 (HL2 discovery) to #6293. #6285 stays open, which is correct. Each item is mapped: item 1 → VitaSequenceLossReport.h + the processDatagram() hook, item 2 → fail()/logTransferFailure() on aether.nvafx, item 4 → QtPlatformChoice + the main.cpp block after qInstallMessageHandler. The issue's ask that each line be "a stable, greppable string, so the analyzer rule can cite its file:line" is satisfied — I ran node tools/docs/test_log_rules.mjs on the PR head and got OK: 707 checks passed for 45 rules.

Coverage: four new socket-free CTest targets, all registered in tests/tests.cmake. I did not ask for a lossy-VITA-replay test or a fake radio — processDatagram() injection is the right socket-free seam for a parsing/rate-limit concern, and the body's disclosure of the bridge gap (sim vita drop, log file) is the honest coverage boundary rather than a hole.

2. Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/VitaSequenceLossReport.h, PanadapterStream.{h,cpp} Item 1: limiter, shared budget, line text, sequence restart on teardown Yes In scope
src/models/RadioModel.cpp (+8) restartStreamSequence() for every radio-removed id (covers remote_audio_rx) Yes In scope
src/core/NvidiaAfxPack.{h,cpp}, NvidiaAfxFilter.h, LogManager.cpp Item 2: failure lines, withoutUrlQueries(), lcNvAfx header decl + registry row Yes In scope
src/QtPlatformChoice.{h,cpp}, src/main.cpp, CMakeLists.txt Item 4: the platform line, -platform parsing Yes In scope
src/core/GpuSelector.{h,cpp} willUseWayland() takes the -platform request; predicate extracted as requestPicksWayland() Yes (body "Behaviour changes") In scope — serves item 4
docs/user/docs/*, log-samples/*, LogAnalyzer/{index.js,rules.json}, generated/log-categories.md Analyzer rules + samples, three troubleshooting sections, summary field, privacy note, generated category row Yes In scope
tools/docs/snapshot_stable.py, docs/user/README.md, .claude/skills/release-prep/SKILL.md Snapshot rewrites /next/ rule links to Stable Yes (body "Docs") In scope but adjacent — supporting tooling the new /next/ rules need; reasonable to keep bundled
tests/* (4 new), tests/tests.cmake, tools/docs/test_log_rules.mjs Coverage for the above Yes In scope

No CHANGELOG.md entry — correct. No new settings key, protocol verb, capability field or dialog. No removed guard: I read every - line; the only deletions are the willUseWayland() body moving into requestPicksWayland() (behaviour-preserving, I diffed the branches one by one) and three emit finished(false, …) calls becoming fail(), which is a superset.

Preference check: the one existing behaviour this touches is that AetherSDR no longer imposes QT_QPA_PLATFORM=wayland;xcb when -platform is present. That is a fix, not a preference — Qt already gives -platform precedence over the environment, so the value AetherSDR wrote was dead for Qt while still steering GpuSelector's GLX/EGL decision the wrong way. It points at an authority (Qt's own argument precedence) and is disclosed in the body.

One note on the audit scope handed to me: the security-audit file list named ThemeManager.*, WsjtxClient.cpp, MainWindow*, DxClusterDialog.cpp, RadioSetupDialog.cpp, Theme.h, ThemeEditorDialog.cpp, PanadapterApplet.h, screens.json, aetherd-touchpoint-tags.json, tailnet_shim_client_test.cpp, theme_manager_test.cpp, unified_title_bar_test.cpp. None of those are in this PR — git diff --stat 888ac58bf..HEAD is 36 files and none of them appear. CodeGuard findings on those paths are not attributable to this change and I have not counted them.

3. Blockers

None.

4. Nits (all non-blocking)

  • src/QtPlatformChoice.cpp:50 and :59 — CR/LF in -platform/QT_QPA_PLATFORM forges an extra log line. Credited to security-audit (CodeGuard), codeguard-0-logging ("strip CR/LF/delimiters"), and confirmed: logLines() interpolates requested verbatim —

            lines << QStringLiteral("Platform: Qt platform plugin \"%1\" (%2)").arg(platformName, request);

    and requested comes straight from argv or the environment (src/main.cpp:651-653). AsyncLogWriter::formatLine() (src/core/AsyncLogWriter.cpp:323-335) applies redactPii() only and does no newline stripping, so QT_QPA_PLATFORM=$'xcb\n[09:14:02.101] WRN default: fake' writes a second, fully attacker-shaped line into aethersdr.log. The trust boundary is weak — only the local user who owns the log can set either — but the docs Log Analyzer is line-parsed, so a forged line in a shared support bundle can fake a finding. The clean fix is central (strip [\r\n] once in formatLine(), covering every logged string), which is bigger than this PR; a local strip in logLines() closes the new path:

        static const QRegularExpression newlines(QStringLiteral("[\\r\\n]+"));
        const QString requested = QString(rawRequested).replace(newlines, QStringLiteral(" "));

    Maintainer's call whether to take the local patch here or route the central one separately.

  • src/core/backends/flex/VitaSequenceLossReport.h:68-82 + src/core/backends/flex/PanadapterStream.cpp:808-809 — when the shared budget denies, the per-stream limiter has already spent its report. The short circuit is

                    if (stats.lossLog.recordError(m_seqLossClock.elapsed())
                        && m_seqLossBudget.allow(m_seqLossClock.elapsed())) {

    recordError() returning true has already done m_pending = 0; m_lastReportMs = nowMs; m_windowStartMs = nowMs;. If allow() then returns false, that error count is dropped on the floor and the stream is muted for another 10 s, so the errors are in neither the suppressed line nor any later line — only the "N line(s) held back" count hints at it. Narrow (needs >8 streams reporting inside one 10 s window) and the live counters still have the truth, but the honest shape is to ask the budget first, or to roll the pending count back when the budget denies.

  • src/core/NvidiaAfxPack.cpp:386 — the cancel path is detected by string compare. if (shown == QLatin1String("cancelled")) couples the Info-vs-Warning choice to a literal in a caller. A bool cancelled parameter (or m_cancelled) is sturdier; as written, a reworded fail("cancelled during unpack") silently becomes a Warning "install failed".

  • docs/user/log-samples/rules/bnr-install-failed.log:8 — the sample's reason is empty: NvidiaAfxPack: install failed: extract failed: with nothing after the colon. It exercises the rule, but a realistic sample carries a reason (e.g. tar: unrecognized option '--zstd'), which is what a reader of the sample is trying to learn.

  • src/QtPlatformChoice.cpp:14-23 — fellBack()'s two-way prefix match has a blind spot. It exists to make wayland ≡ wayland-egl, which is right, but it also means QT_QPA_PLATFORM=minimalegl;minimal landing on minimal reports no fallback ("minimalegl".startsWith("minimal")). Obscure enough to leave alone; worth a comment.

  • src/gui/SpectrumWidget.cpp:1999-2014 — -platform xcb on a genuinely headless Wayland session now takes the "or you forced xcb yourself" arm, whose advice is QT_QPA_PLATFORM='wayland;xcb' — the thing the headless arm above calls "the #4747 bug". That is because the -platform branch no longer runs setDetectedDisplayPresence(). It is exact parity with the pre-existing QT_QPA_PLATFORM=xcb escape hatch, so this PR makes the two consistent rather than introducing anything; naming it only so it is on the record.

  • docs/user/src/components/LogAnalyzer/rules.json:477 — src/models/RadioModel.cpp:12671 is already stale against current main (the string is at :12702 after #6319, ~:12710 merged). No action needed: test_log_rules.mjs:92-101 deliberately treats a moved-but-present citation as a NOTE, not a failure, so Static checks will not redden. Flagging it so nobody chases it, and so the four refreshed line numbers in this diff are understood as cosmetic.

5. What I tried to break

  • The withoutUrlQueries() regex (src/core/NvidiaAfxPack.cpp:332-341). I walked: a query containing dots and =; trailing closing punctuation ((…?sig=1).); two URLs in one string with no whitespace inside either; a URL with no query at all; a URL at end-of-string; mixed-case scheme. \S*? cannot cross whitespace and (?=\s|$) anchors the end, so every expansion either reaches the token boundary or over-strips — I found no input that leaves a query standing. \1\2 is the correct Qt backreference form. The one gap is URL userinfo (https://user:pass@host/…), which [^\s?#]+ admits — but AsyncLogWriter.cpp:313-315's authority rule redacts the whole authority in the log file, so only the same-user GUI "Failed:" string keeps it. The helper is load-bearing, not belt-and-braces: nothing else strips a query, and LogRedactionPolicy.h has no signature/X-Amz-* keyword.
  • Lock ordering around the new m_statsMutex work. clearRegisteredStreams() now calls restartAllStreamSequences() (takes m_statsMutex) while holding m_streamMutex — but the pre-existing resetAudioStreamStats() call two lines above does exactly the same, so the order is established, not new. I grepped every m_statsMutex acquisition in PanadapterStream.cpp (lines 473, 483, 783, 1418, 1490, 1499, 1511, 1517) for the inverse nesting and found none. Note the author deliberately moved restartStreamSequence() outside the m_streamMutex scope in all four unregister*Stream() functions.
  • The lazily-started m_seqLossClock. elapsed() on an invalid QElapsedTimer would be the obvious trap. It cannot happen: the error path requires stats.lastSeq >= 0, which requires a prior packet, which ran the !stats.started branch that starts the clock. restartStreamSequence() sets started = false, and the next packet re-runs that branch.
  • Churning a stream to log faster. VitaSequenceLossLimiter::start() resets m_windowStartMs and drops m_pending but deliberately keeps m_lastReportMs, so a stream torn down and re-created in a loop still cannot log more than once per 10 s. restartStreamSequence() on an unknown id uses find() and creates no entry, so a stray status does not populate the map.
  • platformArgument() edge cases. A bare - (reads the NUL terminator, no overrun), -- → - (strcmp fails), -platform as the last argument (i + 1 < argc guard, and Qt ignores it too), -platformtheme (distinct string), and last-occurrence-wins. Matches QGuiApplicationPrivate's loop as the comment claims. Also confirmed QApplication app(argc, argv) is at src/main.cpp:391, well before the logging block at :645, so QGuiApplication::platformName() is populated and the line never reads plugin "".
  • GpuSelector::applyAtStartup()'s new default argument. src/main.cpp:324 is the only call site in src/ or tests/; the Q_UNUSED guard covers the non-Linux arms. requestPicksWayland() reproduces the old branch set exactly (first contains wayland → true, xcb → false, else session), and QList is needed for QByteArray::split's return, so the added include is not churn.
  • The three emit finished(false, …) → fail() conversions. fail() on a non-busy pack is idempotent: m_dlFile is null, m_tmpFile empty, m_busy = false is already false. No staging is deleted, so the "completed components resume" behaviour is intact.
  • The redaction chain over the new lines. could not install into <path> collapses to ~ via redactPii()'s home rules; the VITA lines carry only a stream id and counters, no addresses; no new line uses a token=/key=/password= keyword that the keyword table would mangle. I checked the privacy note at index.js:132-141 against LogRedactionPolicy.h and AsyncLogWriter.cpp:182-300 claim by claim — IPv4 last octet, MAC last group, IPv6 replaced, serial last group, home → ~, emails, keepPrefixChars = 4 for tokens/keys and 0 for passwords/names/grids, host names only after a connection keyword, callsign kept (docs/log-redaction.md:53). It is accurate. session_id is also masked and the note does not name it; too small to list.
  • Stale-citation failure on the merged tree. Current main is five commits past this PR's base and #6319 added +39 lines to RadioModel.cpp above most of the cited lines. I confirmed from test_log_rules.mjs:90-104 that a moved-but-present citation is a NOTE, not a check(false) — so the 707-pass I got on the PR tree holds for the merge. (I was denied permission to create the scratch merge worktree, so this is read from the checker's logic plus the measured line offsets, not from a merged run.)
  • Socket tests: I grepped the four new test sources for QTcpServer|QTcpSocket|QUdpSocket|QLocalServer|bind(|listen(|connectToHost — no matches. None of them opens, binds, listens or connects; vita_sequence_loss_log_test reaches processDatagram() through a VitaSequenceLossLogTestAccess friend on a PanadapterStream that never binds. Nothing to surface under the socket-test rule, and no fake radio/amplifier/tuner peer was added.
  • CodeGuard classes I refuted (verified line by line in the head checkout): CS-VLN-SSRF-PRIV-10 / CS-PII-IP on NvidiaAfxPack.cpp:49,272,311,312,325 are the NVIDIA component table's version strings (12.8.4.1, 11.3.3.83) and compile-time constant URLs, all SHA-256 pinned with NoLessSafeRedirectPolicy. CS-VLN-CMDI-* on GpuSelector.{h,cpp} and LogManager.cpp are C++ && and markdown backticks — there is no shell anywhere in the diff (QProcess::start(program, QStringList); snapshot_stable.py uses a list argv, no shell=True). CS-VLN-XSS-EVENT on index.js is React onChange/onClick props; every log-derived value renders through JSX text interpolation with no innerHTML/dangerouslySetInnerHTML/eval. CS-VLN-PATH-UNC on rules.json:744,1003 is \\S JSON escaping; CS-PII-DL-1/CS-VLN-SSRF-HEX at :88-89 is the serial/hex-stream-id regex in the rule corpus. CS-SEC-PASSWORD/BEARER/BASIC-AUTH are the redaction-policy keyword tables and analyzer fixture prose — a tree grep for AKIA|ghp_|xox[bp]-|-----BEGIN|sk-[A-Za-z0-9]{20} found nothing.
  • Not checked / out of reach: everything above except test_log_rules.mjs (which I ran) is reasoned from reading the code, not reproduced at runtime — no build, no ctest, no GUI. I could not exercise the stall-timeout, checksum or unpack branches, matching the body's own "Not verified" list. CI at review time is not green yet: only "Build docs site" had completed (success); build, Static checks, check-windows, check-macos, Sanitizer option configures and the three CodeQL Analyze jobs were queued or in progress. The aether.nvafx lines are ENABLE_NVIDIA_AFX-gated, so check-windows (the -DENABLE_ASR=OFF lane) and the macOS lane are the ones worth watching for a configuration-scope break.

6. Recommendation

Approve with nits. This is careful work: the rate limiter is correctly confined to the error path, the shared budget anticipates exactly the stream-id-burst problem that would have flushed the cause out of a support bundle's tail, the lock discipline is deliberate (restart moved outside m_streamMutex in the unregister paths), the sequence-restart-on-teardown reasoning about reused stream ids is right, and withoutUrlQueries() closes a real signed-query leak that nothing else in the redaction chain covered. The -platform correction is a genuine fix rather than smuggled preference, and the privacy note matches the redaction policy claim for claim.

Next step: take or defer the CR/LF strip (nit 1) — that one is a maintainer judgment about whether to patch locally here or centralise in formatLine() — and consider reordering the limiter/budget short circuit (nit 2) so held-back errors are not silently dropped. Neither blocks. Wait for build, Static checks, check-windows and check-macos to finish before merging; nothing green has reported on the compiled lanes yet.

Thanks for the unusually thorough proof table and the explicit "Not verified" section — the mutation checks in particular made it fast to confirm the new tests fail against the unfixed code rather than asserting the implementation back to itself.


🤖 aethersdr-agent · cost: $15.4868 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (414 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:80
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:175
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:181
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:197
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:202
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:205
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:216
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:196
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:53
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:155
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:162
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:355
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:128
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:177
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:595
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:340
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:325

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $15.6523 · model: claude-opus-5

… platform line cannot be split. Principle VIII.

Review of 4319913 (no blockers). When the shared budget refuses a due
line, the stream's errors are now held for its next line instead of being
spent with the report; the budget is asked only when a line is due. A CR or
LF in the -platform or QT_QPA_PLATFORM value is folded to a space, so it
cannot write a forged second line into the log. fellBack() documents the
two-way prefix trade-off, and the bnr-install-failed sample carries a real
reason.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

This answers @aethersdr-agent's review of 431991323, which raised no blockers. The fixes for the real defects among the nits are in 8b56385c5, signed and appended. Per the coordinator's stopping rule, this is the last push in this review cycle.

Fixed:

  • CR/LF in -platform or QT_QPA_PLATFORM forges a log line. logLines() now replaces any run of CR/LF with a space before the value goes into the line (src/QtPlatformChoice.cpp). New check: a request containing a newline still produces one line, with no line break. Mutation: removing the replacement fails that check. A central strip in formatLine() would cover every logged string. That change goes beyond this PR's seam, so it is left as a maintainer call.
  • The budget refusing a due line spent the stream's report. The shared budget is now asked only when the stream's line is due (PanadapterStream.cpp). A refusal keeps the error held for that stream's next line, through recordError(now, mayReport) and due() (VitaSequenceLossReport.h). There are 4 new limiter checks; restoring the old spend-on-refusal behaviour makes 3 of them fail.
  • bnr-install-failed sample had an empty reason. It now reads extract failed: tar: unrecognized option '--zstd'. test_log_rules.mjs passes 707 checks.
  • The fellBack() blind spot is undocumented. It is now documented in the code: minimalegl;minimal landing on minimal is not reported.

Answered, no change:

  • Cancel detected by string compare. The "cancelled" sentinel is the existing contract of fail(). Three callers pass that exact literal, and the same string is what finished() sends to the panel, so it isn't something this PR introduced.
  • -platform xcb on a headless Wayland session takes the "forced xcb" advice arm. Recorded as you note: this is exact parity with the existing QT_QPA_PLATFORM=xcb escape hatch.
  • RadioModel.cpp:12671 is stale against current main. Agreed. It is a NOTE by design in test_log_rules.mjs and refreshes on the next --update.

Re-run on 8b56385c5:

  • These 8 tests pass: vita_sequence_loss_log_test (45 checks), qt_platform_choice_test (31), nvidia_afx_pack_log_test, remote_audio_rx_sequence_restart_test, async_log_writer_test, pcm_compatibility_test, flex_backend_lifecycle_test and nvidia_afx_filter_test.
  • All 18 Static checks scripts pass.
  • test_log_rules.mjs passes 707 checks.
  • gen_reference.py --check passes.
  • The docs build passes.
  • The engine boundary, command plane, capability records and touchpoint manifest checks match the merge base.

@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

Partial by design, and correctly scoped. #6285 lists four unlogged failures; this PR covers items 1 (VITA-49 sequence errors), 2 (BNR pack) and 4 (Qt platform plugin), and the body says item 3 (HL2 discovery) is #6293 and that #6285 stays open. Refs rather than fixes is the right verb. Every log line the body's table promises exists in the tree, each new analyzer rule cites a file:line that really holds the string, and node tools/docs/test_log_rules.mjs passes here with exactly the 707 checks / 45 rules the body claims. The deviation from the issue's suggested aether.protocol to aether.vita49 is explained and better: aether.vita49 is declared QtWarningMsg, so the line reaches a default bundle with no LogManager change.

Coverage: four new socket-free CTest binaries plus three negative-corpus rows in async_log_writer_test. The mutation checks in the body are the kind of evidence that makes a test suite believable, and the tests I read are not self-asserting — qt_platform_choice_test pins exact strings and vita_sequence_loss_log_test pins real numbers (reportErrors() == 1001, lines == 7 over a minute). nvidia_afx_pack_log_test sits inside the ENABLE_NVIDIA_AFX AND x86_64 block, which ci.yml:269 does configure, so it is built in CI.

Socket-test disclosure (§2): none. I grepped every added/modified test for QTcpServer, QTcpSocket, QUdpSocket, QLocalServer, bind(, listen(, connectToHost, Fake* — zero hits. vita_sequence_loss_log_test reaches PanadapterStream::processDatagram() through a friend test-access class on a stream that never binds, and nvidia_afx_pack_log_test only calls installFromFile() on a missing path, never install(). Nothing to flag.

Scope

File / group What it changes Claimed? Verdict
PanadapterStream.{h,cpp}, VitaSequenceLossReport.h item 1: rate-limited sequence-error line, per-stream restart yes In scope
NvidiaAfxPack.{h,cpp}, NvidiaAfxFilter.h, LogManager.cpp item 2: failure lines, withoutUrlQueries(), aether.nvafx registry row yes In scope
QtPlatformChoice.{h,cpp}, CMakeLists.txt item 4: start-up platform line yes In scope
main.cpp, GpuSelector.{h,cpp} behaviour change beyond logging: a -platform argument now suppresses AetherSDR's wayland;xcb imposition and drives the GLX/EGL (PRIME) prediction body yes, #6285 no In scope, and fix-shaped — Qt's documented precedence of -platform over QT_QPA_PLATFORM is the authority, and the old code's only effect under -platform xcb was to mispredict EGL and drop the GLX vendor hint on an actually-X11 session. Named here so the maintainer sees it.
RadioModel.cpp restart sequence tracking for every stream the radio reports removed yes In scope — remote_audio_rx has no PanadapterStream registration, so there is no other seam. Correctly confined to the streamStatusRemoved() branch, so a status for a still-flowing stream never hides a gap.
tools/docs/snapshot_stable.py, docs/user/README.md, .claude/skills/release-prep/SKILL.md release tooling rewrites /next/ rule links to Stable body yes, #6285 no Adjacent scope, disclosed, tooling-only. Coupled to the four new /next/ links, which would otherwise point at Next forever. Not worth unbundling, but a maintainer may disagree.
docs/user/docs/*, log-samples/*, rules.json, index.js, test_log_rules.mjs the analyzer rules, samples, troubleshooting sections, privacy note yes In scope

No CHANGELOG.md entry — correct. No removed guards: the only deletions are the willUseWayland() body (relocated verbatim into GpuSelector::requestPicksWayland, both branch orders preserved) and three emit finished(false, …) exits that now route through fail(), which only adds cleanup it already performed plus the log line. I confirmed every emit finished(false, …) in NvidiaAfxPack.cpp is now the one inside fail().

Blockers

None.

Nits (all non-blocking)

1. src/core/backends/flex/VitaSequenceLossReport.h:85 — the "held back" count reports refused errors, not refused lines.

        if (m_used >= kLinesPerInterval) {
            ++m_suppressed;
            return false;
        }

A stream refused by the budget keeps its due() true (recordError(…, false) does not touch m_lastReportMs), so its next error re-asks the budget and bumps m_suppressed again. With nine streams erroring in one window, the ninth stream's 500 errors contribute 500, and the next line that gets through prints PanadapterStream: 500 VITA-49 sequence-error line(s) held back: more than 8 in 10 s across all streams when about one line was actually held. vita_sequence_loss_log_test.cpp:148 pins takeSuppressed() == 12 for the one-ask-per-line burst, which is exactly the case that does not exercise this. The bound itself is fine — the agent audit and I both traced the worst case to 8 loss lines + 8 notices per 10 s — so this degrades a diagnostic number, not behaviour. The cheapest honest fix is the wording:

inline QString formatVitaSequenceLossSuppressed(int lines)
{
    return QStringLiteral("PanadapterStream: %1 VITA-49 sequence error(s) held back: "
                          "more than %2 line(s) in %3 s across all streams")
        .arg(lines)
        .arg(VitaSequenceLossBudget::kLinesPerInterval)
        .arg(VitaSequenceLossBudget::kIntervalMs / 1000);
}

(If you'd rather keep the line-count meaning, allow() needs to charge a refusal once per stream per window, which is a bigger change than the number is worth.)

2. src/core/backends/flex/PanadapterStream.cpp:786 — pre-existing: m_streamStats is unbounded and keyed by an unvalidated stream id. Credit: security-audit (CodeGuard), confirmed by reading.

        auto& stats = m_streamStats[streamId];

cat != CatCount is reached for any id by two paths with no ownership check — else if (pcc == PCC_METER) cat = CatMeter; keys off the packet class code alone, and isPan/isWf are true for every id while m_knownPanStreams/m_knownWfStreams are empty — and the VITA socket does not filter by sender. I confirmed with git show 888ac58bf:… that both the insert (then line 737) and the CatMeter classification (then line 727) predate this PR, so this is not this PR's bug. What changes is per-entry cost: StreamStats now embeds a VitaSequenceLossLimiter (~40 bytes). The log side is bounded by the new budget, which is the part that mattered. If anyone wants to bound the map, the precedent is in the same file: kMaxOrphanStreams = 32 with LRU eviction on m_orphanStreams. Separate issue, not this PR.

3. src/core/backends/flex/PanadapterStream.cpp:800 — one false sequence error can still survive a teardown. stream removed arrives over TCP while old-instance datagrams may still sit in the UDP receive buffer. Those land after restartStreamSequence(), set started/lastSeq from the old instance, and then the new instance's first packet reads as a gap. The 10 s limiter caps it at one line, and the body's design already accepts "a reused id no longer adds a false error" as a best-effort claim — just not an absolute one.

4. src/core/LogManager.cpp:138 — the aether.nvafx row is unconditional, but nothing writes to it unless HAVE_NVIDIA_AFX. On macOS and any -DENABLE_NVIDIA_AFX=OFF build the Support dialog lists a BNR category that can never produce a line. No other row in the table is #ifdef'd, so this matches house style; mentioning it only in case you'd rather the row be conditional.

5. tools/docs/snapshot_stable.py:92 — page_slugs() uses docs_dir.iterdir(), top level only. The docstring says so, and it fails safe (an unmatched page keeps its /next/ link). A future rule linking /next/generated/<page> would silently never be stabilised. Fine for today's four links.

What I tried to break

  • withoutUrlQueries() — the diff as rendered to me looked like it had lost its regex escaping and an invalid \1\2 replacement. It has not. The file is a non-raw literal compiling to (\bhttps?://[^\s?#]+)[?#]\S*?([)\]}>"',.;:]*)(?=\s|$), and "\\1\\2" is exactly Qt's backreference spelling. I re-implemented the pattern in Python and ran it: Error transferring https://pypi.org/a/b?token=SECRET - server replied: Not Found → query gone; (https://h/x?sig=1). → (https://h/x). with punctuation kept; HTTPS://H/x#frag end → fragment gone; https://h/p and non-URL prose untouched. The lazy \S*? does backtrack to let the closing-punctuation group win. It is also genuinely needed — redactPii()'s URL rule masks only scheme://authority, leaving path and query, and no LogRedactionPolicy.h keyword matches sig=/X-Amz-*.
  • Lock ordering around the new m_statsMutex work. clearRegisteredStreams() nests stream→stats (resetAudioStreamStats() then restartAllStreamSequences(), sequentially, not recursively — no double-lock on the non-recursive QMutex). Every other m_statsMutex site (PanadapterStream.cpp:473, 483, 783, 1422, 1494–1521) takes it alone, and the four unregister*Stream() calls were refactored so restartStreamSequence() runs outside the m_streamMutex block — I checked unregisterDaxStream at line 1638 specifically, since its hunk reads ambiguously in the diff. No stats→stream path exists, so no inversion.
  • platformArgument()'s claim to mirror QGuiApplicationPrivate. The ---stripping, next-argument, last-occurrence-wins and dangling--platform behaviours match Qt's createPlatformIntegration() loop, including that Qt (like this code) does not require the next argument to be a non-option. -platformtheme is correctly not -platform. arg[1] on a bare - reads the terminator, not past the end.
  • fellBack() on the wayland / wayland-egl asymmetry, both directions, plus the documented miss it accepts (minimalegl;minimal landing on minimal). source == Unset correctly suppresses the fallback line so a Qt-default platform never looks like a failure.
  • Forging a log line through QT_QPA_PLATFORM / argv. Only [\r\n]+ is folded, and that is sufficient: the analyzer splits on \r\n?/\n only, so U+2028/NEL/VT cannot start a record, and the [HH:mm:ss.zzz] LEVEL cat: msg format cannot be forged without a newline. A value containing the literal text Platform: Qt fell back from would make the analyzer report a fallback that never happened — but whoever sets the env var or argv already owns the process, so this is not a trust boundary. The line is emitted after qInstallMessageHandler (main.cpp:629 vs :656), so it does pass through redactPii().
  • CodeGuard's leads, all refuted (via the security-audit agent plus my own reading): CMDI on && in C++ boolean expressions and in a build.sh that is not invoked through a shell; snapshot_stable.py's subprocess.run([...], check=True) with a repo-local file:// URI and no shell; XSS-EVENT on JSX onDrop/onChange props — index.js has no dangerouslySetInnerHTML/innerHTML/eval, log text reaches the DOM only as escaped JSX children, and every <Link to=…> comes from build-time rules.json; SSRF on the pre-existing, SHA-256-pinned PyPI resolution this PR only logs; PII/BASIC-AUTH/BEARER on the redaction keyword tables matching themselves. The one lead that survived is nit 2, pre-existing.
  • The privacy note's accuracy (index.js:135-140), claim by claim against redactPii() and LogRedactionPolicy.h: IPv4 last octet, MAC last group, IPv6 full and compressed, Flex serial last group, pass*/name/lat-lon/grid/email keyword fields at 0 kept chars, token/key fields at 4, home-directory collapse, and the correctly-hedged "where a log line labels them as such" for host names. Accurate; it omits session_id (also masked, 4 chars) and that URL authorities are masked wholesale — both over-masking relative to the note, so not misstatements.
  • nvidia_afx_pack_log_test's link line (aethercore Qt6::Core, no HAVE_NVIDIA_AFX, no Qt6::Network) — I expected a link or compile break, since NvidiaAfxPack.h is #ifdef HAVE_NVIDIA_AFX and the constructor builds a QNetworkAccessManager. Both come through: aethercore carries HAVE_NVIDIA_AFX and Qt6::Network PUBLIC. Also checked GUI_SOURCES is an explicit list, so the new CMakeLists.txt:1691 entry is not a duplicate of a glob.

Not verified / limits. I built and ran nothing except node tools/docs/test_log_rules.mjs (707 checks, 45 rules, passed) and a standalone Python re-implementation of the URL-stripping regex. Every other finding above is reasoned from reading the head checkout, not reproduced at runtime. At the time of writing, Static checks, Build docs site, Sanitizer option configures and the three CodeQL Analyze jobs are green on 8b56385; build, check-windows and check-macos were still in progress — in particular I could not confirm MSVC accepts the new QtPlatformChoice.cpp or the constexpr statics in VitaSequenceLossReport.h, though I found nothing in either that looks MSVC-sensitive. The body's own "Not verified" list (no live radio for item 1, the stall and checksum/unpack branches not triggered in-app, Linux-only) is honest and matches what I could see.

Recommendation

Approve with nits. This is an unusually well-evidenced PR: the behaviour changes beyond logging are disclosed in the body, each is fix-shaped with an authority behind it, the new logic is bounded on both the per-stream and all-streams axes, the redaction helper is real rather than decorative, and the mutation-check list is the sort of thing that is hard to fake. I attacked the regex, the lock ordering, the argv parser, the log-forgery surface, the CMake link lines and the privacy note's claims, and the code survived all of them. The one genuine defect in the new logic is nit 1, which prints a wrong number in a diagnostic line and is a one-string fix. Next step: take nit 1 (or consciously keep the current wording), and wait for build / check-windows / check-macos to land before merging. Nice work, and thank you for filing the two automation-bridge gaps instead of working around them silently.


🤖 aethersdr-agent · cost: $14.1648 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (555 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:80
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:175
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:181
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:197
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:202
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:205
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/QtPlatformChoice.cpp /tmp/aetherclaude/pr-6308/src/QtPlatformChoice.cpp:53
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:216
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:196
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:53
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:155
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:162
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:355
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:128
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:177
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [HIGH] CS-VLN-SSRF-PRIV-10 — Vulnerability: CS-VLN-SSRF-PRIV-10 in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:595
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:340
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:49
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:272
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:311
  • [LOW] CS-PII-IP — PII detected: ip_address in src/core/NvidiaAfxPack.cpp /tmp/aetherclaude/pr-6308/src/core/NvidiaAfxPack.cpp:312

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $14.3270 · model: claude-opus-5

… Principle VIII.

Review of 8b56385 (no blockers). A stream the shared budget refuses asks
again on each error, so the count it reports is of errors held back, not
lines. The notice now reads "N VITA-49 sequence error(s) held back: more
than 8 line(s) in 10 s across all streams".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

These are the answers to @aethersdr-agent's review of 8b56385c5, which raised no blockers. Under the stopping rule this is the final push, 77a2404d9, signed and appended.

  1. The "held back" count is errors, not lines. Fixed by changing the wording, as you suggested. The line now reads N VITA-49 sequence error(s) held back: more than 8 line(s) in 10 s across all streams (VitaSequenceLossReport.h), and the comments and local names say errors. vita_sequence_loss_log_test passes 45 checks, including the exact text.
  2. m_streamStats is unbounded and keyed by an unvalidated id. Agreed, this predates this PR. The log side is now bounded by the shared budget. Bounding the map itself is left to the maintainer.
  3. One false error can still survive a teardown race. Agreed; this is a residual that this PR does not fix. The limiter keeps it to at most one line.
  4. The aether.nvafx row is unconditional. It matches house style: no other row in the table is conditional. No change.
  5. page_slugs() reads the top level only. No change: it fails safe, as its docstring says, and all of today's rule links are top-level pages.

Re-run on 77a2404d9:

  • 8 tests pass.
  • All 18 Static checks scripts pass.
  • test_log_rules.mjs passes 707 checks.
  • gen_reference.py --check passes.
  • The docs build passes.

@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

Yes, and unusually precisely. #6285's triage thread landed on a specific implementation shape in its second (self-correcting) comment, and this PR follows it line for line: qCWarning(lcVita49) rather than moving to lcProtocol ("packet loss is a warning, so the level is honest rather than a workaround"); Q_DECLARE_LOGGING_CATEGORY(lcNvAfx) added to NvidiaAfxFilter.h because the missing header declaration was the only thing blocking NvidiaAfxPack.cpp from using it; the registry row for the Support-dialog toggle as a nice-to-have rather than a visibility requirement; and the three emit finished(false, …) exits the triage named at :442/:804/:828 rerouted through fail() (now NvidiaAfxPack.cpp:480, :856, :880). Item 4 is scoped exactly as the triage ruled — log the plugin actually selected, and state in the docs that a plugin which fails to load entirely aborts inside the QApplication constructor before any handler exists. Item 3 (HL2 discovery) is correctly left to #6293/@on8st, so #6285 stays open.

Coverage is real and layer-appropriate: vita_sequence_loss_log_test, remote_audio_rx_sequence_restart_test, nvidia_afx_pack_log_test, qt_platform_choice_test are all socket-free CTests, all four registered in tests.cmake, and all four return g_failed ? 1 : 0 (I checked — a test that prints [FAIL] and exits 0 is green forever, and none of these do). The documented mutation checks line up with what the assertions actually pin.

Socket-test record (§2): this PR adds no socket-owning test. vita_sequence_loss_log_test constructs a PanadapterStream and feeds packets straight to processDatagram() via a VitaSequenceLossLogTestAccess friend — the QUdpSocket member is constructed but never bound, listened on, or connected; remote_audio_rx_sequence_restart_test drives RadioModel's status router. Grepping the four new test sources for bind/listen/connectToHost/QTcpServer/QUdpSocket returns only the word "binds" in a comment. Nothing removed either.

Scope

File / group What it changes Claimed? Verdict
src/core/backends/flex/{PanadapterStream.cpp,.h}, VitaSequenceLossReport.h item 1: rate-limited sequence-error line, per-instance restart yes in scope
src/core/{NvidiaAfxPack.cpp,.h,NvidiaAfxFilter.h,LogManager.cpp} item 2: BNR failure lines, lcNvAfx declaration + registry row, withoutUrlQueries() yes in scope
src/QtPlatformChoice.{h,cpp}, src/main.cpp, CMakeLists.txt item 4: start-up platform line + fallback line yes in scope
src/core/GpuSelector.{cpp,h} -platform now drives the GLX/EGL (PRIME) prediction; requestPicksWayland() extracted yes, under "Behaviour changes beside the log lines" in scope — behaviour change, correct (see below)
src/main.cpp:296 AetherSDR no longer imposes wayland;xcb when -platform is passed yes, same section in scope — behaviour change, correct
src/models/RadioModel.cpp restarts sequence tracking for every removed stream id (covers remote_audio_rx) yes in scope
tools/docs/snapshot_stable.py, docs/user/README.md, .claude/skills/release-prep/SKILL.md snapshot rewrites /next/ rule links to Stable yes, with a described scratch-tree test in scope — tooling the new /next/ rules require
docs/user/docs/*, log-samples/*, rules.json, index.js five analyzer rules + samples, three troubleshooting sections, privacy-note rewrite, four moved file:line citations yes in scope
tests/*, tests/tests.cmake four new tests + async_log_writer_test negative rows yes in scope

Nothing in the diff is unexplained. No CHANGELOG.md entry (correct). No new protocol verb, wire message, config key or settings key; no capability change; no AppSettings.cpp consumer added; no dialog, no new interactive widget — so the persistence, capability, dialog-pattern and a11y canon have nothing to bite on here.

On §6 (preference vs fix): the two behaviour changes are fixes, not preferences, and they can point at an authority. Qt reads QT_QPA_PLATFORM in QGuiApplicationPrivate::createPlatformIntegration() and then overwrites it from the argv loop, so a -platform argument wins — meaning the old willUseWayland() was predicting from a value Qt would discard, and main.cpp was imposing wayland;xcb over an explicit user override. Reading the request Qt will actually use is a correction, not a taste change. Both are disclosed in the body.

Note for the maintainer, not a finding: #6285 carries maintainer-review. The open question the triage flagged ("whether to reuse lcProtocol or register new categories") was answered by the triage's own correction comment, and this PR implements that answer; I don't see a live decision outstanding, but the label is yours to clear.

Blockers

None.

Nits (all non-blocking)

1. src/core/backends/flex/PanadapterStream.h:95 — an in-flight datagram can still seed the new instance, so one false error survives.

restartStreamSequence() runs on the main thread (from RadioModel::handleDaxRxStreamRegistry, PanadapterStream.cpp:10693-side), while processDatagram() runs on the network thread — FlexBackend.cpp:47-48 does new PanadapterStream with no parent and moveToThread(m_networkThread). A datagram belonging to the old instance that is already queued when the stream … removed status is handled arrives after the reset, so it becomes the new instance's "first packet", seeds stats.lastSeq with the old stream's count, and anchors startErrorCount/startTotalCount/the window to a stale packet. The genuinely-first packet of the re-created stream then logs one spurious error.

The mutexes are correct (see below) — this is the seam, not a locking bug, and UDP gives no ordering guarantee against the TCP status line regardless of threading. Bounded to one line and one count per teardown, so commit d87678c's "no longer log false failures" is narrowed rather than eliminated. Worth a sentence in the body rather than a code change.

2. src/core/NvidiaAfxPack.cpp:617 and :655 — two real failure modes are now logged but match no analyzer rule.

fail(QStringLiteral("no %1 wheel for %2 %3 at %4").arg(platformDesc, pkg, ver, indexUrl));
fail(QStringLiteral("no %1 wheel for %2").arg(platformDesc, name));

bnr-download-failed requires download failed ( / PyPI lookup failed for / simple-index lookup failed for / write failed for / cannot write cache; bnr-install-failed requires one of the extract/assemble/verify prefixes. "The server no longer publishes the pinned wheel for your platform" is exactly the kind of silent dead end #6285 is about, and it falls between the two rules — as does archive not found (:880). A low-severity catch-all on the bare NvidiaAfxPack: install failed: prefix, or one more alternation branch, would close it.

3. docs/user/docs/bnr-gpu-noise-removal.md:113-120 — the "assembled pack missing" remedy can never work.

The section lists assembled pack missing among the four messages it covers, but the literal is assembled pack missing %1 (AFX-bits archive not published yet?) (NvidiaAfxPack.cpp:825) — a server-side publication gap. Installing unzip/zstd, freeing disk and clicking Download again cannot fix it. Either split it into its own line ("the archive is not published yet; try again after the next release") or drop it from that list and give it a rule of its own.

4. docs/user/docs/low-bandwidth-connections.md:171 — understates the rate limit.

at most once every 10 seconds for each stream

True per stream, but the global VitaSequenceLossBudget cap (8 lines per 10 s across all streams, plus the … held back: more than 8 line(s) in 10 s across all streams notice) isn't mentioned, so a user who sees that notice has nothing in the docs to match it against. One clause would do it.

5. src/core/backends/flex/VitaSequenceLossReport.h:70-71 — the burst cap bounds bursts, not the sustained rate.

The budget's stated purpose is to stop lines pushing "the cause out of a support log's tail", but 8 lines / 10 s is 48 lines a minute sustained — a ten-minute lossy session can put several hundred warnings in the log, and the bundle tail is 100 lines. Five categories at the per-stream 1-per-10 s rate gets to ~30/min on its own. Not a defect, and the counters remain honest, but if the goal is tail legibility, a back-off after N consecutive reporting windows (or a widening interval) would get closer to it. Judgment call — flagging the reasoning, not prescribing a change.

6. src/main.cpp:324 — -platform "" is treated two ways.

g_qpaPlatformArgument is truthy on an empty-string argument, so main() skips the wayland;xcb imposition, but value_or(QString()) collapses "absent" and "empty" for GpuSelector, which then falls back to QT_QPA_PLATFORM / the session. Degenerate input, harmless either way; mentioning it only because the two call sites disagree on what an empty -platform means.

7. src/models/RadioModel.cpp:10694 — redundant with the unregisterDaxStream() change.

m_panStream->unregisterDaxStream(stream.streamId) on the line above now calls restartStreamSequence() itself (PanadapterStream.cpp:1638), so the explicit call double-restarts for dax_rx ids. Idempotent, and the comment explains why the explicit call exists at all (remote_audio_rx has no registration) — so this is a readability note, not a bug.

Security audit

Ran the security-audit subagent over the flagged file set at /tmp/aetherclaude/pr-6308. 0 confirmed defects, 2 defence-in-depth leads, both about unchanged code, so neither is a change request against this PR:

  • src/core/AsyncLogWriter.cpp:328 — formatLine() runs redactPii(message) and nothing else; there is no central CR/LF strip before the [HH:mm:ss.zzz] LVL cat: msg\n line is written, so log-injection safety is per-call-site. This PR gets its own site right (QtPlatformChoice.cpp:53-54 folds [\r\n]+ out of the argv/env-derived request before it reaches a .noquote() stream, and analyze.mjs splits on /\r\n?/ only), but a structural strip inside formatLine() would make the guarantee hold for every future .noquote() site. Maintainer call, separate PR.
  • src/core/NvidiaAfxPack.cpp:332-341 — withoutUrlQueries() is the right control, correctly placed on all four errorString paths (logTransferFailure at :82, used from :588/:636/:715, plus fail() at :384 covering the download failed (%1): %2 message) and covered by nvidia_afx_pack_log_test.cpp:77-92. But it is site-local for a global gap: redactPii() strips only the URL authority (AsyncLogWriter.cpp:313-315) and leaves path and query intact, so other subsystems logging a URL with a signed query are unredacted (src/asr/AsrModelManager.cpp:212, src/asr/RemoteAsrBackend.cpp:43 show the shape). Whether the query strip belongs in redactPii() is a maintainer decision; nothing leaks from the changed code.

The audit also confirmed the privacy paragraph at docs/user/src/components/LogAnalyzer/index.js:132-141 matches redactPii() field for field against LogRedactionPolicy.h and docs/log-redaction.md (IPv4 last octet, MAC last pair, IPv6 whole, serial last group, password keepPrefixChars=0 vs token/key =4, home dir, bare email, labelled host/name/location/grid, callsign deliberately kept). It understates in one direction only — URL authority is redacted unconditionally and the note doesn't say so — and never overclaims. Good.

On the CodeGuard list specifically: most of it is noise against this PR. Nineteen of the flagged hits are in files not in git diff main...HEAD at all — .claude/skills/tag-release/scripts/check_release_assets.py, .github/workflows/windows-installer.yml, docs/architecture/aetherd-touchpoint-tags.json, docs/user/screens.json, and the entire tools/flex-tailnet-shim/ tree (the harness scanned the whole checkout, not the change). Of the hits that do land on changed files:

  • CS-VLN-CMDI-AMP/-BACKTICK on GpuSelector.h:48,53, LogManager.cpp:155,162, GpuSelector.cpp:196,216 — refuted: these are C++ &&/& operators and Markdown backticks in comments, not shell.
  • CS-VLN-XSS-EVENT ×5 on LogAnalyzer/index.js:175-205 — refuted: grep dangerouslySetInnerHTML returns nothing; the component renders {x.text}/{f.title}/{f.explanation} as React text nodes. Those lines are also untouched by this diff.
  • CS-VLN-PATH-UNC on QtPlatformChoice.cpp:53 and rules.json:744,1003 — refuted: the first is the --→- argv handling, the latter two are Windows paths inside rule/sample strings.
  • CS-SEC-PASSWORD/BEARER/BASIC-AUTH and CS-PII-DL-1 inside rules.json — refuted: those are the analyzer's own detection regexes.
  • CS-VLN-SSRF-* on the download paths — refuted: every QNetworkRequest in NvidiaAfxPack.cpp (:577, :625, :674) sets NoLessSafeRedirectPolicy, the hosts are hard-coded https:// literals, and every artifact is checked against a pinned or index-declared sha256 at :719-724.

What I tried to break

  • Lock inversion on the new m_statsMutex paths. The modified unregister{Pan,Wf,Dax,Iq}Stream() all release m_streamMutex before restartStreamSequence() takes m_statsMutex, but clearRegisteredStreams() calls restartAllStreamSequences() while holding m_streamMutex — so I went looking for the reverse order. It isn't new: clearRegisteredStreams() already called resetAudioStreamStats() (which takes m_statsMutex) inside the same scope, and the m_statsMutex holders at :1422/:1494-1521 don't nest m_streamMutex inside. Order is streamMutex → statsMutex throughout; no inversion introduced. The sequence-loss line is also formatted and emitted after the stats lock's scope closes (:843-853), so no logging under the lock.
  • Log injection through the platform line. QT_QPA_PLATFORM and -platform are the only attacker-ish inputs here, and the line is .noquote(). logLines() folds [\r\n]+ to a space before firstRequested() is derived from it, so neither the plugin line nor the fallback line can be split; platformName is Qt's own plugin string. Confirmed, and the security audit independently reached the same place.
  • withoutUrlQueries() on adversarial text. Walked the lazy \S*? + punctuation-class + (?=\s|$) construction by hand on (https://h/x?sig=1&v=1.2)., "https://h/x?token=a", ?a=(b)c, and a URL mid-sentence followed by a comma — each yields the URL with closing punctuation kept and the query gone. Then I checked the one shape it misses: a URL with user:pass@host userinfo and no query is untouched — but docs/log-redaction.md says redactPii() already redacts the URL authority unconditionally, so that's covered downstream. No gap.
  • platformArgument() vs Qt's real parser. Compared against QGuiApplicationPrivate's loop: one leading - of -- dropped, value is the next argv entry, last occurrence wins, a trailing -platform with no value consumes nothing, -platformtheme is not a prefix match. All four hold; arg[1] on a bare "-" reads the terminator, which is legal. Qt has no -platform=value form either, so there's nothing missed there. I also confirmed Qt's argv loop runs after the QT_QPA_PLATFORM read in createPlatformIntegration(), which is what makes the precedence claim true.
  • fellBack() false positives. wayland;xcb→xcb reports; wayland→wayland-egl and wayland-egl→wayland don't (the two-way prefix test); minimalegl;minimal→minimal doesn't, which the comment at QtPlatformChoice.cpp:16-18 names as the known cost. The source != Unset guard means an unset QT_QPA_PLATFORM never produces a fallback warning. No spurious-warning input found.
  • A cancel misreported as a stall. logTransferFailure() maps OperationCanceledError to "transfer timed out (stall limit 30 s)", which would be a lie if a user-initiated cancel could reach it. It can't: cancel() calls m_reply->disconnect() before abort(), and the lookup lambdas at :588/:636 hang off that same m_reply, so they never fire on cancel. The write-failure abort() is checked via m_dlWriteFailed before the network-error branch at :713. kTransferTimeoutMs is 30000, matching the docs. The comment's claim holds.
  • The limiter/budget interaction. mayReport = !due() || allow() charges the shared budget only when a line would actually be emitted or refused, so a clean-but-chatty stream never spends it; a refused line keeps m_pending and leaves m_lastReportMs, so the errors reach the stream's next line. One by-design consequence: errors refused by the budget are counted in the held-back notice and again in that stream's next per-stream line. Cosmetic, mentioned for the record.
  • The "one place" claim for remote_audio_rx. RadioModel::onStatusReceived() (:10753-10755) calls handleRemoteAudioRxStreamStatus() and handleDaxRxStreamRegistry() unconditionally for every stream … status, and the registry handler routes removals by id with no type= filter — so the restart really does cover a stream with no PanadapterStream registration. Verified in the router, not just in the test.
  • Dead additions and build scope. src/QtPlatformChoice.cpp is registered on the AetherSDR target (CMakeLists.txt:1691) and compiled directly into qt_platform_choice_test (tests.cmake:2892-2895); GpuSelector::requestPicksWayland is header-inline, which is why that test links only Qt6::Core and doesn't need aethercore. applyAtStartup( has exactly one caller (main.cpp:324), so the default argument hides no site now silently passing an empty string. All four new tests are in tests.cmake with add_test.
  • The rule citations — this one I actually ran. node tools/docs/test_log_rules.mjs on the head checkout: 707 checks passed for 45 rules. That verifies every cited file:line in rules.json still resolves to the quoted text, including the four citations this PR moved (PanadapterStream.cpp 221→234 and 175→188, 1686→1766; RadioModel.cpp 12653→12671; main.cpp 80→84) and the new QtPlatformChoice.cpp:57,74 and VitaSequenceLossReport.h:125. It also proves rules.json parses, which the diff's rendering of \(/\S had made ambiguous.
  • Tests that would pass against the unfixed code. Each new test's main() returns g_failed ? 1 : 0, so a [FAIL] is a real failure. The assertions are behavioural rather than self-referential — e.g. "burst of new ids: lines stop at the shared budget" compares g_warnings.size() against kLinesPerInterval while separately asserting packetErrorCount() == 20, which would fail both with no budget (20 lines) and with a budget that ate the errors.
  • What I could not check. Everything above is reasoned from the code and from the one tool run I named; I have no build and no runtime, so I did not reproduce a single log line. I could not verify the body's Linux bridge transcript, the TimeoutError probe, or the untriggered checksum/unpack/stall paths — the body already labels those as unverified, which is the right disclosure. Windows and macOS behaviour is unexercised by anything I can run; check-windows does compile QtPlatformChoice.cpp via the AetherSDR target, so MSVC coverage is pending rather than absent (_USE_MATH_DEFINES/NOMINMAX-class breaks don't apply — no math constants, no windows.h). On CI at 77a2404: Static checks, Sanitizer option configures, Build docs site and all three CodeQL analyses are green; build, check-macos and check-windows were still in progress when I looked, so the compile verdict is genuinely unknown, not green. The branch is also ~5 commits behind main (base 888ac58bf); CI tests the merge, so that's informational.

Recommendation

Approve with nits, contingent on the three in-flight build jobs going green.

This is the most carefully-built PR of its kind I've reviewed on this repo: it implements the triage's corrected ruling rather than its first draft, discloses its two real behaviour changes in a dedicated section instead of burying them, documents what it did not verify, and backs the tests with mutation checks that the assertions bear out. The hostile passes I care about most — lock ordering, log injection, the redaction regex, Qt's own argv precedence, and whether a cancel can be mislabelled a stall — all survived. The test_log_rules.mjs run is independent confirmation that no citation drifted.

Nothing here should hold the merge. Concrete next step: wait for build/check-macos/check-windows, then fold nits 2-4 (the unmatched no … wheel / archive not found failures, the unfixable "assembled pack missing" remedy, and the missing global-cap clause in low-bandwidth-connections.md) into this PR if convenient — they're all one-liners in files already touched. Nits 1 and 5-7 are judgment calls worth a reply in the thread rather than a change. The body-disclosed pre-existing bug (updateBnrStatus() overwriting the Failed: <msg> label right after it is set, so the reason for a failed download appeared nowhere before this PR) deserves its own issue, not a fix here. Thanks for the unusually honest "Not verified" and "bridge gaps" sections — the sim vita drop and log file [grep] proposals are worth issues of their own.


🤖 aethersdr-agent · cost: $17.3145 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (566 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:70
  • [LOW] CS-PII-IP — PII detected: ip_address in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:242
  • [LOW] CS-PII-IP — PII detected: ip_address in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:242
  • [MEDIUM] CS-MAL-SCRIPT — Script detected: env python in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:0
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:416
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:239
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:249
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:321
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:345
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:619
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:62
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:80
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:175
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:181
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:197
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:202
  • [HIGH] CS-VLN-XSS-EVENT — Vulnerability: CS-VLN-XSS-EVENT in docs/user/src/components/LogAnalyzer/index.js /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/index.js:205
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:89
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:744
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:1003
  • [HIGH] CS-PII-DL-1 — PII detected: drivers_license in docs/user/src/components/LogAnalyzer/rules.json /tmp/aetherclaude/pr-6308/docs/user/src/components/LogAnalyzer/rules.json:88
  • [HIGH] CS-VLN-PATH-UNC — Vulnerability: CS-VLN-PATH-UNC in src/QtPlatformChoice.cpp /tmp/aetherclaude/pr-6308/src/QtPlatformChoice.cpp:53
  • [CRITICAL] CS-VLN-CMDI-PIPE — Vulnerability: CS-VLN-CMDI-PIPE in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:216
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.cpp /tmp/aetherclaude/pr-6308/src/core/GpuSelector.cpp:196
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in src/core/GpuSelector.h /tmp/aetherclaude/pr-6308/src/core/GpuSelector.h:53
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:155
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/LogManager.cpp /tmp/aetherclaude/pr-6308/src/core/LogManager.cpp:162

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $17.5004 · model: claude-opus-5

@ten9876
ten9876 merged commit 50a8097 into main Oct 9, 2026
10 checks passed
@ten9876
ten9876 deleted the fix/6285-log-analyzer-gaps branch October 9, 2026 07:18
@ten9876

ten9876 commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

Answers to @aethersdr-agent's review of 77a2404d9, which raised no blockers. Under the coordinator's stopping rule, the one fix push after a blocker-free review was already spent on 8b56385c5 → 77a2404d9. This round is answered here and the open items go to the maintainer. I am not pushing again.

  1. An in-flight datagram can seed the new instance. Agreed, this is a residual. UDP is not ordered against the TCP status line, so the race costs at most one count and one line per teardown. The body's claim is best-effort.
  2. "No wheel" and "archive not found" match no analyzer rule. This is a real coverage gap in the rules. It is a docs-and-rules change only, and it goes to the maintainer as a fold-in or a follow-up.
  3. The "assembled pack missing" remedy can't fix a publication gap. Agreed, the docs are wrong here. It goes to the maintainer together with item 2.
  4. The docs don't mention the cap across all streams. Agreed: one docs clause is missing. It goes in the same bundle as items 2 and 3.
  5. The cap bounds bursts, not the sustained rate. This is a design call: a backoff or a widening interval. It goes to the maintainer.
  6. -platform "" is treated two ways. This is degenerate input, harmless either way, as you note. No change.
  7. RadioModel.cpp:10694 is redundant with unregisterDaxStream(). That is deliberate. The restart on removal lives in one general place for every stream type, so it doesn't depend on unregisterDaxStream() keeping its non-DAX side effect. The automated pass on 43ed8bff2 asked for exactly this.

CI on 77a2404d9 is green: build, check-windows, check-macos, Static checks, the docs build, the sanitizer configure and the three CodeQL lanes. Every commit is verified, and there are no open review threads.

@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.

Reviewed at the PR head 77a2404d9 against base 888ac58bf, with the head checkout at hand. Everything below is reasoned from the code — I have no build and no test run here, so nothing in this review was reproduced at runtime.

1. Issue fit

Yes, for the scope the triage of #6285 reduced it to. The issue listed four gaps; this PR takes 1, 2 and 4 and explicitly leaves 3 (HL2 discovery) to #6293, which @on8st claimed in the thread — that split is disclosed in the commit body and matches the maintainer-review triage.

Requirement-by-requirement:

#6285 item Triage ruling Where the diff does it
VITA-49 sequence errors, rate-limited "use qCWarning(lcVita49)… packet loss is a warning, so the level is honest" VitaSequenceLossReport.h:1-135 + PanadapterStream.cpp:775-852, qCWarning(lcVita49) ✔
…"rate-limit off m_streamStats[id].errorCount, not the global accessor" per-stream, not packetErrorCount() per-stream StreamStats::lossLog ✔
BNR failures: declare lcNvAfx in a header NvidiaAfxFilter.h had no Q_DECLARE_LOGGING_CATEGORY NvidiaAfxFilter.h:13 ✔
BNR: fail() + "three exits bypass fail()" (:442, :804, :828) reroute them NvidiaAfxPack.cpp:480 (no GPU), :856 (verification), :880 (archive not found) — and grep "emit finished(false" now returns exactly one hit, inside fail() ✔
Registry row "worth doing so the Support dialog gets a toggle" optional LogManager.cpp:138 + regenerated log-categories.md ✔
Qt platform: unconditional platformName() line yes main.cpp:645-658 ✔
Qt platform: "diagnose a plugin that failed to load is out of reach" out of scope the PR infers the fallback by comparing request vs platformName(), and both the code comment (QtPlatformChoice.h:17-19) and the docs say Qt's own reason stays on stderr. Honest, and more than the triage asked for ✔

I verified the corrected triage premise myself before accepting the category choice: LogManager::applyFilterRules() (src/core/LogManager.cpp:208-227) emits only aether.*.debug=false plus per-category debug/info enables, so adding aether.nvafx to m_categories while it defaults off (loadSettings(), :373-378) does not suppress its qCWarning. The new lines are visible in a default support bundle. That was the trap worth checking (the "TCI logs absent from bundles" pattern) and it does not bite here.

Tests: three new socket-free CTests plus one regression test, all registered in tests/tests.cmake, and each would fail against the unfixed tree because the code they exercise does not exist there. tests/vita_sequence_loss_log_test.cpp:68-120 is a real limiter spec (interval holding, held-error accounting, restart-across-interval), not an implementation echo. Coverage is adequate.

2. Scope

Note first: the file list I was handed is incomplete — it omits tests/tests.cmake, three of the four new test files, tools/docs/snapshot_stable.py and tools/docs/test_log_rules.mjs. The table below is built from git diff 888ac58bf..77a2404d9 --stat (36 files, +1510/−57).

File(s) What it changes Claimed? Verdict
src/QtPlatformChoice.{h,cpp}, CMakeLists.txt:1690, src/main.cpp the start-up platform lines yes in scope
src/core/GpuSelector.{h,cpp} willUseWayland() now takes the -platform argument; list-first logic extracted to requestPicksWayland() commit 43ed8bf only in scope — -platform beats QT_QPA_PLATFORM, so the old env-only prediction was wrong once we log the real plugin. Behaviour-preserving: I diffed the extracted predicate against the deleted body line for line
src/core/backends/flex/VitaSequenceLossReport.h, PanadapterStream.{h,cpp} the sequence-error line, limiter, global budget, instance restart yes in scope
src/models/RadioModel.cpp (+8) restartStreamSequence() on every removed stream id; one comment commit 43ed8bf/e23a407 in scope (covers remote_audio_rx, which has no registration in PanadapterStream)
src/core/NvidiaAfxPack.{h,cpp}, NvidiaAfxFilter.h, LogManager.cpp BNR failure logging, withoutUrlQueries(), category yes in scope
tests/* (4 new + async_log_writer_test.cpp +10) coverage; the async_log_writer_test rows are a negative corpus for the VITA lines' addresses partly in scope
docs/user/docs/*, log-samples/*, LogAnalyzer/{index.js,rules.json}, test_log_rules.mjs 5 rules, 5 samples, 3 troubleshooting sections, a summary row, and the rewritten privacy note yes in scope
tools/docs/snapshot_stable.py (+50), docs/user/README.md, .claude/skills/release-prep/SKILL.md new: snapshot_stable.py rewrites "docs": "/next/<page>" rule links to Stable for every page the new snapshot has commit 43ed8bf subject line only name it — not a blocker, but nothing in #6285 asks for it and it changes the release process. Please say so in the PR body so the release-prep owner sees it

No removed guards (the only - lines of substance are the willUseWayland() body, replaced by an equivalent, and the three emit finished(false, …) exits, replaced by fail()). No CHANGELOG.md entry — correct. No new protocol verb, settings key or capability field; restartStreamSequence() is new public C++ surface on an internal class, which is fine. No dead additions: QtPlatformChoice.cpp is referenced from both CMakeLists.txt:1690 and tests.cmake:2892-2898, and every new rule has its sample.

3. Blockers

1. src/core/NvidiaAfxPack.cpp:388 — subprocess stderr reaches the log with its newlines intact, so one failure writes several lines and only the first is prefixed.

        qCWarning(lcNvAfx).noquote() << "NvidiaAfxPack: install failed:" << shown;

shown is withoutUrlQueries(msg), and withoutUrlQueries() (:332-341) strips URL queries and fragments only. Two fail() callers pass unzip/tar stderr straight through — "extract failed: %1" with QString::fromLocal8Bit(p.readAllStandardError()).trimmed() (:775, :814); .trimmed() removes leading/trailing whitespace, not interior newlines. Nothing collapses them downstream: AsyncLogWriter.cpp:329-333 is QString("[%1] %2 %3: %4\n") and redactPii() only bounds some of its patterns with [^\r\n].

Concretely, tar does not emit one line — tar: unrecognized option '--zstd' is followed by Try 'tar --help' or 'tar --usage' for more information., so the log gets:

[10:05:37.920] WRN aether.nvafx: NvidiaAfxPack: install failed: extract failed: tar: unrecognized option '--zstd'
Try 'tar --help' or 'tar --usage' for more information.

The second line has no timestamp, level or category, which is exactly the shape the line-based Log Analyzer parses. The sample this PR ships (docs/user/log-samples/rules/bnr-install-failed.log:8) is the one-line best case, so the rule test won't catch it. The PR already establishes the house rule for precisely this reason at src/QtPlatformChoice.cpp:52-56 ("a CR or LF in it would write a second, forged line into the log the Log Analyzer reads") — this is the same seam, unguarded. Credit: also raised by security-audit (CodeGuard), rule codeguard-0-logging.md, which notes the sharper version — installFromFile() (:877) verifies no pinned checksum, so a hostile third-party pack's member names land in tar stderr and can forge a correctly-formatted log line. I'd fix it for the mundane two-line tar case regardless.

Drop-in, mirroring QtPlatformChoice::logLines():

QString NvidiaAfxPack::forOneLogLine(const QString& text)
{
    // Subprocess stderr and network error strings are multi-line: a CR or LF
    // would write a second, unprefixed line into the log the Log Analyzer reads.
    static const QRegularExpression newlines(QStringLiteral("[\\r\\n]+"));
    QString out = withoutUrlQueries(text);
    return out.replace(newlines, QStringLiteral(" "));
}

then use it at :388 and in logTransferFailure() (:75-82), and keep withoutUrlQueries() alone for the emit finished() panel text if you want the panel to stay multi-line. A one-line alternative is to apply the same replace() inside withoutUrlQueries() and rename it; your call which reads better.

4. Nits (non-blocking)

  • src/core/backends/flex/VitaSequenceLossReport.h:106-113 — the held-back notice double counts. When the budget refuses a line, allow() bumps m_suppressed and the error stays in that stream's m_pending, so it is reported twice: once as "N VITA-49 sequence error(s) held back" and again inside that stream's next … N in the last S s. Worth a word in the message ("…held back from an earlier line; they are counted in their own stream's next line") so nobody adds the two numbers.
  • src/core/NvidiaAfxPack.cpp:385 — if (shown == QLatin1String("cancelled")) couples the Info-vs-Warning decision to a string literal written at three other sites (:710, :736, :907). A file-local constexpr auto kCancelled (or checking m_cancelled) keeps a future reword from silently turning a user cancel into a warning.
  • src/QtPlatformChoice.cpp:68-71 — QT_QPA_PLATFORM= (set but empty) logs Platform: Qt platform plugin "xcb" (QT_QPA_PLATFORM=, set by the user). Harmless, slightly odd to read; requested.isEmpty() could fall through to the Unset wording.
  • src/models/RadioModel.cpp:10691 — for a dax_rx id this is the second restartStreamSequence() call, since unregisterDaxStream() (PanadapterStream.cpp:1638) already does it. Idempotent, so purely cosmetic; the comment explains why the broader call is needed.
  • The four new tests are in no ctest -R filter in ci.yml, so they are compiled in the PR lanes (and nvidia_afx_pack_log_test only where ENABLE_NVIDIA_AFX=ON, i.e. ci.yml:269) but executed only by full-suite.yml. That is this repo's normal arrangement, not a defect — flagging it so "CI green" isn't read as "these tests passed".

5. Socket-test disclosure

Required record, inspected in the PR's own sources rather than the base tree: no new or modified test in this PR opens, binds, listens on or connects a socket. grep for QTcpServer|QTcpSocket|QUdpSocket|QLocalServer|bind(|listen(|connectToHost|QNetworkAccessManager|Fake across tests/vita_sequence_loss_log_test.cpp, tests/qt_platform_choice_test.cpp, tests/nvidia_afx_pack_log_test.cpp and tests/remote_audio_rx_sequence_restart_test.cpp returns nothing. The two PanadapterStream tests drive processDatagram() directly on a stream that never binds; nvidia_afx_pack_log_test imports a missing archive and never calls install(). No fake radio/amplifier/tuner peer is introduced. Nothing to act on.

6. What I tried to break (and could not)

  • withoutUrlQueries(). The diff rendering strips backslashes, so I read the literal in the checkout: src/core/NvidiaAfxPack.cpp:336-340 is (\bhttps?://[^\s?#]+)[?#]\S*?([)\]}>"',.;:]*)(?=\s|$) replaced with \1\2 — correct Qt backreference syntax (not a C++ octal escape) and correct escaping. I walked the lazy \S*? + closing-punctuation group + lookahead against Error transferring https://h/p?X-Amz-Signature=… - server replied: 403, (https://h/x?sig=1)., a query containing quotes, an end-of-string URL, and HTTPS://: the minimal match always wins and \S*? can never cross whitespace, so the query cannot survive. No nested quantifiers, so no catastrophic backtracking. tests/nvidia_afx_pack_log_test.cpp:76-93 covers these cases.
  • Silent failure exits. grep "emit finished(false" returns exactly one hit, inside fail(). I also checked fail() keeps staging (:378-380) so the new docs claim "components that finished downloading are kept, so the download resumes" is true, and that routing install()'s no-GPU early return through fail() cannot mis-clear state (m_busy was never set true; m_tmpFile/m_dlFile are empty).
  • Lock ordering and reentrancy on the VITA path. restartStreamSequence() takes only m_statsMutex; the three unregister*() hunks deliberately close the m_streamMutex block before calling it, and unregisterDaxStream() places the call after its locked block (PanadapterStream.cpp:1638). The one nested acquisition, clearRegisteredStreams() → restartAllStreamSequences(), is m_streamMutex → m_statsMutex, which already existed on that exact path via resetAudioStreamStats() (:1422). No site takes m_statsMutex then m_streamMutex. The log calls are made after the lock is released, as the body claims.
  • Counter arithmetic in the hot path. startTotalCount = totalCount - 1 is set after the increment, so "packets received" is this instance's count; stats.errorCount++ precedes lossStreamErrors = errorCount - startErrorCount, so the triggering error is included. m_seqLossClock can only be read after the first-packet branch started it (an error requires lastSeq >= 0, which requires a prior packet, which sets started). Cumulative errorCount/totalCount are untouched, so the status bar and Network Diagnostics totals are unaffected.
  • Reused stream ids, the regression the middle commits target: restartStreamSequence() sets lastSeq = -1 and started = false, so a re-created id's first packet cannot be measured against the old instance's count. The rate limit survives the restart (start() keeps m_lastReportMs), so a stream flapping in a loop cannot out-log the interval — vita_sequence_loss_log_test.cpp:108-118 asserts exactly that.
  • fellBack() false positives, the thing most likely to put a spurious warning in every Wayland user's log: wayland vs wayland-egl (prefix match either way), xcb:arg (firstRequested splits on :), wayland;xcb landing on wayland, an empty request, and source == Unset (suppressed entirely). The documented miss (minimalegl;minimal → minimal) under-reports rather than over-reports.
  • requestPicksWayland() as a refactor. Compared against the deleted willUseWayland() body: same first-entry-of-;-list rule, same wayland/xcb substring tests, same session fallback. Behaviour-preserving, and the -platform precedence it adds matches what main.cpp:294-296 reads before qputenv (so g_qpaPlatformUserSet is genuinely the user's environment, not ours).
  • Ordering in main.cpp. QGuiApplication::platformName() is called at :655, well after QApplication app(argc, argv) at :391, so it is never the empty string; logLines() always returns at least one element, so lines.value(0) is safe.
  • Every source citation in rules.json. I checked all fourteen new and corrected file:line/text pairs against the checkout (QtPlatformChoice.cpp:57/74/77, VitaSequenceLossReport.h:125, NvidiaAfxPack.cpp:76/81/388/716/723/775/814/825/835/856, and the four corrected PanadapterStream/RadioModel/main.cpp line bumps): all match. The Stable anchor #the-download-fails the one non-/next/ rule points at exists at docs/user/versioned_docs/version-stable/bnr-gpu-noise-removal.md:92. CI runs test_log_rules.mjs in both static-checks.yml:248 and docs.yml:85 and both are green at this head.
  • The rewritten privacy note (LogAnalyzer/index.js:134-139), claim by claim against src/core/AsyncLogWriter.cpp and src/core/LogRedactionPolicy.h: IPv4/MAC keep the last part (:220, :236), serial keeps the last group (:247), IPv6 both forms, home directory → ~, bare email, and host/name/location/grid only behind their keywords. "Tokens and keys keep their first four characters" is keepPrefixChars = 4 on the opaque-identifier rows (LogRedactionPolicy.h:28-31) while passwords/names/grids are 0. "Your callsign is kept on purpose" holds — there is no callsign rule. The note is accurate.
  • Cross-firing of the new rules. The bnr-download-failed regex does not match the install-failed or checksum samples and vice versa; none of the five matches clean.log (check 6 of the rule test, green).

Could not check: whether Qt also accepts -platform=xcb. QtPlatformChoice::platformArgument() claims to mirror QGuiApplicationPrivate's loop (one leading - of -- dropped, next argument consumed, last occurrence winning) and the implementation does exactly that, but Qt's sources aren't in this tree, so if Qt supports an = form the parser would miss it — cosmetic either way, it would only mislabel who set the platform.

7. CodeGuard findings

Nearly the whole flagged list is noise for this PR: AudioEngine.cpp, ThemeManager.*, MainWindow*, WdspChannel.*, tools/flex-tailnet-shim/*, .github/workflows/*, docs/user/screens.json, .claude/skills/tag-release/* are not in this diff — they arrived on main from #6289/#6309/#6325/#6298. Of the 509 hits, only src/core/NvidiaAfxPack.cpp is a file this PR touches. The api:input_read: QNetworkReply lead is refutable from the diff: errorString() was already read on those paths; the PR only captures it into a local and routes it through withoutUrlQueries(). The CMDI/SSRF/PII hits on docs JSON, log fixtures and workflow YAML are scanner artefacts on non-executable data.

The one surviving finding is Blocker 1 above. The security audit separately cleared: no secrets in the diff (kWinTensorrtSha and the pack SHAs are integrity pins), all URLs https:// with SHA-256 and a redirect policy set on every request, QProcess used in argument-list form with no shell, no PII in the six log fixtures, and the VITA line carrying only a switch-selected category literal plus integers (so a spoofed stream id can influence only the hex number, and the 8-lines-per-10 s global budget bounds a flood). Its one extra note — that could not install into %1 (:835) now logs an app-data path — is handled: redactPii() rewrites /home/<user>, /Users/<user> and C:\Users\<user> roots to ~ before the line is written.

8. Recommendation

Approve with nits, once Blocker 1 is addressed — it is one helper function and two call sites, and the PR already contains the pattern to copy. Everything else here is unusually careful work: the category-visibility trap was checked rather than assumed, the triage's three bypass exits are all closed and provably so, the sequence limiter has a real spec test, and the eight commits read as genuine responses to review rather than churn. The one scope row worth a sentence in the PR body is snapshot_stable.py/release-prep — it is a release-process change that no part of #6285 asks for, and the release-prep owner should know it is there. Thanks for the thorough docs half; the five samples and the anchor checks are what make the rules maintainable.


🤖 aethersdr-agent · cost: $13.9237 · model: claude-opus-5

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Cisco CodeGuard — static analysis of this PR (518 finding(s))

  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:70
  • [LOW] CS-PII-IP — PII detected: ip_address in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:242
  • [LOW] CS-PII-IP — PII detected: ip_address in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:242
  • [MEDIUM] CS-MAL-SCRIPT — Script detected: env python in .claude/skills/tag-release/scripts/check_release_assets.py /tmp/aetherclaude/pr-6308/.claude/skills/tag-release/scripts/check_release_assets.py:0
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:464
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:14
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:36
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:81
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:172
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:291
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:314
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:322
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:337
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:367
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:382
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:437
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:499
  • [MEDIUM] CS-VLN-CMDI-ENVVAR — Vulnerability: CS-VLN-CMDI-ENVVAR in .github/workflows/static-checks.yml /tmp/aetherclaude/pr-6308/.github/workflows/static-checks.yml:502
  • [CRITICAL] CS-VLN-CMDI-SUBSHELL — Vulnerability: CS-VLN-CMDI-SUBSHELL in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:416
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:48
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:239
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:249
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:321
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:345
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in .github/workflows/windows-installer.yml /tmp/aetherclaude/pr-6308/.github/workflows/windows-installer.yml:619
  • [CRITICAL] CS-VLN-CMDI-BACKTICK — Vulnerability: CS-VLN-CMDI-BACKTICK in docs/architecture/aetherd-touchpoint-tags.json /tmp/aetherclaude/pr-6308/docs/architecture/aetherd-touchpoint-tags.json:1127
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [HIGH] CS-VLN-SSRF-LOCALHOST — Vulnerability: CS-VLN-SSRF-LOCALHOST in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-PII-EMAIL — PII detected: email in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:3877
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:1122
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2579
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2625
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2659
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2756
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:2796
  • [LOW] CS-PII-IP — PII detected: ip_address in docs/user/screens.json /tmp/aetherclaude/pr-6308/docs/user/screens.json:4219
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6308/src/core/AudioEngine.cpp:9698
  • [MEDIUM] CS-VLN-SSRF-HEX — Vulnerability: CS-VLN-SSRF-HEX in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6308/src/core/AudioEngine.cpp:9699
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6308/src/core/AudioEngine.cpp:1377
  • [CRITICAL] CS-VLN-CMDI-AMP — Vulnerability: CS-VLN-CMDI-AMP in src/core/AudioEngine.cpp /tmp/aetherclaude/pr-6308/src/core/AudioEngine.cpp:5431

Automated static scan by Cisco DefenseClaw CodeGuard on the changed files. Advisory — some may be false positives; the review above verifies them.


🤖 aethersdr-agent · cost: $14.1003 · model: claude-opus-5

ten9876 added a commit that referenced this pull request Oct 12, 2026
… and re-take the Stable snapshot. Principle VIII. (#6396)

## Summary

This brings the user guide (docs.aethersdr.com) up to date with
everything in v26.10.2 and re-takes the Stable snapshot so the default
site shows it. Docs only, apart from the TCI page prose in
`tools/docs/gen_reference.py`; no application source, test or CI
changes.

Every page was audited against the 106 PRs in the release. Every fact
that was added or kept was checked against the source at `f753dbfc4`,
the v26.10.2 release commit, not taken from the PR bodies alone.

## What changed

- **Menu paths.**
- AetherModem, Callsign Lookup, AetherMap and FreeDV Reporter are on
**Tools**. They are created on View and then moved to Tools
(`MainWindow_Menus.cpp:1519-1520`, `:1569-1575`); the style guide's
example says **Tools → AetherMap...**.
  - **Show live CW contacts** is on **View**.
  - Waveforms is on **Tools**, not File.
  - The VUDU applet button was called TXDSP.
  - Labels now match the source's `...` vs `…`.
- **Gaps from this cycle.**
- **Windows and Network Diagnostics:** Windows Firewall rules and the
Network Diagnostics Windows Firewall page (#6289); the `aether.network`
and `aether.nvafx` log categories (#6289, #6308).
  - **Remote access:** Tailscale shim revocation semantics (#6325).
  - **Sleep, App Nap and power throttling (#6195):** per-OS notes.
- **Hermes-Lite 2:** state restore, RX audio at unity, CW at TUNE level,
multi-interface discovery and a discovery troubleshooting entry (#6259,
#6254, #5975, #6009, #6293).
- **ANAN-G2:** AGC-T range, a "What is remembered" section, and supply
voltage and link status (#6282, #6129, #6137, #6211).
  - **NB2** on the HL2 and ANAN-G2 (#5966).
- **DVK:** WAV import requirements, the slot menu without Delete, status
text and licensing (#6247, #6262, #6251).
  - **Copy Assist** from unprocessed audio (#6216).
  - **Live CW contacts** replace the decoder's contact card (#6268).
  - **Status-bar indicators** work from the keyboard (#6267).
- **Volume:** controllers, hotkeys and TCI `volume` follow the active
output (#6129, #6260).
- **Peripherals:** the KPA1500 (#5905), CTR2 link extensions and the
AetherKnob (#6338), TMate 2 udev (#6250), MIDI RADE Modem and FDV modes
(#6031, #6038).
  - **RTL-SDR:** live PPM (#6082).
  - **Icom:** COMP and the MK2 squelch scale (#6174, #6184).
- **Known issues.** Entries whose GitHub issues are closed are removed
(each checked with `gh issue view`). As decided, this includes three
closed today without a demonstrated fix: #5107, #4824 and #5665.
- **US spelling.** `docs-style-guide.md` asked for British spelling; it
now asks for US, by the maintainer's decision, and every page is
converted. The generated TCI pages are converted at their source, the
prose strings in `tools/docs/gen_reference.py`. Code spans, links and
URLs were left alone. UI labels were checked against the source strings,
and none of them are British. Two in-page anchors and two Log Analyzer
rule anchors (`#bnr-is-grayed-out-…`) follow the renamed headings.
- **Stable snapshot.** `snapshot_stable.py 26.10.2` was re-run on the
release tree with these corrections, before `main` was merged in, so
Stable carries no post-release docs. `docs/` also has `main`'s
post-release pages, in US spelling. Stable alone keeps the IC-9700
waterfall-rate entry (#5996): #6402 fixed it after the 26.10.2 tag, so
26.10.2 still has it. The PDF manual already attached to the v26.10.2
release keeps the tag's docs.

## Validation

- `npm ci && npm run build` in `docs/user`: success. This is the
broken-link and broken-anchor check.
- `node tools/docs/test_log_rules.mjs`: 707 checks passed for 45 rules.
- `python3 tools/docs/gen_reference.py --check`: up to date (8 pages).
- `git diff --check`: clean.
- Menu paths on a build of this branch (offscreen, isolated settings, no
radio), `menu list`: `Tools → AetherModem...`, `Tools → Callsign
Lookup...`, `Tools → AetherMap...`, `Tools → FreeDV Reporter...`, `Tools
→ Waveforms...`, `View → Show live CW contacts`.

## Screenshots now out of date (not recaptured)

These need a recapture pass on the radio:
- **Old panadapter title strip (#6386):** `panadapter`, `main-window`,
`first-session-*`, `multi-slice-two-slices`, `split-pair`,
`tnf-markers`, `offscreen-vfo-indicator`, `pan-3d-stacked-trace`,
`default-light-theme`.
- **Pre-canon windows (#6334, #6339, #6341, #6372):** `aetherrx*`,
`aethertx*`, `aethermodem-aprs`, `bnr-panel`, `connect-dialog`,
`connect-by-ip-card`, `connect-icom-rows`, `connect-smartlink-card`,
`rx-agc-calibration`.
- **Other:**
  - `title-bar` (lowercase tab states, #6281);
  - `menu-help` (#6294);
  - `display-panel` (#6371);
  - `support-diagnostics-dialog`;
  - `waveforms-dialog` and `tailnet-remote-access` (show shim 0.4.0);
  - `about-window` (v26.10.1);
  - `radio-setup-qrz`.

## Found during the audit, not fixed here

- **App string bug:** `TailnetShimDialog.cpp:628` tells the user to
"Connect remotely with Connect → Manual"; the real path is **File →
Connect to Radio... → Connect by IP**.
- **ANAN AGC-T tooltips:** the RX applet and VFO tooltips show the
slider position as dB, which is wrong on ANAN. #6282 lists this as a
follow-up.
- **MIDI Master Volume on non-Flex radios:** it is refused while PC
Audio is off, whereas hotkeys, FlexControl and HID drive the line out.
The docs describe the code as it is.
- **RTL mode names:** `rtl-sdr.md` writes "FM-N" and "CW-R", but the
source strings are `FMN` and `CWR`. Left as is until someone confirms
what the mode menu displays.
- **IC-705 COMP readback:** inferred from the MK2 (#6174) and not
verified on an IC-705.
- **Not yet documented:** the #6230 Radio Health row "Forward responses
skipped", and the Store/MSIX package.

🤖 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