Skip to content

fix(lockdown): re-lock serial console admin auth on USB link drop#11028

Open
thebentern wants to merge 1 commit into
developfrom
fix/serial-console-stale-admin-auth
Open

fix(lockdown): re-lock serial console admin auth on USB link drop#11028
thebentern wants to merge 1 commit into
developfrom
fix/serial-console-stale-admin-auth

Conversation

@thebentern

@thebentern thebentern commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

⚠️ Security fix — touches the lockdown auth model. Please get a security review before merging.

The bug

On lockdown builds (MESHTASTIC_PHONEAPI_ACCESS_CONTROL + MESHTASTIC_ENCRYPTED_STORAGE, nRF52 — see configuration.h), per-connection admin authorization persists across serial-console client swaps.

SerialConsole is a process-lifetime singleton (src/SerialConsole.cpp, global console) and inherits PhoneAPI via StreamAPI, so the same PhoneAPI object services every USB/serial client for the whole boot. Admin auth lives in the file-static g_authSlots table keyed by the PhoneAPI* this. Because the console's this never changes, an operator's unlock stays latched in that slot. The only re-lock paths for the serial console were close() (15-min inactivity timeout / explicit disconnect) and the !isConnected() branch of handleStartConfig().

Impact: an attacker who plugs into the USB/serial port before the prior authenticated session times out inherits admin authorization — the same stale-state-on-reuse class the BLE onConnect()/resetBleSessionState fix just closed, with no serial equivalent until now.

The fix

Give the serial console a per-physical-link re-lock. Each runOnce() samples the USB-CDC host link state; on the link-drop edge it calls close(), which frees the auth slot (clearAuthSlot_LH) and resets PhoneAPI state to STATE_SEND_NOTHING. Whoever connects next then re-locks through the existing !isConnected() branch of handleStartConfig() on their first want_config — the same physical-link boundary BLE enforces in onConnect().

const bool linkUp = static_cast<bool>(Port);
if (s_serialLinkUp && !linkUp)
    close();
s_serialLinkUp = linkUp;

On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected(): true while a host holds the CDC port open with DTR asserted, false on cable unplug or host port-close.

Design notes

  • Entirely #ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL-gated (nRF52-only). Non-lockdown builds compile byte-identically; when access control is off, getAdminAuthorized() returns true unconditionally so the gates are already no-ops.
  • Link-drop edge + close() only. close() already clears the slot and resets state, so a rising-edge setAdminAuthorized(false) would be redundant. (setAdminAuthorized is also declared under the access-control #ifdef, and in this TU PhoneAPI.h is parsed before configuration.h defines the macro — close() is unconditionally declared and is the right primitive here.)
  • File-scope static, not a member — per the note at PhoneAPI.h that per-instance members on the PhoneAPI hierarchy have perturbed nRF52 USB-CDC enumeration. Safe because console is a singleton.
  • Structural asymmetry vs BLE (intentional, analyzed): serial has no onConnect event, so it relies on the disconnect edge + the new client's handleStartConfig re-lock rather than an unconditional connect-time clear. Reaching an un-cleared authorized slot requires the passphrase (every setAdminAuthorized(true) path is passphrase-gated) and the next client re-locks before any config is emitted, so the gap isn't exploitable.
  • Residuals (out of threat model, no regression): a client that never asserts DTR falls back to the existing 15-min inactivity timeout (same as before); a machine-speed cable swap that beats the ~250 ms runOnce poll on a battery-powered device is beyond "reconnect before timeout."
  • Completeness: lockdown is nRF52-only, and nRF52 has exactly two PhoneAPI transports — BLE (already fixed) and the serial console (this PR).

Testing

  • Builds clean on a lockdown env (rak4631_ctf_dev, real nRF52 toolchain): SerialConsole.cpp compiles + links, firmware .hex/OTA produced.
  • clang-format 16.0.3 (repo config) — no changes.
  • Non-lockdown / native builds unaffected by construction (all new code inside the nRF52-only #ifdef).
  • Hardware plug/unplug re-lock verification recommended before merge.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved USB/serial connection handling by resetting the administrative session when a physical connection is disconnected.
    • Ensured new USB/serial connections begin with a fresh authentication state.

On lockdown builds (MESHTASTIC_PHONEAPI_ACCESS_CONTROL, nRF52) the SerialConsole
is a process-lifetime singleton, so the per-connection admin-auth slot keyed by
its inherited PhoneAPI* is reused for every USB/serial client for the whole boot.
An operator's admin unlock stayed latched across serial client swaps: an attacker
plugging into the USB/serial port before the prior session's 15-minute inactivity
timeout inherited admin authorization -- the serial analog of the BLE stale-session
reuse bug closed by resetting state in onConnect()/onDisconnect().

Sample the USB-CDC host link (DTR/mount) state each runOnce(); on the link-drop
edge call close(), which frees the auth slot and resets PhoneAPI state so whoever
connects next re-locks via handleStartConfig()'s !isConnected() branch on their
first want_config -- the same physical-link boundary BLE enforces in onConnect().
On the nRF52 TinyUSB (Adafruit) core, (bool)Port == tud_cdc_n_connected(), which
goes false on cable unplug or host port-close. Console transports without a real
DTR line fall back to the existing inactivity timeout, no worse than before.

Entirely nRF52-lockdown-gated; non-lockdown builds are byte-identical.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

SerialConsole now tracks USB/serial connectivity on access-controlled builds and closes the session when a connected link becomes unavailable, allowing the next physical connection to reinitialize authentication.

Changes

Serial link session reset

Layer / File(s) Summary
Connection-bound session reset
src/SerialConsole.cpp
Stores the previous USB/serial link state and calls close() when Port transitions from connected to disconnected under MESHTASTIC_PHONEAPI_ACCESS_CONTROL.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: jp-bennett, p0ns

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits the template's Attestations checklist and explicit test/regression checkboxes. Add the Attestations section from the template and check which devices/builds were tested, or note that hardware regression testing is still pending.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: re-locking serial console auth when the USB link drops.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/serial-console-stale-admin-auth

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/SerialConsole.cpp`:
- Around line 34-40: The comments in src/SerialConsole.cpp lines 34-40 and
102-118 are overly verbose; reduce each explanatory block to no more than one or
two lines. Keep the file-scope comment focused on s_serialLinkUp and its
purpose, and keep the runOnce() comment focused on calling close() to re-lock
admin authentication after the USB-CDC link drops.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d12873eb-f049-494f-93fb-b19e17610596

📥 Commits

Reviewing files that changed from the base of the PR and between 53a6b5e and a99c3c0.

📒 Files selected for processing (1)
  • src/SerialConsole.cpp

Comment thread src/SerialConsole.cpp
Comment on lines +34 to +40
// Last-seen USB-CDC host link (DTR/mount) state, sampled each runOnce() so a
// physical unplug/replug re-locks the per-connection admin auth (see runOnce()).
// Kept at file scope rather than as a member both because there is exactly one
// console singleton and because adding per-instance members to the PhoneAPI
// hierarchy has historically perturbed nRF52 USB-CDC enumeration (see PhoneAPI.h).
// Only compiled on lockdown (nRF52) builds.
static bool s_serialLinkUp = false;

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep code comments minimal.

Both of these locations add multi-paragraph explanatory blocks, which violates the coding guidelines for C++ files requiring comments to be one or two lines maximum. As per coding guidelines, do not restate straightforward code or add multi-paragraph explanatory blocks.

  • src/SerialConsole.cpp#L34-L40: reduce this file-scope rationale to one or two lines (e.g., // Last-seen USB-CDC host link state; sampled each runOnce() to re-lock admin auth on unplug. Kept at file scope to avoid perturbing nRF52 USB-CDC enumeration with per-instance members.).
  • src/SerialConsole.cpp#L102-L118: reduce this detailed behavior explanation inside runOnce() to one or two lines (e.g., // Re-lock admin auth when the physical USB-CDC link drops by calling close(). This frees the auth slot and resets the session for the next client.).
📍 Affects 1 file
  • src/SerialConsole.cpp#L34-L40 (this comment)
  • src/SerialConsole.cpp#L102-L118
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SerialConsole.cpp` around lines 34 - 40, The comments in
src/SerialConsole.cpp lines 34-40 and 102-118 are overly verbose; reduce each
explanatory block to no more than one or two lines. Keep the file-scope comment
focused on s_serialLinkUp and its purpose, and keep the runOnce() comment
focused on calling close() to re-lock admin authentication after the USB-CDC
link drops.

Source: Coding guidelines

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (29)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-15. Updated for a99c3c0.

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