Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da10540d1
|
||
|
|
cf2ec92c4a
|
||
|
|
975713636b
|
||
|
|
688938d8e8
|
||
|
|
e9360c28ba
|
||
|
|
d7a1ee86d0
|
||
|
|
0b9f9ee882
|
||
|
|
04dc58c943
|
No files matched your search
+76
-1
@@ -7,6 +7,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [1.5.2] - 2026-05-11
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Refreshed `Cargo.lock` to pull in the latest compatible patch/minor
|
||||||
|
releases of dependencies. Notable bumps:
|
||||||
|
- `donglora-protocol` 1.1.0 → 1.2.0
|
||||||
|
- `esp-hal` 1.1.0 → 1.1.1
|
||||||
|
- `heapless` 0.9.2 → 0.9.3 (propagates through embassy + esp crates)
|
||||||
|
- `embassy-usb-driver` 0.2.0 → 0.2.1
|
||||||
|
- `embassy-usb-synopsys-otg` 0.3.2 → 0.3.3
|
||||||
|
- `enumset` 1.1.10 → 1.1.12
|
||||||
|
- `digest` 0.11.2 → 0.11.3, `hashbrown` 0.17.0 → 0.17.1,
|
||||||
|
`hybrid-array` 0.4.11 → 0.4.12, `cc` 1.2.61 → 1.2.62
|
||||||
|
No source changes; all 10 board targets compile clean and host-side
|
||||||
|
protocol tests pass.
|
||||||
|
|
||||||
|
## [1.5.1] - 2026-05-11
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **`LoRaConfig.sync_word` is now actually applied to the SX126x.**
|
||||||
|
`reconfigure_radio` accepted the host's `sync_word` field in name
|
||||||
|
only — the value was stored in `self.config` but never written to
|
||||||
|
the chip's `LoRaSyncwordMSB`/`LoRaSyncwordLSB` register pair
|
||||||
|
(0x0740/0x0741, SX1261/2 datasheet Rev 2.2 §13.4.4). The chip sat
|
||||||
|
on whatever `LoRa::new(_, false, _)` set at cold init —
|
||||||
|
`LORAWAN_PRIVATE_SYNCWORD` (byte `0x12`, register pair `0x1424`).
|
||||||
|
Everyone got byte `0x12` regardless of the requested value. Worked
|
||||||
|
by accident for MeshCore (which uses byte `0x12`); silently broke
|
||||||
|
any host trying to talk to a non-MeshCore network. Fix wires
|
||||||
|
`cfg.sync_word` through to `lora.set_lora_sync_word(byte)` after
|
||||||
|
`enter_standby`, using the new lora-phy API from
|
||||||
|
[lora-rs/lora-rs#433](https://github.com/lora-rs/lora-rs/pull/433).
|
||||||
|
The `u16` protocol field is decoded back to its underlying byte
|
||||||
|
(`byte = (hi & 0xF0) | (lo >> 4)` after `to_be_bytes()`), the
|
||||||
|
inverse of lora-phy's `convert_sync_word` nibble-expansion. Honors
|
||||||
|
SX1261/2 §8.3.1 (BUSY-low precondition) via the existing
|
||||||
|
`ensure_ready + conditional set_standby` preamble in
|
||||||
|
`LoRa::set_lora_sync_word`.
|
||||||
|
- **First TX after `SET_CONFIG` no longer fails with `ERR(ERadio)` on
|
||||||
|
a freshly-booted chip.** The chart's `prep_for_cad_act`
|
||||||
|
(`TxCadPrep` entry) and `prep_for_tx_act` (`TxPrep` entry) both
|
||||||
|
read `ctx.mdltn` and emit `TransmitFailed` → `ERR(ERadio)` if it's
|
||||||
|
`None`. But `ctx.mdltn` was only ever assigned on the RX path —
|
||||||
|
`try_start_rx_hw` (`RxArming` entry) and `cache_rx_pkt`
|
||||||
|
(`RxSession` entry). `reconfigure_radio` already called
|
||||||
|
`to_lora_mod_params(lora, cfg)` during `SET_CONFIG` but discarded
|
||||||
|
the result via `let _ = …?;` (the dropped comment claimed image
|
||||||
|
calibration; the function is pure compute on SX126x). Hidden in
|
||||||
|
practice by any prior `RX_START` (ai-bot, `ping_pong --role rx`,
|
||||||
|
etc.) leaving `ctx.mdltn` populated until the next reflash. Fix
|
||||||
|
threads the `ModulationParams` out of `reconfigure_radio` as its
|
||||||
|
return value, and `apply_set_config` caches it in `self.mdltn`
|
||||||
|
alongside `self.config`. First TX on a fresh boot now works
|
||||||
|
without a preceding `RX_START`.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`lora-phy` `swaits/lora-rs#fixed` bumped from `e8a52ebd` →
|
||||||
|
`043d7659`.** The fork's `fixed` merge now includes a new
|
||||||
|
`feat/set-sync-word` branch alongside the three pre-existing
|
||||||
|
`fix/*` branches.
|
||||||
|
[lora-rs/lora-rs#433](https://github.com/lora-rs/lora-rs/pull/433)
|
||||||
|
adds the runtime `LoRa::set_lora_sync_word(u8)` API the radio fix
|
||||||
|
above depends on (was unavailable in stock lora-phy; the only
|
||||||
|
public sync-word entry points were `with_syncword` /
|
||||||
|
`new(_, public_network: bool, _)` at construction time). Strictly
|
||||||
|
additive in lora-phy: pure addition, no behavioral change to
|
||||||
|
`init_lora` / `with_syncword` / any existing path.
|
||||||
|
- **`hsmc` 0.5 → 0.6.** Patch bump tracked via `cargo update -p
|
||||||
|
hsmc`; no behavior change in this crate's usage. Pulled in
|
||||||
|
separately from the radio fixes so the dep churn doesn't blur the
|
||||||
|
fix commits.
|
||||||
|
|
||||||
## [1.5.0] - 2026-04-30
|
## [1.5.0] - 2026-04-30
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -477,7 +552,7 @@ supported board.
|
|||||||
### Infrastructure
|
### Infrastructure
|
||||||
|
|
||||||
- Cargo.toml `repository` + `homepage` now point at the canonical
|
- Cargo.toml `repository` + `homepage` now point at the canonical
|
||||||
`https://github.com/donglora/firmware` (the `swaits/donglora`
|
`https://git.swaits.com/donglora/firmware` (the `swaits/donglora`
|
||||||
repo is archived).
|
repo is archived).
|
||||||
|
|
||||||
## [0.5.0] - 2026-04-16
|
## [0.5.0] - 2026-04-16
|
||||||
|
|||||||
Generated
+46
-44
@@ -167,9 +167,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.61"
|
version = "1.2.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"shlex",
|
"shlex",
|
||||||
@@ -210,7 +210,7 @@ checksum = "ac78aa94ce13e432b332a4d1bf2eff167d3a2520188ee05b337180a42fd2e62e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cipher",
|
"cipher",
|
||||||
"dbl",
|
"dbl",
|
||||||
"digest 0.11.2",
|
"digest 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -537,9 +537,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.11.2"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
|
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-buffer 0.12.0",
|
"block-buffer 0.12.0",
|
||||||
"crypto-common 0.2.1",
|
"crypto-common 0.2.1",
|
||||||
@@ -586,7 +586,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora"
|
name = "donglora"
|
||||||
version = "1.5.0"
|
version = "1.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"cortex-m",
|
"cortex-m",
|
||||||
@@ -615,7 +615,7 @@ dependencies = [
|
|||||||
"esp-hal",
|
"esp-hal",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"esp-rtos",
|
"esp-rtos",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"hsmc",
|
"hsmc",
|
||||||
"lcd-async",
|
"lcd-async",
|
||||||
"lora-phy",
|
"lora-phy",
|
||||||
@@ -630,12 +630,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donglora-protocol"
|
name = "donglora-protocol"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6bae9e9e5a8cebc6b4167f9ef696b61ce25b94c6d8283dbe1b2e97075a481b9d"
|
checksum = "0eea2a87a5ad31629140accc2bae6c931434bf1946706f2184dec9b3d356505d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"ucobs",
|
"ucobs",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -853,7 +853,7 @@ dependencies = [
|
|||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-sink",
|
"futures-sink",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -888,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693"
|
checksum = "168297bf80aaf114b3c9ad589bf38b01b3009b9af7f97cd18086c5bbf96f5693"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"embassy-executor-timer-queue",
|
"embassy-executor-timer-queue",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -905,30 +905,32 @@ dependencies = [
|
|||||||
"embassy-time",
|
"embassy-time",
|
||||||
"embassy-usb-driver",
|
"embassy-usb-driver",
|
||||||
"embedded-io-async 0.7.0",
|
"embedded-io-async 0.7.0",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"ssmarshal",
|
"ssmarshal",
|
||||||
"usbd-hid",
|
"usbd-hid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb-driver"
|
name = "embassy-usb-driver"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17119855ccc2d1f7470a39756b12068454ae27a3eabb037d940b5c03d9c77b7a"
|
checksum = "6a189cbf1d8ab3c591f88632f85c1df892b6ab04116037cbee39dc37cbdcd79f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"embedded-io-async 0.6.1",
|
"embedded-io-async 0.7.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "embassy-usb-synopsys-otg"
|
name = "embassy-usb-synopsys-otg"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cbe46f4083109c7ea12a03ca61095d1e87c76fec52c7ca9ee06a42935606dacb"
|
checksum = "fb205e26d59e483e8c48f91f637ec217ec7e2cfb0b61d50482301b991b4ff431"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"critical-section",
|
"critical-section",
|
||||||
"embassy-sync 0.8.0",
|
"embassy-sync 0.8.0",
|
||||||
|
"embassy-time",
|
||||||
"embassy-usb-driver",
|
"embassy-usb-driver",
|
||||||
|
"portable-atomic",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1071,18 +1073,18 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumset"
|
name = "enumset"
|
||||||
version = "1.1.10"
|
version = "1.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25b07a8dfbbbfc0064c0a6bdf9edcf966de6b1c33ce344bdeca3b41615452634"
|
checksum = "7f96a4a12fe60ac746ae295a1a4ecb5bb02debc20856506c8635288065f142de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"enumset_derive",
|
"enumset_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enumset_derive"
|
name = "enumset_derive"
|
||||||
version = "0.14.0"
|
version = "0.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f43e744e4ea338060faee68ed933e46e722fb7f3617e722a5772d7e856d8b3ce"
|
checksum = "4bd536557b58c682b217b8fb199afdff47cd3eff260623f19e77074eb073d63a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.21.3",
|
"darling 0.21.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -1108,7 +1110,7 @@ dependencies = [
|
|||||||
"esp-config",
|
"esp-config",
|
||||||
"esp-metadata-generated",
|
"esp-metadata-generated",
|
||||||
"esp-println",
|
"esp-println",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"riscv",
|
"riscv",
|
||||||
"semihosting",
|
"semihosting",
|
||||||
"xtensa-lx",
|
"xtensa-lx",
|
||||||
@@ -1147,9 +1149,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "esp-hal"
|
name = "esp-hal"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6af8fa8216bc126941bd43b5a200a50eab16e43881ccd0dd0b6792f4a82805f0"
|
checksum = "2bfcf2a0842903717f4663f6a08512c32b0f6b2d7fb7db3c8a6895d2e6d49f72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitfield 0.19.4",
|
"bitfield 0.19.4",
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
@@ -1553,9 +1555,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.0"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
@@ -1569,9 +1571,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heapless"
|
name = "heapless"
|
||||||
version = "0.9.2"
|
version = "0.9.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af2455f757db2b292a9b1768c4b70186d443bcb3b316252d6b540aec1cd89ed"
|
checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 1.0.1",
|
"defmt 1.0.1",
|
||||||
"hash32",
|
"hash32",
|
||||||
@@ -1592,9 +1594,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc"
|
name = "hsmc"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "497ed9593e4baf6ef120b24aff80cb898f6832d32cdf01be9d90be0c06624e77"
|
checksum = "18bb06fda289e60e17bc7483a222fc6d9d6865b92648f836242b7a79115cc91d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"embassy-futures",
|
"embassy-futures",
|
||||||
@@ -1606,9 +1608,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hsmc-macros"
|
name = "hsmc-macros"
|
||||||
version = "0.5.1"
|
version = "0.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49d0b1413ed69ba168f9d0edbf4a8ea21569eaf352edf4e11335ae3d765ba004"
|
checksum = "289a7c9f1b677168229c8ad4e992380c3951d0908b52b1d2bfae8ff3c980c35d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1617,9 +1619,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.11"
|
version = "0.4.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5"
|
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
@@ -1637,7 +1639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.17.0",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1812,7 +1814,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lora-modulation"
|
name = "lora-modulation"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
]
|
]
|
||||||
@@ -1820,7 +1822,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lora-phy"
|
name = "lora-phy"
|
||||||
version = "3.0.2-alpha"
|
version = "3.0.2-alpha"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
@@ -1834,7 +1836,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan"
|
name = "lorawan"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"cmac",
|
"cmac",
|
||||||
@@ -1846,13 +1848,13 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan-device"
|
name = "lorawan-device"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"defmt 0.3.100",
|
"defmt 0.3.100",
|
||||||
"document-features",
|
"document-features",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"futures",
|
"futures",
|
||||||
"heapless 0.9.2",
|
"heapless 0.9.3",
|
||||||
"lora-modulation",
|
"lora-modulation",
|
||||||
"lorawan",
|
"lorawan",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
@@ -1861,7 +1863,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lorawan-macros"
|
name = "lorawan-macros"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#e8a52ebd35352ce0267148fd392a0b1956d7735f"
|
source = "git+https://github.com/swaits/lora-rs.git?branch=fixed#043d7659dfe2148913f16b2f7ea3e9241642b770"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2551,9 +2553,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "siphasher"
|
name = "siphasher"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
|
|||||||
+33
-5
@@ -3,14 +3,14 @@ members = ["."]
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "donglora"
|
name = "donglora"
|
||||||
version = "1.5.0"
|
version = "1.5.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Transparent LoRa radio over USB — plug in a board, talk LoRa from your host"
|
description = "Transparent LoRa radio over USB — plug in a board, talk LoRa from your host"
|
||||||
repository = "https://github.com/donglora/firmware"
|
repository = "https://git.swaits.com/donglora/firmware"
|
||||||
homepage = "https://github.com/donglora/firmware"
|
homepage = "https://git.swaits.com/donglora/firmware"
|
||||||
keywords = ["lora", "radio", "embedded", "usb", "firmware"]
|
keywords = ["lora", "radio", "embedded", "usb", "firmware"]
|
||||||
categories = ["embedded", "hardware-support", "no-std"]
|
categories = ["embedded", "hardware-support", "no-std"]
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ embassy-usb-driver = "0.2"
|
|||||||
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
ucobs = { version = "0.3", features = ["legacy-msrv"] }
|
||||||
heapless = "0.9"
|
heapless = "0.9"
|
||||||
donglora-protocol = "1"
|
donglora-protocol = "1"
|
||||||
hsmc = { version = "0.5", default-features = false, features = ["embassy", "trace-defmt"] }
|
hsmc = { version = "0.6", default-features = false, features = ["embassy", "trace-defmt"] }
|
||||||
# Fork branch carrying CAD IRQ-clear / standby-after-CAD fixes awaiting upstream PRs.
|
# Fork branch carrying CAD IRQ-clear / standby-after-CAD fixes awaiting upstream PRs.
|
||||||
# Switch back to crates.io once changes are upstreamed.
|
# Switch back to crates.io once changes are upstreamed.
|
||||||
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fixed", features = ["defmt-03"] }
|
lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fixed", features = ["defmt-03"] }
|
||||||
@@ -70,7 +70,16 @@ esp-backtrace = { version = "0.19", optional = true, features = [
|
|||||||
esp-hal = { version = "1.1", optional = true, features = [
|
esp-hal = { version = "1.1", optional = true, features = [
|
||||||
"unstable",
|
"unstable",
|
||||||
] }
|
] }
|
||||||
esp-println = { version = "0.17", optional = true, features = [
|
# `default-features = false` strips `auto` + `colors`. The output target
|
||||||
|
# (jtag-serial / uart / no-op / auto) is selected explicitly per board
|
||||||
|
# feature below. UART-host boards (heltec_v3_uart, elecrow_thinknode_m2,
|
||||||
|
# lilygo_tbeam) MUST use `no-op` — esp-println's `auto` falls back to
|
||||||
|
# UART0 on every ESP target here (no USB-Serial-JTAG host is ever
|
||||||
|
# attached to those boards), and UART0 IS the host protocol wire, so
|
||||||
|
# any defmt::error!() or panic backtrace would corrupt frames mid-flight.
|
||||||
|
# Native-USB boards keep `auto` so espflash-monitor still works in dev.
|
||||||
|
esp-println = { version = "0.17", optional = true, default-features = false, features = [
|
||||||
|
"critical-section",
|
||||||
"defmt-espflash",
|
"defmt-espflash",
|
||||||
] }
|
] }
|
||||||
esp-rtos = { version = "0.3", optional = true, features = [
|
esp-rtos = { version = "0.3", optional = true, features = [
|
||||||
@@ -171,6 +180,11 @@ heltec_v3 = [
|
|||||||
"esp-bootloader-esp-idf/esp32s3",
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
|
# Native USB CDC-ACM via OTG_FS — UART0 (GPIO43/44) is dangling, so
|
||||||
|
# `auto`'s fallback target writes into the void: invisible but legal.
|
||||||
|
# Keeps espflash-monitor working in dev when USB-Serial-JTAG ROM mode
|
||||||
|
# is active before the firmware switches to OTG_FS.
|
||||||
|
"esp-println/auto",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
]
|
]
|
||||||
heltec_v3_uart = [
|
heltec_v3_uart = [
|
||||||
@@ -185,6 +199,11 @@ heltec_v3_uart = [
|
|||||||
"esp-bootloader-esp-idf/esp32s3",
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
|
# UART0 IS the host protocol wire on this board. `no-op` discards all
|
||||||
|
# esp-println / defmt output so error-level logs or panic backtraces
|
||||||
|
# cannot corrupt frames mid-flight. Use `debug-checkpoint` for
|
||||||
|
# development diagnostics (LED-blink pattern instead of log bytes).
|
||||||
|
"esp-println/no-op",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
]
|
]
|
||||||
heltec_v4 = [
|
heltec_v4 = [
|
||||||
@@ -199,6 +218,8 @@ heltec_v4 = [
|
|||||||
"esp-bootloader-esp-idf/esp32s3",
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
|
# Native USB CDC-ACM — same rationale as heltec_v3.
|
||||||
|
"esp-println/auto",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
]
|
]
|
||||||
elecrow_thinknode_m2 = [
|
elecrow_thinknode_m2 = [
|
||||||
@@ -213,6 +234,8 @@ elecrow_thinknode_m2 = [
|
|||||||
"esp-bootloader-esp-idf/esp32s3",
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
|
# UART0 IS the host protocol wire — see heltec_v3_uart rationale.
|
||||||
|
"esp-println/no-op",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
]
|
]
|
||||||
lilygo_tbeam_supreme = [
|
lilygo_tbeam_supreme = [
|
||||||
@@ -227,6 +250,8 @@ lilygo_tbeam_supreme = [
|
|||||||
"esp-bootloader-esp-idf/esp32s3",
|
"esp-bootloader-esp-idf/esp32s3",
|
||||||
"esp-hal/esp32s3",
|
"esp-hal/esp32s3",
|
||||||
"esp-println/esp32s3",
|
"esp-println/esp32s3",
|
||||||
|
# Native USB CDC-ACM — same rationale as heltec_v3.
|
||||||
|
"esp-println/auto",
|
||||||
"esp-rtos/esp32s3",
|
"esp-rtos/esp32s3",
|
||||||
]
|
]
|
||||||
lilygo_tbeam = [
|
lilygo_tbeam = [
|
||||||
@@ -241,6 +266,9 @@ lilygo_tbeam = [
|
|||||||
"esp-bootloader-esp-idf/esp32",
|
"esp-bootloader-esp-idf/esp32",
|
||||||
"esp-hal/esp32",
|
"esp-hal/esp32",
|
||||||
"esp-println/esp32",
|
"esp-println/esp32",
|
||||||
|
# ESP32 classic has no USB-Serial-JTAG peripheral — `auto` would
|
||||||
|
# unconditionally route to UART0 = host protocol wire. Use `no-op`.
|
||||||
|
"esp-println/no-op",
|
||||||
"esp-rtos/esp32",
|
"esp-rtos/esp32",
|
||||||
]
|
]
|
||||||
rak_wisblock_4631 = [
|
rak_wisblock_4631 = [
|
||||||
|
|||||||
@@ -63,8 +63,35 @@ See [src/board/PORTING.md](src/board/PORTING.md).
|
|||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
See [PROTOCOL.md](https://github.com/donglora/protocol-rs/blob/main/PROTOCOL.md)
|
See [PROTOCOL.md](https://git.swaits.com/donglora/protocol-rs/src/branch/main/PROTOCOL.md)
|
||||||
in the [donglora-protocol](https://github.com/donglora/protocol-rs) repo
|
in the [donglora-protocol](https://git.swaits.com/donglora/protocol-rs) repo
|
||||||
(crates.io: [`donglora-protocol`](https://crates.io/crates/donglora-protocol))
|
(crates.io: [`donglora-protocol`](https://crates.io/crates/donglora-protocol))
|
||||||
for the complete wire format specification — that is the canonical home;
|
for the complete wire format specification — that is the canonical home;
|
||||||
this firmware just consumes it.
|
this firmware just consumes it.
|
||||||
|
|
||||||
|
## Trust model and identifiers
|
||||||
|
|
||||||
|
DongLoRa is a **transparent LoRa bridge**, not a security boundary:
|
||||||
|
|
||||||
|
- **No authentication on the host transport.** Anything that can write
|
||||||
|
to the CDC-ACM endpoint or UART pins drives the radio. Physical USB
|
||||||
|
attachment is the trust boundary.
|
||||||
|
- **No RF-regulatory enforcement.** Frequency, sync word, and TX power
|
||||||
|
are whatever the host sends, within the chip's hardware range.
|
||||||
|
Regional compliance (FCC / CE / ETSI / duty cycle) is the user's
|
||||||
|
responsibility, not the firmware's.
|
||||||
|
- **Device identifier exposed without authentication.** The board's
|
||||||
|
6-byte MAC (factory-burned, permanent) is published in two places
|
||||||
|
readable by anyone who can enumerate or open the device:
|
||||||
|
1. The USB CDC-ACM `iSerialNumber` string descriptor (12 hex
|
||||||
|
chars, uppercase). Visible to any OS / userspace process that
|
||||||
|
can read USB device descriptors.
|
||||||
|
2. The `GET_INFO.mcu_uid` field on the wire, available to any
|
||||||
|
host that can issue protocol frames.
|
||||||
|
|
||||||
|
This is intentional — host-side tools (`donglora-mux`,
|
||||||
|
`client-py`, etc.) rely on it to distinguish multiple dongles on
|
||||||
|
one host. The privacy trade-off: a user reusing the same dongle
|
||||||
|
across different hosts/networks is correlatable by that MAC across
|
||||||
|
contexts. If that matters in your threat model, isolate the dongle
|
||||||
|
per context or run it behind a trusted multiplexer.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Phase 1 — Reconnaissance
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`donglora` v1.5.2 — Embedded Rust firmware. Single binary per board, compiled with mutually-exclusive cargo features. Exposes the LoRa radio over a host transport (USB CDC-ACM or UART) using a COBS-framed binary protocol (DongLoRa Protocol v2).
|
||||||
|
|
||||||
|
## Languages / runtime
|
||||||
|
|
||||||
|
- **Rust 1.94 stable** for ARM (thumbv6m, thumbv7em); **Rust esp** fork for Xtensa (esp32, esp32-s3). `no_std`, `no_main`, `panic = abort`, LTO fat, opt-level 3.
|
||||||
|
- **No allocator.** All collections are fixed-capacity `heapless::Vec<_, N>`.
|
||||||
|
- **Async runtime: embassy-executor 0.10** (Cortex-M) / **esp-rtos 0.3** (Xtensa). Three long-lived tasks per board.
|
||||||
|
- **Statecharts: hsmc 0.6** drive the host, radio, and display tasks.
|
||||||
|
|
||||||
|
## Supported boards (one feature → one binary)
|
||||||
|
|
||||||
|
| Board | MCU | Radio | Host transport |
|
||||||
|
|-----------------------------|------------|--------|------------------------|
|
||||||
|
| heltec_mesh_node_t114 | nRF52840 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| heltec_v3 | ESP32-S3 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| heltec_v3_uart | ESP32-S3 | SX1262 | UART0 via CP2102 |
|
||||||
|
| heltec_v4 | ESP32-S3 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| elecrow_thinknode_m2 | ESP32-S3 | SX1262 | UART0 via CP2102 |
|
||||||
|
| lilygo_tbeam | ESP32 | SX1276 | UART0 via CP2102 |
|
||||||
|
| lilygo_tbeam_supreme | ESP32-S3 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| rak_wisblock_4631 | nRF52840 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| waveshare_rp2040_lora | RP2040 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
| wio_tracker_l1 | nRF52840 | SX1262 | USB CDC-ACM (native) |
|
||||||
|
|
||||||
|
## Entry points (untrusted-byte ingress)
|
||||||
|
|
||||||
|
There is **exactly one network-facing entry point per board image**: the host transport (USB CDC-ACM **or** UART, never both in the same build).
|
||||||
|
|
||||||
|
| Module | Inbound bytes from |
|
||||||
|
|------------------------------|-------------------------------------|
|
||||||
|
| `src/host/usb.rs` | USB CDC-ACM (host OS, kernel-side) |
|
||||||
|
| `src/host/uart.rs` | UART0 (CP2102 → host OS) |
|
||||||
|
| `src/radio.rs` (RX path) | LoRa over-the-air (any peer in RF range, on configured freq+sync) |
|
||||||
|
|
||||||
|
There is **no networking** (TCP/UDP/HTTP/etc.), **no filesystem**, **no shell**, **no storage**. The radio is the only RF surface; OTA RX packets become a `DeviceMessage::Rx(...)` forwarded to the host with `tag = 0` — the firmware itself does not interpret payload bytes.
|
||||||
|
|
||||||
|
USB descriptors / VID:PID `1209:5741` (composite IAD device). The CDC-ACM serial-number string is the device's MAC address as 12-char uppercase hex (`src/host/usb.rs:184-189`).
|
||||||
|
|
||||||
|
## Wire format (one path from byte → command)
|
||||||
|
|
||||||
|
```
|
||||||
|
raw bytes
|
||||||
|
→ FrameDecoder (donglora-protocol 1.2.0 frame.rs)
|
||||||
|
COBS-delimited, CRC16/CCITT-FALSE-protected, payload ≤ MAX_PAYLOAD_FIELD (275 B)
|
||||||
|
→ dispatch_frame (host/framing.rs)
|
||||||
|
rejects tag=0 with async ERR(EFRAME)
|
||||||
|
calls Command::parse(type_id, payload)
|
||||||
|
→ InboundEvent::Command { tag, cmd } via CommandChannel (depth 8)
|
||||||
|
→ radio task statechart (radio.rs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Egress (`DeviceMessage::Ok|Err|Rx|TxDone`) is symmetric: built by the radio task, sent through `OutboundChannel` (depth 64), encoded by `encode_outbound`, written packet-by-packet.
|
||||||
|
|
||||||
|
Commands accepted from host: `PING`, `GET_INFO`, `SET_CONFIG`, `TX`, `RX_START`, `RX_STOP` (only these `type_id` bytes; anything else → `ERR(EUNKNOWN_CMD)`).
|
||||||
|
|
||||||
|
## Trust boundaries
|
||||||
|
|
||||||
|
| Boundary | Direction | Treated as |
|
||||||
|
|---------------------------------------|-----------------|------------------|
|
||||||
|
| USB / UART host → firmware | inbound bytes | **trusted** (it's the user's PC) |
|
||||||
|
| LoRa RF peer → firmware (RX payload) | inbound bytes | **untrusted**, but only memcpy'd to host — firmware never interprets |
|
||||||
|
| firmware → SX1262/SX1276 via SPI | outbound | trusted (own peripheral) |
|
||||||
|
|
||||||
|
The firmware is intentionally a **transparent radio bridge**: there is no auth, no crypto, no session state, no key material on device. The radio's freq, power, sync_word, etc. are whatever the host sets (within hardware limits enforced by `validate_lora`).
|
||||||
|
|
||||||
|
## Auth / session layer
|
||||||
|
|
||||||
|
**None.** Anything that can write CDC-ACM bytes to the device can drive the radio. Per design — the device is a "dongle," ownership is physical-attachment.
|
||||||
|
|
||||||
|
The protocol's inactivity watchdog (`PROTOCOL.md §3.4`, 1 s) drops the radio to `Unconfigured` if the host stops sending bytes — this is liveness/cleanup, **not** auth. USB-side this is gated on DTR (USB CDC line state); UART has no DTR so it relies purely on the watchdog.
|
||||||
|
|
||||||
|
## Data at rest
|
||||||
|
|
||||||
|
**None of security interest.**
|
||||||
|
- Static `Info` blob built once at boot — MAC, fw version, capability bitmaps. Visible to anyone who issues `GET_INFO`.
|
||||||
|
- No credentials, tokens, keys, PII.
|
||||||
|
- Persistent storage: only board-specific bootloader settings region; **firmware never writes to flash at runtime** (see `ld/*.x` — app region is read-only post-flash).
|
||||||
|
- nRF52840 boards' `s140 SoftDevice` region (0x01000–0x26000) preserved at flash time; firmware never touches it.
|
||||||
|
|
||||||
|
## Dependencies (Cargo.lock, security-relevant subset)
|
||||||
|
|
||||||
|
- `donglora-protocol 1.2.0` — own crate, the wire codec (frame, command, modulation, info, events). Fuzz/mutation-tested in sibling repo (`/home/swaits/Code/donglora-org/protocol/`).
|
||||||
|
- `ucobs 0.3` — COBS encode/decode, no_std, very small.
|
||||||
|
- `lora-phy` — git fork from `swaits/lora-rs branch=fixed` (carries CAD IRQ-clear fixes pending upstream PRs #428/#431/#432). All SX126x/SX127x SPI primitives.
|
||||||
|
- `hsmc 0.6` — own crate (statecharts).
|
||||||
|
- `embassy-* 0.5/0.6/0.10/0.8` — well-known embedded async runtime.
|
||||||
|
- `defmt 0.3`, `panic-probe 1.0` (Cortex-M boards) / `esp-backtrace 0.19` + `esp-println 0.17` (Xtensa boards).
|
||||||
|
- `heapless 0.9` — fixed-capacity collections.
|
||||||
|
|
||||||
|
No HTTP, JSON, TLS, SQL, template, regex, deserialization-of-user-data dependencies.
|
||||||
|
|
||||||
|
## Deployment context
|
||||||
|
|
||||||
|
Firmware is flashed via UF2 (Cortex-M) or `espflash` (Xtensa). No OTA update path on the device itself — re-flashing requires physical USB. Production builds run with `DEFMT_LOG` unset → defmt strips info/debug/warn at compile time; only `error!` survives.
|
||||||
|
|
||||||
|
## Existing security controls / hardening already in place
|
||||||
|
|
||||||
|
- **`#![cfg_attr(not(test), no_std)]` / `panic = abort`** — no panic unwinding to abuse.
|
||||||
|
- **`#![deny(clippy::undocumented_unsafe_blocks)]`** in Cargo.toml — every `unsafe` block must justify itself.
|
||||||
|
- **`unsafe` audit** (4 sites total):
|
||||||
|
- `main.rs:105` — one-time `static mut INFO` init at boot, before any task spawns.
|
||||||
|
- `hal/nrf52840.rs:102, 140, 151` — `read_volatile` / `write_volatile` on MMIO registers (POWER.RESETREAS, FICR.DEVICEADDR). Standard.
|
||||||
|
- `hal/rp2040.rs:68, 79` — one-time `static mut MAC_REF` init at boot.
|
||||||
|
- **Memory layout (`ld/nrf52840-memory.x`)** preserves the factory SoftDevice region; UF2-only flashing prevents accidental overwrite.
|
||||||
|
- **CRC-16/CCITT-FALSE** on every frame, COBS framing — corrupt bytes are caught by either layer before reaching `Command::parse`.
|
||||||
|
- **Strict typed parsing**: every enum field rejects out-of-range bytes (`from_u8` returns `Option` and `InvalidField` is propagated); reserved bits in `TxFlags` rejected; reserved byte in `LrFhssConfig` rejected.
|
||||||
|
- **`validate_lora` (`radio.rs:2125-2145`)**: freq within board's chip-supported range, SF ∈ {5..=12} **and** in board bitmap, BW in board bitmap, TX power within board range, preamble ≥ 6.
|
||||||
|
- **Hardware-limit clamping**: `MAX_OTA_PAYLOAD = 255` matches the chip's `payload_len` register width; the receive buffer is sized exactly to it.
|
||||||
|
- **`RADIO_THROTTLED` backpressure flag** prevents queue floods if the host is unresponsive.
|
||||||
|
- **State-machine cancel-safety**: cancel-unsafe lora-phy futures (`lora.tx`, `lora.cad`, `LoRa::new`, `lora.init`) are hosted in chart states whose only event handlers are emitted by the future itself on natural completion — chart cannot drop them.
|
||||||
|
- **Reset-and-recovery state machine** with bounded attempts (`MAX_RECOVERY_ATTEMPTS = 4`) and a terminal `Dead` state that still answers `PING`/`GET_INFO`.
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# 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).
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
# Phase 3 — Targeted analysis (raw notes)
|
||||||
|
|
||||||
|
Walked through every category in MYTHOS Phase 3 against the firmware's actual code paths. Most categories are not applicable to no_std embedded LoRa firmware with no networking, filesystem, shell, database, templates, deserializers, crypto, or auth. The categories that *do* apply have been traced in full below.
|
||||||
|
|
||||||
|
## Categories N/A by construction
|
||||||
|
|
||||||
|
- **SQL / NoSQL injection** — no database, no query language anywhere.
|
||||||
|
- **Command injection** — no `exec` / `spawn`; the firmware has no shell.
|
||||||
|
- **Template / SSTI** — `minijinja` is used only in `build.rs` at build time on a developer-controlled template (`src/board/mod.rs.j2`). Not exposed to runtime input.
|
||||||
|
- **LDAP / XPath / EL** — none.
|
||||||
|
- **Header injection / CRLF / log injection** — no HTTP, no syslog, no user-controlled string output.
|
||||||
|
- **Authentication / Authorization** — none exists; firmware does not implement auth (design choice, see threat model).
|
||||||
|
- **Cryptography** — none used; CRC-16/CCITT-FALSE is integrity-only, not security.
|
||||||
|
- **Deserialization** — no serde, pickle, yaml, marshal, BinaryFormatter, or other reflective deserializer. Every field is decoded byte-by-byte via fixed-offset slicing in `donglora-protocol`.
|
||||||
|
- **XSS / CSRF / CORS / Open redirect / Prototype pollution / ReDoS** — no JS, no HTML, no regex, no URL routing.
|
||||||
|
- **SSRF** — no outbound HTTP / DNS / proxy. The "outbound URL fetcher" analog would be `lora.tx()`, but the host fully controls every parameter by design.
|
||||||
|
- **File operations / Zip slip / Symlink** — no filesystem at runtime.
|
||||||
|
- **XML** — no XML parser.
|
||||||
|
|
||||||
|
## Categories that DO apply (and the traces)
|
||||||
|
|
||||||
|
### Memory safety in parsing pipelines
|
||||||
|
|
||||||
|
Walked from `host_task` byte ingress to every consumer:
|
||||||
|
|
||||||
|
**Frame decode (`donglora-protocol-1.2.0/src/frame.rs`):**
|
||||||
|
- `FrameDecoder::buf: [u8; MAX_COBS_ENCODED = 283]` — bounded.
|
||||||
|
- Index `self.len` only ever written via `if self.len < self.buf.len() { ... self.len += 1 }` — safe.
|
||||||
|
- On overflow → `overflowed = true; len = 0` — bytes are discarded silently until the next `0x00` sentinel resyncs; never out-of-bounds.
|
||||||
|
- On `0x00` → `let mut decoded = [0u8; MAX_PRE_COBS_FRAME = 280]; ucobs::decode(&self.buf[..self.len], &mut decoded)` returns `Option<usize>`; None → emit `FrameDecodeError::Cobs` and reset.
|
||||||
|
- `emit_decoded` requires `decoded.len() >= FRAME_HEADER_SIZE + FRAME_TRAILER_SIZE` (5 B); checks CRC; only then slices `type_id / tag / payload` from fixed offsets.
|
||||||
|
|
||||||
|
**Confirmed safe**: no path to an OOB read/write; no panic; no unbounded allocation.
|
||||||
|
|
||||||
|
**Command parse (`donglora-protocol-1.2.0/src/commands.rs::Command::parse`):**
|
||||||
|
- `TYPE_TX`: requires `!payload.is_empty()`; reads `payload[0]` for flags; rejects reserved bits via `TxFlags::from_byte`; rejects `body.is_empty()`; rejects `body.len() > MAX_OTA_PAYLOAD`; `extend_from_slice` is bounded by both the slice length and the heapless capacity (`MAX_OTA_PAYLOAD = 255`).
|
||||||
|
- `TYPE_SET_CONFIG`: every field byte feeds an `Option`-returning `from_u8` (rejects out-of-range), or a `match { 0 => false, 1 => true, _ => Err }` (rejects non-boolean), or an explicit length check `if buf.len() != WIRE_SIZE { Err }`.
|
||||||
|
- All other types require empty payload.
|
||||||
|
|
||||||
|
**Confirmed safe.**
|
||||||
|
|
||||||
|
**`validate_lora` (`src/radio.rs:2125-2145`):** numeric range checks on `freq_hz`, `sf`, `bw`, `tx_power_dbm`, `preamble_len`. Shift-by-`sf` is safe (`sf` already bounded ≤12 by the preceding check, so `1u16 << sf ≤ 4096`); shift-by-`bw.as_u8()` is safe (`bw` enum repr is ≤ 13, `1u16 << 13 = 8192`).
|
||||||
|
|
||||||
|
Not validated: `preamble_len` upper bound, `sync_word`, the post-parser-checked `header_mode`/`payload_crc`/`iq_invert`.
|
||||||
|
|
||||||
|
- `preamble_len = 65535` — fed to `lora.create_*_packet_params`. `toa_us` math (`src/lbt.rs:44-77`) uses u64 throughout and clamps to `u32::MAX`. With SF12/BW7.81 kHz/preamble 65535: `t_preamble_us = 65535 * 524520 ≈ 3.4e10` which is u64-safe and gets clamped to `u32::MAX = 4.29e9`. **No overflow.** The radio chip's HW register handles whatever value is loaded; reasonable lora-phy behavior expected.
|
||||||
|
- `sync_word = anything` — the memory note flags this as intentional (matches RadioLib/Semtech precedent). Confirmed not a vulnerability.
|
||||||
|
|
||||||
|
**Confirmed safe.**
|
||||||
|
|
||||||
|
**`try_start_rx_hw` / `cache_rx_pkt` (`src/radio.rs:1348+, 1628+`):** `MAX_OTA_PAYLOAD as u8 = 255` — current `MAX_OTA_PAYLOAD = 255` exactly fits `u8::MAX`. If the constant were ever raised above 255 the `as u8` would silently truncate — but the SX126x chip register is one byte wide so the constant is hardware-bound. No current bug.
|
||||||
|
|
||||||
|
**Confirmed safe.**
|
||||||
|
|
||||||
|
**`collect_rx_packet` (`src/radio.rs:1664-1702`):**
|
||||||
|
- `rx_buf: [u8; MAX_OTA_PAYLOAD]` — the buffer handed to `lora.get_rx_result(pkt, &mut rx_buf)`.
|
||||||
|
- The chip's `RX_PLD_LENGTH` is `255` (set in `try_start_rx_hw`), and the chip hardware register is one byte wide → `len` returned is bounded by 255 at the silicon level.
|
||||||
|
- `let copy_len = (len as usize).min(MAX_OTA_PAYLOAD)` — defensive clamp.
|
||||||
|
- `data.extend_from_slice(&ctx.rx_buf[..copy_len])` into `HVec<u8, MAX_OTA_PAYLOAD>` — bounds-safe.
|
||||||
|
|
||||||
|
**Confirmed safe.**
|
||||||
|
|
||||||
|
### Panic reachability from wire input
|
||||||
|
|
||||||
|
Catalog of panic sites and reachability:
|
||||||
|
|
||||||
|
| Location | Trigger | Reachable from wire? |
|
||||||
|
|---|---|---|
|
||||||
|
| `radio.rs:937` `NonZeroU16::new(tag).expect(...)` for `InboundEvent::Command` | tag=0 reaches radio task | No — `host/framing.rs:36-40` rejects tag=0 with `ERR(EFRAME)` before forwarding to `commands` channel |
|
||||||
|
| `radio.rs:1209-1213` `tag_or_panic` | Action reads `incoming_tag` when None | No — actions that read it only run after `next_command` has stashed a tag (chart shape) |
|
||||||
|
| `radio.rs:1753-1754` `PendingTx.tag was 0` | Pending TX with tag=0 | No — same framing layer rejects tag=0 |
|
||||||
|
| `radio.rs:2232-2235` `radio chart returned` | Chart `run()` returns | No — chart is non-terminating (all leaf states have `during:` activities; this would require a chart-design bug, not a wire input) |
|
||||||
|
| `main.rs:108, 113, 125, 138, 145` `.expect("…")` | Spawn / init failure | Boot-time only |
|
||||||
|
| `host/usb.rs:189` `from_utf8(MAC hex)` | Invalid UTF-8 in MAC hex | Hex chars are constants `0-9 A-F`; always valid ASCII |
|
||||||
|
|
||||||
|
**Confirmed: no panic site is reachable from network input.**
|
||||||
|
|
||||||
|
### `unsafe` audit
|
||||||
|
|
||||||
|
Four `unsafe` blocks in firmware code:
|
||||||
|
|
||||||
|
1. `src/main.rs:105` — `INFO = Some(info); INFO.as_ref().expect(...)`. Touched only at boot, in the single-threaded `run(spawner)` before any task is spawned. Subsequent reads via `&'static Info` reference passed by argument. **Sound.**
|
||||||
|
2. `src/hal/nrf52840.rs:102, 140` — `read_volatile` / `write_volatile` on `0x40000400` (POWER.RESETREAS). Valid MMIO. **Sound.**
|
||||||
|
3. `src/hal/nrf52840.rs:151` — `read_volatile` on `0x10000000.byte_add(0xA4/0xA8)` (FICR.DEVICEADDR). Valid MMIO. **Sound.**
|
||||||
|
4. `src/hal/rp2040.rs:68, 79` — `static mut MAC_REF` written once in `init_mac` (single-threaded init), read-only thereafter. **Sound.**
|
||||||
|
|
||||||
|
**Confirmed: no unsoundness in declared unsafe blocks.**
|
||||||
|
|
||||||
|
### TOCTOU / races
|
||||||
|
|
||||||
|
All cross-task communication uses `embassy_sync::channel::Channel` with `CriticalSectionRawMutex` — atomic enqueue/dequeue. Cross-task atomic flags (`RESET_PENDING`, `RADIO_THROTTLED`, `LORA_WEDGED`) use `portable_atomic::AtomicBool` with `AcqRel` swap or `Acquire/Release` loads/stores. The radio chip itself has exactly one user (the radio task), so there is no SPI-bus race.
|
||||||
|
|
||||||
|
**No exploitable race observed.**
|
||||||
|
|
||||||
|
### Information disclosure
|
||||||
|
|
||||||
|
USB serial-number descriptor = MAC address (hex). `GET_INFO` returns same MAC in `mcu_uid`. Either is readable by anyone enumerating the USB device or any successful `GET_INFO`, with no authentication.
|
||||||
|
|
||||||
|
- **Status:** Confirmed
|
||||||
|
- **Severity:** Info (privacy / cross-host tracking)
|
||||||
|
- **Reasoning:** This is the device's only stable cross-context identifier and it leaks unconditionally. A user reusing the dongle across hosts/networks is trivially correlatable.
|
||||||
|
|
||||||
|
### Dependency / supply chain
|
||||||
|
|
||||||
|
- `donglora-protocol 1.2.0` — sibling crate, fuzz-tested in `/home/swaits/Code/donglora-org/protocol/fuzz/`. No known CVEs.
|
||||||
|
- `ucobs 0.3` — small crate; no known CVEs.
|
||||||
|
- `lora-phy` — `git = "https://github.com/swaits/lora-rs.git", branch = "fixed"`, no `rev=` pin. Cargo.lock pins the current commit by SHA → reproducible *today*. But `cargo update -p lora-phy` will silently advance to whatever is on `branch=fixed` at that moment. If the maintainer's GitHub account is compromised, the next `cargo update` ingests the malicious commit.
|
||||||
|
- `heapless 0.9`, `embassy-* 0.5/0.6/0.10/0.8`, `defmt 0.3`, `esp-hal 1.1`, `embassy-usb 0.6` — all current versions; no known reachable CVE in their used surface.
|
||||||
|
|
||||||
|
The `branch=` (not `rev=`) git dep is the only flag — a **Low** finding (hardening).
|
||||||
|
|
||||||
|
### USB stack / control-transfer surface
|
||||||
|
|
||||||
|
`embassy_usb` handles control transfers internally. The firmware allocates `CTRL_BUF: [u8; 128]`. A code comment (`host/usb.rs:191-196`) records that the previous 64-byte size matched the panic-site message `"control buffer too small"` in `embassy_usb/src/lib.rs:758` — a real bug observed during enumeration. The 128-byte size is documented as safely above the largest CDC-ACM request.
|
||||||
|
|
||||||
|
**Status:** Mitigated; relies on third-party (embassy-usb 0.6) correctness for control-transfer parsing. Not an actionable firmware finding.
|
||||||
|
|
||||||
|
### Reliability bug worth noting (not strictly a security finding)
|
||||||
|
|
||||||
|
`lilygo_tbeam` (ESP32 classic) — the host transport is UART0 (`GPIO1/GPIO3 → CP2102 → host`). `esp-println` with the `auto` feature falls back to UART0 on ESP32 classic (no USB-Serial-JTAG peripheral). In production builds (`DEFMT_LOG` unset → `error!` only), if the radio chart enters `Dead` or similar `error!`-emitting paths, defmt bytes are emitted onto the same UART as the binary protocol, corrupting frames.
|
||||||
|
|
||||||
|
- **Status:** Suspected; not reachable from wire input (the error-level logs fire on hardware faults).
|
||||||
|
- **Severity:** Low (reliability, not security).
|
||||||
|
- **Why noted:** Documents a real frame-corruption scenario that an operator should be aware of when soak-testing the T-Beam classic.
|
||||||
|
|
||||||
|
For `heltec_v3_uart` and `elecrow_thinknode_m2` (both ESP32-S3, which has USB-Serial-JTAG), esp-println's `auto` selects USB-Serial-JTAG, which is a *different* peripheral than the host UART. Those boards are NOT affected.
|
||||||
|
|
||||||
|
### Intentional design choices flagged for transparency
|
||||||
|
|
||||||
|
These are NOT findings — they are deliberate, documented features. They appear here so the audit's reader knows they were considered and accepted:
|
||||||
|
|
||||||
|
- **No authentication on host transport.** A "dongle" trust model: physical attachment = full access.
|
||||||
|
- **No RF regulatory enforcement.** Frequency, sync word, and TX power are whatever the host sends (within hardware-supported range). FCC / CE / ETSI compliance is the *user's* responsibility.
|
||||||
|
- **TX `skip_cad=1` bypasses carrier-sense.** Documented in `PROTOCOL.md`; available to anyone with host-transport access.
|
||||||
|
|
||||||
|
## Items considered and dismissed
|
||||||
|
|
||||||
|
- Panic-on-malformed-frame (no — framing layer reflects `ERR(EFRAME)` and clears the decoder).
|
||||||
|
- Integer overflow in `toa_us` (no — u64 intermediates, u32 clamp; bound-checked).
|
||||||
|
- Buffer overrun on max-payload RX (no — chip HW register caps to 255; firmware buffer is sized to 255; `extend_from_slice` is bounded).
|
||||||
|
- Frame-decoder buffer exhaustion DoS (mitigated — overflow state silently discards bytes until resync; one packet, one frame; no accumulation across the limit).
|
||||||
|
- Stack overflow from deep call chains (no recursion; nested stack frames in `dispatch_bytes → feed → emit_decoded → Command::parse → Modulation::decode → LoRaConfig::decode` use ~1.5 KB total on a 4–32 KB task stack budget).
|
||||||
|
- Memory safety in the unsafe blocks (audited above).
|
||||||
|
- TOCTOU between USB read and radio dispatch (channel-mediated; no race).
|
||||||
|
- Side channels (timing on CRC compare, etc.) — irrelevant: there are no secrets to time-leak.
|
||||||
|
- `panic-probe` emitting panic message: emitted via `defmt-rtt`, which is a *different* hardware channel (SWD probe) than USB/UART. A panic locks the device (DoS) but does not leak to host or peer.
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
# Security Audit Report — donglora firmware v1.5.2
|
||||||
|
|
||||||
|
Auditor: **Mythos** (adversarial security review per `MYTHOS.md`)
|
||||||
|
Repo: `/home/swaits/Code/donglora-org/firmware`, HEAD `688938d` (chore: release 1.5.2)
|
||||||
|
Phases: Recon (`01-recon.md`) → Threats (`02-threats.md`) → Targeted analysis (`03-findings-draft.md`) → Verification → this report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive summary
|
||||||
|
|
||||||
|
**3 findings total: 0 Critical, 0 High, 0 Medium, 2 Low, 1 Info.**
|
||||||
|
|
||||||
|
The firmware is a small, tightly-scoped, no_std embedded Rust binary that exposes a SX126x/SX127x radio over a single host transport (USB CDC-ACM or UART). It performs **no networking, no filesystem I/O, no shell exec, no template rendering, no SQL/NoSQL/XML, no cryptography, and no deserialization beyond explicit byte slicing**. The whole vulnerability landscape MYTHOS targets is out of scope by construction; the realistic attack surface is exactly two paths: (1) inbound frames on the host transport, and (2) inbound LoRa packets on the configured RF parameters.
|
||||||
|
|
||||||
|
Both paths were traced end-to-end:
|
||||||
|
|
||||||
|
- **Host-transport ingress.** Every reachable byte goes through a CRC-checked, COBS-framed `FrameDecoder` → strict typed `Command::parse` → numeric-bounded `validate_lora`. Reserved bits, unknown enum values, length mismatches, and tag=0 are all explicitly rejected and turned into `ERR(...)` responses. No reachable panic, no buffer overrun, no integer overflow, no OOB access was found.
|
||||||
|
- **OTA RX ingress.** Payload bytes are bounded to 255 (SX126x register width); copied into a `heapless::Vec<u8, 255>` and forwarded to the host verbatim. The firmware never parses OTA bytes — there is no interpretation surface for a remote attacker to exploit.
|
||||||
|
|
||||||
|
The remaining findings concern **disclosure** (MAC visible in the USB serial descriptor — by design but worth documenting), **supply-chain hardening** (git dep not commit-pinned), and **reliability on one board variant** (esp-println output collides with host UART on lilygo_tbeam classic only). All are Low or Info severity.
|
||||||
|
|
||||||
|
**Top 3 systemic observations (NOT findings — design choices flagged for transparency):**
|
||||||
|
|
||||||
|
1. **No authentication on host transport.** Anyone with kernel-level access to the CDC-ACM/UART device node controls the radio. The product is intentionally a "transparent dongle" — physical attachment is the auth boundary.
|
||||||
|
2. **No RF-regulatory enforcement.** Frequency, sync word, and TX power are whatever the host commands, within hardware capability. FCC / CE / ETSI compliance is the user's responsibility.
|
||||||
|
3. **`TX skip_cad=1` bypasses listen-before-talk on demand.** Documented in `PROTOCOL.md`; available to any host-side actor.
|
||||||
|
|
||||||
|
These are deliberate features of a transparent LoRa bridge; treating them as bugs would be a category error. They are surfaced here so a security reader knows the trust model.
|
||||||
|
|
||||||
|
**Recommended remediation priorities:** None are urgent. The two Low findings can be addressed when convenient as hardening work; the Info finding is a documentation matter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### [LOW] lora-phy git dependency uses mutable branch ref, not a pinned commit
|
||||||
|
|
||||||
|
- **File(s):** `Cargo.toml:35`
|
||||||
|
- **Category:** Supply chain
|
||||||
|
- **Status:** Confirmed
|
||||||
|
- **Severity:** Low (CVSS-ish 4.4 — depends on a future `cargo update` and a maintainer-account compromise; reproducible builds are not affected today)
|
||||||
|
- **CWE:** CWE-829 (Inclusion of functionality from untrusted control sphere)
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
|
||||||
|
`lora-phy` is sourced from a git fork via `branch = "fixed"` rather than `rev = "<sha>"`. `Cargo.lock` currently pins the specific commit `043d7659…` (confirmed in `Cargo.lock:632-640`), so builds in this exact tree are reproducible. However, any future `cargo update -p lora-phy` (or running on a developer machine without a committed lockfile, which the firmware repo _does_ commit) will silently advance to whatever sits on `branch=fixed` at that moment. If the `swaits/lora-rs` GitHub account were compromised, the next `cargo update` ingests the malicious commit and the next-built firmware ships it.
|
||||||
|
|
||||||
|
#### Data flow
|
||||||
|
|
||||||
|
1. `Cargo.toml:35` declares `lora-phy = { git = "https://github.com/swaits/lora-rs.git", branch = "fixed", features = ["defmt-03"] }`.
|
||||||
|
2. `Cargo.lock:634` records the current resolved commit hash.
|
||||||
|
3. Any contributor running `cargo update` (or any auto-update tooling on the maintainer's machine) replaces the lockfile entry with whatever is at HEAD of the branch at update time.
|
||||||
|
4. The resulting binary executes whatever the new HEAD commit contains, with no human-readable diff at the dependency boundary unless someone inspects the lockfile delta.
|
||||||
|
|
||||||
|
#### Proof of concept
|
||||||
|
|
||||||
|
Reproducibility today is fine — `cargo build` against the committed lockfile uses commit `043d7659…`. The exposure is **temporal**: the moment `cargo update -p lora-phy` runs, the lockfile takes whatever commit is then on the branch. There is no PoC to demonstrate beyond reading the manifest.
|
||||||
|
|
||||||
|
#### Impact
|
||||||
|
|
||||||
|
A maintainer-account compromise (GitHub credential theft, PAT leak, repo-write malware) lets the attacker push a backdoored commit to the `fixed` branch. The next build that runs `cargo update` ships the backdoor into every flashed dongle. Severity is bounded by (a) the rate at which `cargo update` is run and (b) the difficulty of compromising the maintainer's GitHub. It's not zero — pinning to a commit closes the window.
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
Switch the manifest from `branch = "..."` to `rev = "<commit_sha>"`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
lora-phy = { git = "https://github.com/swaits/lora-rs.git", rev = "043d7659dfe2148913f16b2f7ea3e9241642b770", features = ["defmt-03"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Re-pin (bump the `rev`) on each lora-phy upgrade. Drops the temporal exposure entirely; the dep changes only when a human edits the manifest. `TODO.md` already plans to drop the fork once upstream PRs merge — at that point switch back to a crates.io version, which gets the same effect from the registry's immutability.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [LOW] On `lilygo_tbeam` (ESP32 classic) only, esp-println output collides with host UART
|
||||||
|
|
||||||
|
- **File(s):** `src/board/lilygo_tbeam.rs:112-120`, `Cargo.toml:73-77`, `src/main.rs:53-57`
|
||||||
|
- **Category:** Wire-level reliability / information disclosure
|
||||||
|
- **Status:** Confirmed (frame corruption when error-level logs fire); not reachable from wire input today
|
||||||
|
- **Severity:** Low (reliability with disclosure side-effects; not directly attacker-triggerable)
|
||||||
|
- **CWE:** CWE-209 (Information Exposure Through an Error Message) — secondary
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
|
||||||
|
For the `lilygo_tbeam` build target (ESP32 classic, SX1276, CP2102-bridged UART), the host transport is `UART0` on `GPIO1/GPIO3`. `esp-println 0.17` is enabled with the `auto` (default) + `defmt-espflash` features. On ESP32 classic there is no USB-Serial-JTAG peripheral, so `auto` falls back to `UART0` — the _same_ UART the host protocol uses. Any defmt output (in production builds, only `error!` survives the compile-time filter) writes bytes onto the host wire mid-frame, corrupting the binary protocol the host is reading.
|
||||||
|
|
||||||
|
For `heltec_v3_uart` and `elecrow_thinknode_m2` (ESP32-S3, which _does_ have USB-Serial-JTAG), `esp-println auto` selects USB-Serial-JTAG — a _different_ peripheral. Those boards are unaffected.
|
||||||
|
|
||||||
|
#### Data flow
|
||||||
|
|
||||||
|
1. `Cargo.toml:73-77` pulls `esp-println` with `defmt-espflash` (no `uart` / `jtag-serial` feature explicitly set → `auto` picks at runtime).
|
||||||
|
2. `src/main.rs:53-57` installs `esp_println` as defmt's global handler for the esp boards.
|
||||||
|
3. `src/board/lilygo_tbeam.rs:113-120` initializes `Uart::new(p.UART0, ...).with_tx(p.GPIO1).with_rx(p.GPIO3)` as the host transport.
|
||||||
|
4. The ESP32 classic has no `USB_SERIAL_JTAG` peripheral → esp-println's runtime auto-selector falls back to UART0.
|
||||||
|
5. When a `defmt::error!(...)` or `defmt::panic!(...)` line executes on this board, esp-println writes bytes to UART0 → host sees them interleaved with COBS-framed protocol bytes.
|
||||||
|
|
||||||
|
#### Reachability from wire input
|
||||||
|
|
||||||
|
Walked the `error!` and `panic!` callsites: all fire only on hardware faults (`Recovering: exhausted ... transitioning to Dead`, `radio chart entered Dead state`, `lora cold init failed`, chart-invariant panics that are themselves not wire-reachable). **None are reachable from a malformed command.** Treat as a reliability concern, not a wire-attack.
|
||||||
|
|
||||||
|
#### Proof of concept
|
||||||
|
|
||||||
|
Run firmware compiled for `lilygo_tbeam`, intentionally induce a hardware-side error (e.g., disconnect the SX1276 SPI bus while the host is sending commands). Observe `defmt error!` output corrupting the COBS frames on the same UART. Without hardware tampering there is no PoC.
|
||||||
|
|
||||||
|
#### Impact
|
||||||
|
|
||||||
|
If the chip ever wedges hard enough to drive the chart into `Recovering` exhaustion or `Dead`, the host suddenly sees frame-CRC errors caused by defmt bytes instead of the chip-failure response it should have received. The host's framing layer recovers at the next clean `0x00` sentinel, but the lost frames could be the very `ERadio` reply that would have signaled the underlying failure. **Diagnostic blindness during hardware failure** on this one board variant.
|
||||||
|
|
||||||
|
#### Fix
|
||||||
|
|
||||||
|
Pick one of these for the lilygo_tbeam build:
|
||||||
|
|
||||||
|
- **Option A (preferred):** add `"no-op"` to the `esp-println` feature set for the `lilygo_tbeam` board feature, so the defmt global handler discards output entirely. Trade-off: no `probe-rs` defmt log capability on this board. Acceptable since the board has no SWD path anyway — it's CP2102-only.
|
||||||
|
- **Option B:** explicitly route esp-println to a _different_ UART (e.g., `UART1`) by adding the `uart` feature with manual pin selection. Requires reading the esp-println 0.17 docs to confirm the pin override hook.
|
||||||
|
|
||||||
|
This is also the spirit of the existing `debug-checkpoint` feature for UART-only boards (the LED-blink workaround), per `Cargo.toml:139-144`. The same "no shared UART for protocol + logs" rule should apply.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [INFO] MAC address disclosed in USB serial descriptor and `GET_INFO` without authentication
|
||||||
|
|
||||||
|
- **File(s):** `src/host/usb.rs:182-189`, `src/main.rs:159-161, 180-181`
|
||||||
|
- **Category:** Information disclosure / privacy
|
||||||
|
- **Status:** Confirmed
|
||||||
|
- **Severity:** Info (intentional disclosure of a stable device identifier; standard for USB devices but worth documenting)
|
||||||
|
- **CWE:** CWE-200 (Exposure of Sensitive Information)
|
||||||
|
|
||||||
|
#### Summary
|
||||||
|
|
||||||
|
The device's 6-byte hardware MAC (nRF52840 FICR.DEVICEADDR / ESP32 eFuse / RP2040 flash-UID-derived) appears in two places, both readable without any authentication:
|
||||||
|
|
||||||
|
1. The USB CDC-ACM serial-number string descriptor (12 chars uppercase hex, no separator).
|
||||||
|
2. `GET_INFO.mcu_uid` (first 6 bytes; remaining 26 are zero).
|
||||||
|
|
||||||
|
Both surfaces are present at every boot. USB descriptors are read by every OS during enumeration without user consent; `GET_INFO` is wire-reachable by anyone who can write to the CDC-ACM/UART device.
|
||||||
|
|
||||||
|
#### Data flow (USB descriptor)
|
||||||
|
|
||||||
|
1. `src/main.rs:99-109` — boot reads `parts.mac` via `board::Board::mac_address()`.
|
||||||
|
2. `src/main.rs:118-124` — MAC passed to `host_task`.
|
||||||
|
3. `src/host/usb.rs:185-189` — MAC bytes formatted as 12 ASCII hex chars; assigned to `config.serial_number`.
|
||||||
|
4. Embassy USB stack publishes it as USB string descriptor (the OS retrieves on enumeration).
|
||||||
|
|
||||||
|
#### Data flow (GET_INFO)
|
||||||
|
|
||||||
|
1. `src/main.rs:159-161` — `mcu_uid[..6].copy_from_slice(&mac); Info.mcu_uid_len = 6`.
|
||||||
|
2. Any host issuing a valid `PING`-tag-2-format `GET_INFO` frame gets the `Info` blob back, including the 6-byte MAC.
|
||||||
|
|
||||||
|
#### Impact
|
||||||
|
|
||||||
|
The MAC is a permanent, factory-burned device identifier. A user moving a dongle between hosts/networks (laptop ↔ desk; home ↔ office; conference Wi-Fi) is correlatable across contexts by any malicious userspace program that enumerates USB. The information is _useful_ — `donglora-mux` and friends rely on it to distinguish multiple dongles — so it cannot simply be removed.
|
||||||
|
|
||||||
|
**This is also the standard model for USB devices in general.** Every USB device with a serial-number descriptor exposes a stable identifier; the user gets no consent prompt at enumeration time. Treat this finding as documentation rather than a remediation request.
|
||||||
|
|
||||||
|
#### Fix (optional, only if you care about cross-host de-anonymization)
|
||||||
|
|
||||||
|
- Replace the MAC in the USB serial descriptor with a per-install random byte string generated once and stored in MCU non-volatile memory (if available). Keep the real MAC accessible via authenticated `GET_INFO` if/when authentication is added.
|
||||||
|
- Or: leave the descriptor MAC alone (it's the standard pattern) and document the privacy expectation in `README.md` so users understand the dongle is trackable.
|
||||||
|
|
||||||
|
The Info-severity rating is the right call: this is a transparency / documentation question, not an actionable bug.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix A — Out-of-scope items
|
||||||
|
|
||||||
|
The audit did not cover:
|
||||||
|
|
||||||
|
- **Host-side applications** that consume the protocol (`ai-bot`, `bridge`, `client-py`, `mux-rs`, `mux-py`, `client-rs`, `tools`). Each is a separate audit target.
|
||||||
|
- **The `donglora-protocol` crate's invariants** beyond how the firmware uses them. The sibling repo at `/home/swaits/Code/donglora-org/protocol/` already maintains a fuzz suite (`protocol/fuzz/`) and mutation tests (`protocol/mutants.out*`); those validate the codec independent of this audit.
|
||||||
|
- **Physical-layer / RF-regulatory compliance** (FCC / CE / ETSI). The firmware is _intentionally_ transparent; user is responsible.
|
||||||
|
- **Side-channel analysis** (timing, power, electromagnetic) — irrelevant on a device with no secrets.
|
||||||
|
- **Build-time compromise** (malicious `build.rs`, compromised toolchain, supply-chain attacks against rustc / espup / cargo). The Low finding above is the closest the audit gets to this surface.
|
||||||
|
- **Bootloader / DFU surface** (UF2 / Adafruit nRF52 bootloader / espflash). Those are upstream code paths; this audit covers the application image only.
|
||||||
|
- **Hardware tampering / cold-boot RAM scraping / glitching the MCU**. Out of scope for source-code audit.
|
||||||
|
|
||||||
|
## Appendix B — Negative findings (audited and concluded not vulnerable)
|
||||||
|
|
||||||
|
Each item below was checked at source level and confirmed safe. This list is the most useful part of the audit: it documents what was _examined_ and why a Critical / High finding does not exist.
|
||||||
|
|
||||||
|
- **Frame decoder buffer handling** (`donglora-protocol-1.2.0/src/frame.rs::FrameDecoder::feed`): fixed-size buf, bounded index, overflow → resync. No OOB. No panic. No unbounded growth.
|
||||||
|
- **CRC-16/CCITT-FALSE on every frame**: cryptographically weak (it's a CRC, not a MAC) but appropriate for transport integrity. Defends against the firmware acting on bit-flipped frames.
|
||||||
|
- **`Command::parse` for every type_id** (`donglora-protocol-1.2.0/src/commands.rs:133-181`): reserved bits, unknown enum values, length mismatches, empty payloads all rejected explicitly with typed errors that the firmware turns into `ERR(ELENGTH|EPARAM|EMODULATION|EUNKNOWN_CMD)`. No silent acceptance.
|
||||||
|
- **`Modulation::decode` and `LoRaConfig::decode`** (`donglora-protocol-1.2.0/src/modulation.rs:743-755, 224-263`): every byte either bound-checked by `from_u8` or `match` arms reject non-{0,1} booleans. `WrongLength` returns on size mismatch.
|
||||||
|
- **`validate_lora`** (`radio.rs:2125-2145`): freq, SF, BW, power, preamble bounds. The two unchecked fields (`preamble_len` upper bound and `sync_word`) were traced into downstream `toa_us` (u64 math with `u32::MAX` clamp — confirmed overflow-safe) and `set_lora_sync_word` (per memory-note, silent acceptance is intentional).
|
||||||
|
- **`try_start_rx_hw` → `prepare_for_rx` chain** (`radio.rs:1348-1438`): `MAX_OTA_PAYLOAD as u8 = 255` is hardware-bounded (SX126x payload-length register is one byte). No truncation today; would surface at compile-time if `MAX_OTA_PAYLOAD` ever changed.
|
||||||
|
- **`collect_rx_packet`** (`radio.rs:1664-1702`): `rx_buf: [u8; MAX_OTA_PAYLOAD]` matches the chip-level cap. `copy_len = len.min(MAX_OTA_PAYLOAD)`. `extend_from_slice` is bounds-safe.
|
||||||
|
- **Panic reachability from wire input**: 6 panic sites enumerated; all reach only via chart-shape violations, not from network input. Specifically, the `NonZeroU16::new(tag).expect(...)` at `radio.rs:937` is gated by the `if tag == 0 { ... return; }` at `host/framing.rs:36-40` _before_ the envelope can be enqueued.
|
||||||
|
- **`unsafe` blocks** (4 sites): one-time-init `static mut` patterns (sound under stated preconditions, before any task spawns) and MMIO `read_volatile` / `write_volatile` (sound for `RESETREAS` / `FICR.DEVICEADDR`).
|
||||||
|
- **Cross-task races / TOCTOU**: `embassy_sync::channel::Channel<CriticalSectionRawMutex, ...>` for every cross-task hop. `portable_atomic::AtomicBool` with `AcqRel` swaps for shared flags. The radio chip has exactly one user (radio task), so no SPI-bus race.
|
||||||
|
- **USB-descriptor / control-transfer handling**: `embassy_usb` third-party. The firmware's `CTRL_BUF = 128` is explicitly oversized to defeat a known panic site in the 0.5/0.6 era (documented in source comment at `host/usb.rs:191-196`).
|
||||||
|
- **`embassy_usb_driver::EndpointError` handling** (`host/usb.rs:281-313`): `Disabled` is logged at debug only (normal during enumeration), `BufferOverflow` at warn — and the decoder is reset and the read loop sleeps 100 ms to avoid burning the executor on a stuck driver.
|
||||||
|
- **Stack overflow from nested parse calls**: depth-bounded (`feed → emit_decoded → Command::parse → Modulation::decode → LoRaConfig::decode`); cumulative stack use ≈ 1.5 KB on a 4–32 KB task stack.
|
||||||
|
- **DoS via async-err flood**: `RADIO_THROTTLED` gates `emit_async_err`; outbound depth-64 ring absorbs bursts; the channel can never grow.
|
||||||
|
- **DoS via Reset queue flood**: `RESET_PENDING` AtomicBool dedupes injected resets; documented at `channel.rs:16-29`.
|
||||||
|
- **Reachable `defmt::panic!()` in `radio_task` epilogue**: gated by chart non-termination invariant (every leaf has a `during:` activity); not reachable by wire input.
|
||||||
|
- **TX path payload handling**: `pending_tx.data` is `heapless::Vec<u8, MAX_OTA_PAYLOAD>` — bounded at parse-time. `lora.tx()` writes whatever bytes the host supplied. No memory-corruption surface; no auth surface to bypass.
|
||||||
|
- **OTA RX → outbound queue**: the firmware does not interpret OTA bytes. Wire encoding (`events.rs:228-249`) is a fixed-layout memcpy with bound checks at the front (`if buf.len() < total { Err }`).
|
||||||
|
- **`InboundEvent::Reset` clears the radio chart`incoming_tag` slot** (`radio.rs:923-930`) so a tag-echoing action that runs after Reset doesn't pull a stale tag.
|
||||||
|
- **No deserializers that take untrusted input by reflection** (no serde, no XML, no YAML, no pickle, no protobuf with dynamic types).
|
||||||
|
- **No SQL, NoSQL, shell, template, regex, HTTP, DNS, file, zip, XML surfaces in this firmware.**
|
||||||
|
|
||||||
|
## Appendix C — Confidence and methodology
|
||||||
|
|
||||||
|
- **Confidence:** High for the negative-findings list (every claim was traced with file:line); High for the three findings (each verified by re-reading the cited code after writing the draft).
|
||||||
|
- **Method:** MYTHOS Phase 1–5 walkthrough as defined in `MYTHOS.md`. Static review only; no runtime fuzzing was performed in this pass. The donglora-protocol crate's own fuzz suite (sibling repo) is presumed to validate codec invariants and is not duplicated here.
|
||||||
|
- **Not done:** `cargo audit` / `cargo deny` were not run as part of this audit. Recommended as a CI addition (CHANGELOG.md mentions hardening pulses; this would fit naturally). At the time of writing, no known reachable CVE for any used dependency surface is known to the auditor.
|
||||||
|
|
||||||
|
## Self-check
|
||||||
|
|
||||||
|
- [x] Every finding has file + line numbers
|
||||||
|
- [x] Every Confirmed finding has a PoC, a data-flow trace, or an unambiguous "this is what's in the source today" reference (the Low / Info ratings make a remote PoC moot for the supply-chain and disclosure findings)
|
||||||
|
- [x] Every finding has a one-sentence impact statement
|
||||||
|
- [x] No findings are pure pattern-matches without reachability analysis
|
||||||
|
- [x] Severity ratings are calibrated — nothing rated above Low; that matches what the audit actually found
|
||||||
|
- [x] Negative findings appendix exists
|
||||||
|
- [x] No suggestions beyond what's needed to fix the actual bug
|
||||||
+46
-26
@@ -1252,29 +1252,33 @@ impl RadioActions for RadioActionContext<'_> {
|
|||||||
Some(lora) => reconfigure_radio(lora, &cfg).await,
|
Some(lora) => reconfigure_radio(lora, &cfg).await,
|
||||||
None => Err(RadioError::Busy),
|
None => Err(RadioError::Busy),
|
||||||
};
|
};
|
||||||
if recfg_result.is_err() {
|
let mdltn = match recfg_result {
|
||||||
// Hardware failure: drop to Unconfigured (spec §6.3).
|
Ok(m) => m,
|
||||||
// Clear all per-session state — including any in-flight
|
Err(_) => {
|
||||||
// RxStart tag — so the next CmdRxStart in Unconfigured
|
// Hardware failure: drop to Unconfigured (spec §6.3).
|
||||||
// is rejected cleanly (rather than racing a stale tag
|
// Clear all per-session state — including any in-flight
|
||||||
// through respond_err_start_rx on an unrelated path).
|
// RxStart tag — so the next CmdRxStart in Unconfigured
|
||||||
self.config = None;
|
// is rejected cleanly (rather than racing a stale tag
|
||||||
self.cad_retry_delay_us = 0;
|
// through respond_err_start_rx on an unrelated path).
|
||||||
self.pending_rx_start_tag = None;
|
self.config = None;
|
||||||
info!("config cleared (apply_set_config failure tag={=u16})", tag);
|
self.cad_retry_delay_us = 0;
|
||||||
self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await;
|
self.pending_rx_start_tag = None;
|
||||||
// Wedge takes precedence over ConfigFailedRadio so the
|
info!("config cleared (apply_set_config failure tag={=u16})", tag);
|
||||||
// chart enters Recovering (NRESET + lora.init()) instead
|
self.send(tag, DeviceMessage::Err(ErrorCode::ERadio)).await;
|
||||||
// of bouncing back to Unconfigured with a still-wedged
|
// Wedge takes precedence over ConfigFailedRadio so the
|
||||||
// chip that the next SET_CONFIG would also fail on.
|
// chart enters Recovering (NRESET + lora.init()) instead
|
||||||
if take_wedge_flag() {
|
// of bouncing back to Unconfigured with a still-wedged
|
||||||
let _ = self.emit(RadioInput::WedgeDetected);
|
// chip that the next SET_CONFIG would also fail on.
|
||||||
} else {
|
if take_wedge_flag() {
|
||||||
let _ = self.emit(RadioInput::ConfigFailedRadio);
|
let _ = self.emit(RadioInput::WedgeDetected);
|
||||||
|
} else {
|
||||||
|
let _ = self.emit(RadioInput::ConfigFailedRadio);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
};
|
||||||
}
|
|
||||||
self.config = Some(cfg);
|
self.config = Some(cfg);
|
||||||
|
self.mdltn = Some(mdltn);
|
||||||
info!("config set (apply_set_config success tag={=u16})", tag);
|
info!("config set (apply_set_config success tag={=u16})", tag);
|
||||||
self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B);
|
self.cad_retry_delay_us = toa_us(&cfg, CAD_ASSUMED_PEER_PAYLOAD_B);
|
||||||
if self.cad_retry_delay_us > CAD_BUDGET_US {
|
if self.cad_retry_delay_us > CAD_BUDGET_US {
|
||||||
@@ -2094,12 +2098,28 @@ fn to_lora_mod_params(
|
|||||||
lora.create_modulation_params(sf, bw, cr, cfg.freq_hz)
|
lora.create_modulation_params(sf, bw, cr, cfg.freq_hz)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn reconfigure_radio(lora: &mut LoRaDriver, cfg: &LoRaConfig) -> Result<(), RadioError> {
|
async fn reconfigure_radio(
|
||||||
|
lora: &mut LoRaDriver,
|
||||||
|
cfg: &LoRaConfig,
|
||||||
|
) -> Result<lora_phy::mod_params::ModulationParams, RadioError> {
|
||||||
lora_setup("reconfigure_radio.enter_standby", lora.enter_standby()).await?;
|
lora_setup("reconfigure_radio.enter_standby", lora.enter_standby()).await?;
|
||||||
// Calibrate image etc. via a throwaway create_modulation_params call
|
// Apply the host-supplied LoRa sync word. The protocol field is a
|
||||||
// — inside the SET_CONFIG OK window so first TX/RX is fast.
|
// u16 holding the SX126x register pair in big-endian order
|
||||||
let _ = to_lora_mod_params(lora, cfg)?;
|
// (RadioLib convention: 0x1424 == byte 0x12 == LoRa "private" /
|
||||||
Ok(())
|
// MeshCore; 0x3444 == byte 0x34 == LoRaWAN "public"). lora-phy's
|
||||||
|
// set_lora_sync_word takes the underlying single-byte form, so
|
||||||
|
// undo the nibble encoding: convert_sync_word(byte) puts the
|
||||||
|
// chip's mandatory 0x4 in the low nibble of each register byte,
|
||||||
|
// and the high nibble of each register byte holds the original
|
||||||
|
// byte's high/low nibble respectively.
|
||||||
|
let [hi, lo] = cfg.sync_word.to_be_bytes();
|
||||||
|
let sync_byte = (hi & 0xF0) | (lo >> 4);
|
||||||
|
lora_setup(
|
||||||
|
"reconfigure_radio.set_lora_sync_word",
|
||||||
|
lora.set_lora_sync_word(sync_byte),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
to_lora_mod_params(lora, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_lora(cfg: &LoRaConfig, info: &Info) -> Result<(), ErrorCode> {
|
fn validate_lora(cfg: &LoRaConfig, info: &Info) -> Result<(), ErrorCode> {
|
||||||
|
|||||||
Reference in new issue
Block a user