Files
swaits 975713636b docs(audit): add Mythos security audit and harden esp-println on UART boards
Audit conducted at v1.5.2 (commit 688938d). Findings:
- 0 Critical / 0 High / 0 Medium / 2 Low / 1 Info
- Realistic attack surface = host transport + OTA RX; both traced
  end-to-end with no exploitable defects found.

Hardening applied as part of audit follow-up:
- esp-println now uses default-features=false; output target is
  selected per-board. UART-host boards (heltec_v3_uart,
  elecrow_thinknode_m2, lilygo_tbeam classic) get `no-op` so
  defmt/panic output cannot corrupt frames on UART0 (the host
  protocol wire). Native-USB boards keep `auto` so espflash-monitor
  still works.
- README documents the trust model: no host-transport auth, no
  RF-regulatory enforcement, MAC exposed via USB iSerialNumber +
  GET_INFO.mcu_uid (intentional, needed by donglora-mux for
  multi-dongle disambiguation).
2026-05-13 13:19:59 -06:00

8.4 KiB
Raw Permalink Blame History

Phase 2 — Threat model

Actors

Actor Capabilities Reach
Local USB attacker (LUSB) Read/write raw bytes to CDC-ACM endpoint or UART pins; read USB descriptors. Implies kernel-level access on the host or physical hardware access. high impact, but presumes the host is already compromised
OTA peer (OTA) Transmit arbitrary LoRa packets within RF range, on the configured frequency / sync word / SF / BW. Cannot read or affect host transport directly. low impact (firmware does not interpret), broad reach
Hostile host application (APP) Subset of LUSB: a malicious userspace program on the host PC that opens the CDC-ACM device — possibly without the user's intent. Limited to whatever the OS permits on that device node. depends on host's permissions on the tty/dev node
Build-time adversary (BUILD) Compromised crate in the dependency graph or compromised local toolchain. out of scope of runtime audit; noted only for completeness

Entry points and worst-case impact, in attacker-reachability order

Entry point #1 — Host transport (USB CDC-ACM / UART)

Reachable by: LUSB, APP.

What they can influence: every byte of every frame after 0x00 sentinels. Specifically: type_id (1 B), tag (2 B LE), payload (≤ 275 B), crc (2 B). After CRC + COBS + parser checks, controlled fields are:

  • type_id ∈ {1..=6} (validated by Command::parse)
  • tag ∈ 1..=65535 (tag=0 rejected by framing layer)
  • For SET_CONFIG: every byte of LoRaConfig (15 B) — freq, SF, BW, CR, preamble_len, sync_word, tx_power, header_mode, payload_crc, iq_invert. The other three modulations parse but the firmware rejects with EMODULATION after parsing.
  • For TX: TxFlags byte (bit 0 only — reserved bits rejected) + 1..=255 bytes of OTA payload.

Worst-case if attacker fully controls input:

The firmware is a transparent bridge by design. Any attacker who can write to the host transport can:

  • Tune the radio to any frequency in the chip's range (e.g. 137 MHz – 1020 MHz for SX127x; 150–960 MHz for SX126x).
  • Set TX power up to the board's TX_POWER_RANGE.max (typically +22 dBm).
  • Transmit arbitrary packet bytes with arbitrary sync word at arbitrary spreading factor.
  • Receive on arbitrary parameters.
  • Read the device's MAC address (via GET_INFO and/or USB serial descriptor).

This is the intentional product feature. The firmware does not enforce regional frequency rules, duty cycle, listen-before-talk policy beyond the SF-tuned CAD, or any auth: the host owns the radio. Treating "host can drive the radio" as a vulnerability would be a category error.

The remaining attacker goals against the firmware itself are:

  • DoS — crash, hang, reboot, or wedge the firmware so the user has to power-cycle.
  • Memory corruption / RCE — overflow a buffer, hit an OOB write, leak ASLR-less code-pointer.
  • Cross-device escalation — pivot from the radio task into the host PC (e.g. malicious USB descriptors / report).
  • Information leak — exfiltrate cross-tenant data. But there is no cross-tenant data on this device. Skip.

Entry point #2 — Over-the-air RX

Reachable by: OTA. Anyone with a LoRa radio that can transmit on the configured (freq, sf, bw, sync_word, cr, preamble_len, iq_invert) tuple while the firmware is in RxActive.

What they can influence: the bytes of a single received LoRa packet — 0..=MAX_OTA_PAYLOAD (255) bytes of body. Side-channel metadata that the firmware generates (not the attacker): RSSI (signed i16), SNR (signed i16), timestamp (u64 micros). The header_mode / CRC flags are configured by the host, not the OTA peer.

What this eventually does: collect_rx_packet (radio.rs:1664-1702) calls lora.get_rx_result(pkt, &mut ctx.rx_buf). Length is clamped to MAX_OTA_PAYLOAD before extend_from_slice into a fresh heapless::Vec<u8, 255>. The vec becomes the body of a DeviceMessage::Rx. Wire encoding (events.rs:228-249) is a fixed-layout memcpy, no parsing of OTA bytes inside the firmware.

Worst-case if attacker fully controls the OTA packet:

  • The firmware does not interpret OTA payload bytes; they are an opaque buffer relayed verbatim to the host. Worst is a memory-corruption bug in get_rx_result (lora-phy crate) or in extend_from_slice (heapless), which would be a third-party bug not firmware logic.
  • Spam packets at the configured peer ID to flood OutboundChannel (depth 64). Throttled by RADIO_THROTTLED once host stalls; otherwise the host sees a burst. Standard RF DoS — there is no defense at the device level beyond "host stops listening."
  • Trigger IrqWaitFailed → RxResuming retry loop. After MAX_RESUME_RETRIES = 5 falls back to Idle. Recovers cleanly.

OTA is a broad reach but very narrow impact surface: the firmware exposes essentially no interpretation surface to the OTA peer.

Entry point #3 — USB descriptor handling (control transfers)

Reachable by: LUSB, APP (anyone enumerating the device).

What they can influence: USB control transfer request type / index / length. Handled inside embassy_usb::Builder::run() — out of firmware's code.

Worst-case: a malformed control transfer crashes the USB stack and the firmware. The CTRL_BUF size was bumped to 128 specifically because 64 was crashing on some Linux enumeration paths (see comment in host/usb.rs:191-196). The user reports modem-manager's SET_INTERFACE with out-of-range alt setting is normal and embassy_usb's STALL is correct. Suspected dependency-level risk that we accept as a third-party concern.

Code paths that warrant deep audit (prioritized)

In rough descending order of attacker-reachability × impact:

  1. Frame decode → command dispatch (host/framing.rs, host/usb.rs::dispatch_bytes, host/uart.rs::dispatch_bytes, donglora-protocol/frame.rs, commands.rs, modulation.rs): every reachable byte from LUSB flows through here. Look for panics, buffer overruns in fixed-capacity scratch buffers, ambiguous overflow / CRC-skip paths.

  2. Command::parse for SET_CONFIG: the deepest parse path — modulation-id dispatch, then nested decode. LoRaConfig::decode and friends.

  3. apply_set_config + validate_lora (radio.rs:1233-1309, 2125-2145): post-parse validation. Where do unchecked fields go — sync_word, iq_invert, header_mode, payload_crc? Memory note flags sync_word low-nibble validation as intentionally absent.

  4. Command::Tx ingestion → on-air TX (radio.rs chart TxSession): TX payload (1..=255 attacker bytes) reaches the SX1262 PA without further parsing. The body bytes never act on firmware state except as opaque memcpy. Audit for buffer-length confusion between the chart's pending_tx.data and lora-phy's prepare_for_tx(.., &pt.data).

  5. get_rx_result → record_packet → OutboundFrame encode (radio.rs:1664-1715, 2008-2040; events.rs:228-289): the only path where OTA-attacker bytes are ingested. Audit length handling and RxPayload encoding's bounds.

  6. State machine reachability: chart panics (tag_or_panic, NonZeroU16::new(...).expect(...), radio chart returned) — are any reachable from wire input?

  7. unsafe blocks (4 sites): are they sound under the stated preconditions?

  8. USB serial number / Info disclosure — what does the device disclose without prior auth?

  9. Dependencies: any reachable CVE-bearing dep? Check donglora-protocol, ucobs, lora-phy (fork), hsmc, heapless, embassy-usb, embassy-nrf / embassy-rp / esp-hal.

  10. Build-time (build.rs): code generation reads files from src/board/. Build-time only; out of runtime attacker scope.

Out-of-scope explicitly

  • The host-side application that consumes the protocol (out of repo: ai-bot, bridge, client-py, mux-rs).
  • The Anthropic / Claude scripting context.
  • The donglora-protocol crate's fuzz-targeted invariants — they are validated in the sibling repo (/home/swaits/Code/donglora-org/protocol/fuzz/); we audit only how the firmware uses the crate.
  • Physical-side-channel attacks (power, timing, EM).
  • RF-regulatory compliance (firmware is intentionally transparent — see above).