Skip to content

log: session diagnostics with a redaction guard (closes #438) - #442

Merged
benmcollins merged 3 commits into
mainfrom
438-logging
Aug 22, 2026
Merged

benmcollins merged 3 commits into
mainfrom
438-logging

Conversation

@benmcollins

Copy link
Copy Markdown
Owner

Plan

Four new qumesh.app.* categories plus the redirection transcript that redir_client.cpp already had a category for but never wrote. A three-level ladder keeps it proportionate: warning for anomalies, info for ~6–10 once-per-session facts (both on by default), debug for the phase-by-phase transcript, off unless QT_LOGGING_RULES='qumesh.*=true' is set.

Two things the issue text didn't know

1. Q_LOGGING_CATEGORY's default severity is QtDebugMsg. The two-arg form the existing code uses therefore has qCDebug enabled by default — Qt only hard-wires debug off for categories literally named qt/qt.*. Adding debug lines under those declarations would have shipped a per-transition firehose to every user's stderr with nothing set. Every category moves to the three-arg QtInfoMsg form; that's a required part of this change, not a style preference. (Not using Q_STATIC_LOGGING_CATEGORY — it's Qt 6.9+, and CLAUDE.md pins the floor at 6.6.)

2. There is already a leak, and it's on by default. kvm/src/kvm_codec.cpp hex-dumps 32 bytes of the compressed RLE block at qCWarning on an inflate failure. That block is framebuffer content — the remote screen — printed with no env var, repeating per failed block. Now logs the 2-byte zlib header only. redir/ and wsman/ audited and clean.

The issue is titled "no logging in app/src/", but the fix can't stay there: four of the six named gaps live in redir/src/redir_client.cpp, and the leak is in kvm/.

Redaction-critical sites

site rule
RedirectionClient digest challenge presence only — never realm, nonce, cnonce, or the computed response. Together they turn a pasted transcript into an offline dictionary attack on the AMT password.
MachineDetailsController::sendOptInCode the consent PIN is a live authorisation secret for the length of the round. Logs (code redacted). The issue text didn't flag this one.
setKvmSettings fields.keys() only — fields["rfbPassword"] is the RFB password being written to firmware.
SolController's data handler never the bytes, and not a running count either: this stream carries whatever is typed at a getty prompt. One line on the first chunk proves the session is live.
0x11 oemData / desync m_inbox sizes only, never contents.

What's logged

redir_client.cpp — every setState transition, the connection-open line, 0x11 status (with a warning naming BUSY/UNSUPPORTED, which previously reached only lastError), the 0x14 auth caps and challenge, the 0x40→0x41 KVM round-trip, TLS peer cert and which trust branch was taken, the unexpected-frame desync, and a phase-tagged line at the single fail() choke point.

kvm_session.cpp — setState transitions and a phase-tagged fail().

app/src/ — qumesh.app.kvm, .sol, .ider, .optin: controller state transitions (not redundant with the client's — this is what QML binds to, and AwaitingTrust has no client-side counterpart), open lines carrying the parameters that decide the session, terminal outcomes, IDE-R's SessionInfo, and every OptIn transition guarded on change so the 500 ms poll doesn't become a firehose.

Tests — app/tests/test_log_redaction.cpp (new)

QTest::ignoreMessage can't express this: it matches and consumes one exact message, whereas the assertion is "no line anywhere contains this substring". So a custom qInstallMessageHandler captures everything with all categories forced on — exactly what a reporter following the README would send.

  • redirectionLoggingNeverCarriesThePassword — drives KVM and SOL through open→fail; asserts the password is absent and the username present, so a capture that silently caught nothing can't pass.
  • consentPinIsNeverLogged — verified to fail when the PIN is deliberately interpolated.
  • kvmSettingsLogsKeysNotValues — rfbPassword appears as a key, never as a value.
  • categoriesAreQuietByDefault — behavioural, not by constructing a QLoggingCategory (that would only measure the constructor's default): runs the same failure with and without the env var, and asserts warnings survive the default filter while the transcript doesn't, but does appear once it's set.

Full suite green (42/42).

benmcollins and others added 3 commits August 21, 2026 20:47
`grep -rE 'qCDebug|qCInfo|qCWarning' app/src/` returned zero hits, and
the redirection client had a logging category it never wrote to. #433
arrived as a screenshot of a generic Qt socket string with no way to
tell which protocol phase had failed; this is the gap between that and
the promise README.md already makes to reporters.

Adds four qumesh.app.* categories and fills in the redirection
transcript: every state transition, the 0x11 StartSessionReply status
(BUSY/UNSUPPORTED previously reached only lastError), the 0x14 auth
caps and challenge, the 0x40 -> 0x41 KVM round-trip, TLS trust
outcomes, and a phase-tagged line at the single fail() choke point.
Warnings and once-per-session facts stay on; the phase-by-phase
transcript sits behind QT_LOGGING_RULES='qumesh.*=true'.

Two things the issue text didn't know:

Q_LOGGING_CATEGORY's default severity is QtDebugMsg, so the two-arg
form the existing code uses has qCDebug ON by default — Qt only
hard-wires debug off for categories named "qt"/"qt.*". Adding debug
lines under those declarations would have shipped a per-transition
firehose to every user's stderr with nothing set. All categories move
to the three-arg QtInfoMsg form. That's required, not cosmetic.

And there was already a leak, on by default: kvm_codec.cpp hex-dumped
32 bytes of the compressed RLE block at qCWarning on an inflate
failure. That block is framebuffer content — the remote screen — with
no env var needed, repeating per failed block. Now logs the 2-byte
zlib header only. redir/ and wsman/ audited and clean.

Redaction is the load-bearing part, since users paste these into
public issues: presence-only for the digest realm/nonce (with the
cnonce and response never logged at all, since together they'd allow
an offline attack on the AMT password), "(code redacted)" for the
consent PIN, keys-not-values for setKvmSettings (rfbPassword lives
there), and nothing whatsoever from the SOL byte stream, which
carries whatever is typed at a getty prompt.

test_log_redaction.cpp captures everything with all categories forced
on and asserts known secrets never appear — QTest::ignoreMessage can't
express "no line contains this", so it uses a message handler. Each
positive assertion is paired with a negative one so a capture that
caught nothing can't pass, and the PIN test is verified to fail when
the redaction is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The test failed on Windows only, and took 16s there against 4s
locally: its assertions waited for a connection to a closed local port
to fail, and Windows doesn't refuse as promptly as Linux/macOS, so the
waits ran to their full timeouts and the expected lines were never
observed.

Nothing it checks actually needs the connection to fail. The open-time
lines are emitted synchronously inside open(), before any socket
activity, and the severity test now uses the empty-host path, which
emits both a warning and a state transition without opening a socket
at all. Runs in 0.87s and says nothing about platform connect
behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The consent status line carried the flattened bool because
`m_optInRequiredRaw` didn't exist when this branch was cut. It does
now, and "which protocols does this machine gate" is the question a
consent report actually needs answering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@benmcollins
benmcollins merged commit 5d4ff8d into main Aug 22, 2026
10 checks passed
@benmcollins
benmcollins deleted the 438-logging branch August 22, 2026 01:04
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