1e982fa78c15bdcb950804e031106ab9b569e09a
305
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1e982fa78c |
Sign plaintext packets in licensed mode (#10969)
* feat: sign licensed plaintext packets Preserve and publish identity keys in licensed mode so existing XEdDSA signatures can authenticate plaintext traffic. Sign licensed broadcasts and direct messages when they fit, while keeping PKI encryption disabled and normal routing behavior unchanged. * fix(security): persist licensed channel sanitation * fix(security): close licensed migration lifecycle gaps * fix(security): address licensed signing review feedback * test: restore signing globals from Unity teardown * fix(baseui): allow confirmed ham region selection * fix(baseui): guard region picker validation * style: trunk fmt --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Austin <vidplace7@gmail.com> |
||
|
|
5c5cb094e6 |
fix(admin): stop the admin dispatcher from overflowing the ESP32 loopTask stack (#11239)
AdminModule::handleReceivedProtobuf carried a 3008-byte stack frame - 37% of the 8 KB Arduino loopTask stack. GCC inlines the eight handleGet* helpers into it, each of which builds a whole meshtastic_AdminMessage (480 B), so the dispatcher reserved a slot for every one of them at once even though only one case ever runs. Two more full AdminMessages lived directly in the function. That left any admin write short of stack for what runs beneath it: NodeDB::saveToDisk -> saveProto -> pb_encode -> SafeFile -> LittleFS -> flash. On heltec-v4 the canary fired at 7824 of 8192 bytes while writing /prefs/config.proto, so no setting ever persisted and the device rebooted. Mark the response builders NOINLINE and move the module-API and observer responses into their own functions. Pure refactor; measured on heltec-v4 the dispatcher frame drops 3008 -> 384 bytes, taking the crashing path from 368 bytes of headroom to 2992. Fixes #11237 |
||
|
|
8e104a909b |
fix(admin): persist TAK module config (team color / member role) (#11216)
Setting TAK team/role ACKed and rebooted but stored nothing: handleSetModuleConfig had no tak case, saveToDisk never set has_tak, and handleGetModuleConfig had no TAK_CONFIG case. Add all three, plus a native suite sweeping every ModuleConfig submessage through set -> save -> load -> get and a TAK value-fidelity suite. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d195ec6748 | fix(security): round-trip packet policy defaults | ||
|
|
e11635baa2 | Merge branch 'develop' into codex/packet-auth-policy | ||
|
|
d587f08481 |
Pin admin responses to the stored peer key and request id (#11092)
* Pin admin responses to the stored peer key and request id noteOutgoingAdminRequest derived its PKC pin from p.public_key, which nothing populates on the outgoing path, so keyValid was false for every client request and the pin never engaged. The accepted-response predicate reduced to an unauthenticated from plus variant and subtype. Resolve the destination key from NodeDB the way perhapsEncode does, and pin it only when the request would actually be PKC-encrypted. Extract that condition from perhapsEncode as wouldEncryptWithPKC so both use one predicate. Also record the request's packet id and require the response to echo it. * Treat a zero request id as no pairing token |
||
|
|
7d954e668d |
Merge branch 'develop' into codex/packet-auth-policy
Resolve conflicts against the NodeDB signer/key primitives (#11050) and the admin-key PKI decrypt budget (#11100). - NodeDB: drop this branch's hasSeenXeddsaSigner in favour of develop's isKnownXeddsaSigner. They answer the same question, but develop's reads the dedicated warm signer bit (warmSignerOf) rather than the WarmProtected category, and TrafficManagementModule already depends on it. Keep develop's copyPublicKey/copyPublicKeyAuthoritative, isVerifiedSignerForKey and commitRemoteKey/KeyCommitTrust. - checkXeddsaReceivePolicy: keep this branch's Strict/Balanced/Compatible policy, which is a superset of develop's balanced-only downgrade gate, and call isKnownXeddsaSigner from it. develop's !pki_encrypted term is dropped because the policy returns early for PKI packets before that check. - perhapsDecode: keep develop's key resolution (NodeDB then pending-key, only for real PKI candidates) plus its admin-key token bucket, and re-apply this branch's pkiAttempted flag feeding the DECODE_OPAQUE verdict. Keep both passesRoutingAuthGate and adminKeyFallbackAllowed/Refund. - test_A17: model eviction the way NodeDB actually does it, passing the warm signer bit as well as the XeddsaSigner category, since isKnownXeddsaSigner reads the former. Native suite: 38 suites, 743/743 cases, no sanitizer findings. |
||
|
|
6f522aad17 |
Return the secret sentinel for remote admin config gets (#11093)
writeSecret is a setter, so calling it on the NETWORK_CONFIG get path was a no-op: the buffer already holds the stored psk, never the sentinel. MQTT_CONFIG returned the broker password verbatim. Both get paths now return secretReserved when req.from != 0, and the matching set paths call writeSecret so a read-modify-write round trip keeps the stored value. |
||
|
|
fb75410e53 |
Null-check packet allocations in allocForSending and its callers (#11086)
* Null-check packet allocations in allocForSending and its callers
Sibling of
|
||
|
|
51019b14f4 |
Hold cryptLock around RNG use in admin and key verification (#11096)
setPassKey drew the session passkey outside cryptLock, unlike the signing and key verification paths. Also replaces random() for the key verification nonce with the hardware RNG. |
||
|
|
08cfb3d683 | Merge upstream develop into packet authentication policy | ||
|
|
5cf346311c |
fix: don't wipe admin keys when regenerating the keypair (#11088)
A client's "regenerate keys" action sends a blank SecurityConfig carrying only the new private key rather than the config it read from the device, so assigning it wholesale cleared admin_key, is_managed, serial_enabled, debug_log_api_enabled, admin_channel_enabled and packet_signature_policy. Losing the admin keys locks the owner out of remote admin with no recourse but a physical connection to the node. Detect that bare-rotation shape and swap in just the keypair, leaving the rest of the security config intact. Deliberately clearing admin keys still works through a SET that leaves the private key alone. Fixes #11073 |
||
|
|
59ab12675a | Merge develop into packet authentication policy | ||
|
|
8c6cc5dbde |
Don't show messages from Ignored nodes (#11068)
* Honor Node Ignore messages * Screenless node fix |
||
|
|
b20d89974a |
Stop accelerometer thread when double-tap/wake-on-motion disabled at runtime (#11025)
* Stop accelerometer thread when double-tap/wake-on-motion disabled at runtime double_tap_as_button_press and wake_on_tap_or_motion are applied live: the OFF->ON edge calls accelerometerThread->start(), but there was no ON->OFF branch, so turning either flag off left the sensor thread running (polling I2C, drawing power) until reboot. Worse, because enabled stayed true, a later OFF->ON edge was a no-op (the enabled==false guard blocked re-start), leaving the feature un-restartable without a reboot. Add the symmetric ON->OFF branch in both handlers. When a flag goes true->off and the other consumer of the shared thread is also off, call accelerometerThread->disable() (stops runOnce polling and clears enabled so a later re-enable can start() again). Each branch checks the other flag first so disabling one feature never stops the sensor while the other still needs it. * Keep accelerometer thread running when its sensor drives the compass * Guard accelerometer thread config toggles against a null thread pointer * AdminModule: factor shared accelerometer start/stop into a helper The device and display config handlers had mirror-image blocks reconciling the shared accelerometer thread. Extract reconcileAccelerometerThread(wasOn, nowOn, otherFeatureOn) so the null guard, edge logic, compass (providesHeading) guard, and rationale live in one place; each call site is now a single call. Behavior is unchanged. Also drops the redundant per-field assignment that the whole-struct `config.device = ...` / `config.display = ...` overwrites anyway. --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> |
||
|
|
d4aa7760cc |
Use hardware RNG for session passkey and PKC extra nonce (#11047)
* Use hardware RNG for session passkey and PKC extra nonce The admin session passkey and the Curve25519 extra nonce were drawn from Arduino random(), which is not a CSPRNG. Source them from HardwareRNG::fill, mirroring the signing path, and fall back to the seeded CSPRNG (CryptRNG) only when no hardware source is available. * AdminModule: make session passkey expiry rollover-safe session_time was compared as millis()/1000 seconds with additive thresholds, which breaks across the millis() wrap and could keep a stale admin session key valid. Store session_time in millis() and use Throttle::isWithinTimespanMs for the 150s refresh and 300s validity windows. * AdminModule: track session passkey validity with an explicit flag session_time == 0 was used as the uninitialized sentinel, but millis() is legitimately 0 in the first millisecond of uptime, so a passkey issued then would be treated as no session. Use a dedicated session_passkey_valid flag instead. * AdminModule: camelCase the session passkey validity flag --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
8147970957 |
AdminModule: only accept admin responses to requests we sent (#11024)
* AdminModule: only accept admin responses to requests we sent An admin *_response short-circuited the auth and session-passkey checks that gate every other admin message, so any node could deliver one. On a channel the module listens to unauthenticated, a get_module_config_response drives the remote-hardware pin handler with attacker-supplied values. Track the destination of outgoing admin requests (per remote, with the pinned PKC key when there is one) and accept a response only from a node with a matching outstanding request, inside the same window as the session passkey. Local (from == 0) admin is unchanged; PhoneAPI already gates it. Also fix the response dispatch: get_module_config_response.which_payload_variant is a ModuleConfig oneof tag, but it was compared against the AdminMessage ModuleConfigType enum (different numbering), so the handler never ran. Compare against the oneof tag. * AdminModule: rollover-safe request window, bind response to request type Two review refinements to the request/response pairing: Use Throttle::isWithinTimespanMs for the outstanding-request expiry instead of comparing millis()/1000 sums, which mis-expired across the millis() rollover. Bind each accepted response to a request type actually sent to that node. Each outstanding record now carries a bitmask of the response variants its requests authorize, so a get_owner request no longer admits a get_module_config response. The mask accumulates per remote, so a client may still pipeline several request types to one node and have every answer accepted. * AdminModule: track admin requests per-request, not per-node Reworks the outstanding-request table so each request is its own entry with its own expiry window and pinned key, replacing the per-node bitmask that shared one timestamp and one key across every response variant. That sharing let a later request to the same node extend an earlier one's window and, worse, clear its PKC pin: an unpinned request cleared keyValid, so a plaintext response to an earlier PKC-pinned request was then accepted. Per-request entries keep each pin intact. Identical requests are de-duplicated (a client may fetch several config subtypes, all answered by one response variant) and eviction compares elapsed time, which is rollover-safe. Test: a pinned request's response still requires its key after an unpinned request to the same node. * AdminModule: match module-config subtype and consume answered requests Two refinements to the request/response pairing: Only remote_hardware get_module_config_response mutates state (the pin table), so it must answer a request for that exact ModuleConfigType, not just any module-config request. Each entry records the requested subtype and the gate checks it. A matched request is now consumed on accept, so a node cannot replay a state-mutating response within the window. Because one request yields one response, request de-dup is dropped (a client's N indexed get_channel requests are N entries, each consumed once). Tests: a non-remote-hardware request does not admit a remote_hardware response, and a second copy of an answered response is rejected. |
||
|
|
b71c1adb26 |
stm32wl: add hardware RTC support (rak3172) (#10961)
* stm32wl: add hardware RTC support infrastructure Wires the STM32WL chip's internal RTC (running off the LSE 32.768kHz crystal) into meshtastic's existing time-of-day framework (perhapsSetRTC()/readFromRTC()), following the same pattern already used for I2C RTC chips (RV3028, PCF8563/85063, RX8130CE). LSE is started and polled manually before ever calling into the STM32RTC library, with our own bounded timeout - the library's own internal LSE startup path has no bounded fallback and hangs forever via Error_Handler() if the crystal never locks, so this is required for a board with a missing/faulty crystal to boot normally rather than hang. Gated behind a new HAS_LSE variant flag (currently unset everywhere, so this is inert until a variant opts in - see follow-up commit). Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * gps: qualify RTC.h includes to avoid case-insensitive filesystem collision with STM32RTC The stm32duino STM32RTC library (added to lib_deps in a follow-up commit) ships its own src/rtc.h. On case-insensitive filesystems (the macOS default), an unqualified #include "RTC.h"/<RTC.h> from any file outside src/gps/ resolves to the library's rtc.h instead of src/gps/RTC.h, since PlatformIO's LDF puts lib_deps include paths ahead of the project's own -Isrc/gps. Qualify every include as gps/RTC.h so it can't collide with any same-named header a future dependency might ship, regardless of filesystem case sensitivity. Purely mechanical, no behavior change. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl(rak3172): enable hardware RTC support Opts rak3172 into the HAS_LSE infrastructure added previously: sets STM32WL_LSE_DRIVE to a conservative default and pulls in the STM32RTC library. rak3172 has ~63KB flash headroom going in; build-verified at 76.7% flash usage after this change (up from a 73.8% baseline), well within budget. wio-e5 is not opted in here despite sharing the same STM32WLE5 chip - it's already at 96.8% flash usage today (GPS + I2C sensor support compiled in, unlike rak3172), leaving too little headroom to safely add STM32RTC without first trimming something else. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl: add docstrings for LSE/RTC setup functions Addresses CodeRabbit's docstring coverage check on PR #10961. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * stm32wl: address CodeRabbit nitpicks on PR #10961 - Brace the single-statement HAS_LSE branch in perhapsSetRTC() to match the sibling readFromRTC() branch's style. - Quote the RTC.h include in PhoneAPI.cpp for consistency with every other qualified include site. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
d3d02af817 |
AdminModule: don't return the device private key to remote config reads (#11030)
A SECURITY_CONFIG get_config response copied config.security verbatim, so a remote request was answered with the device identity private_key and sent over the air. Only the local owner needs it, for backup. Zero private_key in the SECURITY_CONFIG response when the request is remote (from != 0); the local BLE/USB/TCP path (from == 0) still receives it. public_key and admin_key are public and stay as they were. |
||
|
|
5513c36757 |
Add helpful checks of channel names and PSK (#10792)
* added warnings from firmware for simple channel setting mistakes * more and better checks * one ping only * Drop literal 'AQ==' from blank-PSK channel warnings The base64 encoding of the default channel key isn't something a user should type in by hand; state the condition instead of a raw key value. --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
d6b12ea3f1 | feat(security): enforce packet authenticity policies | ||
|
|
515fe8b94f | Clamp bw for malformed use_preset false with empty bandwidth | ||
|
|
6085a4f003 | fix(nrf52): keep BLE active during reset cleanup (#10873) | ||
|
|
fc7043f117 |
second pass adding beacons admin (#10839)
* second pass adding beacons admin * Bump protobufs submodule to merged MESHBEACON_CONFIG (PR #970) clod helped. * Gate MeshBeacon module config behind admin auth in PhoneAPI The FromRadio module-config sync copied moduleConfig.mesh_beacon unconditionally. MeshBeaconConfig embeds two ChannelSettings (broadcast_offer_channel, broadcast_on_channel) that carry PSKs, so an unauthorized client could exfiltrate channel PSKs when MESHTASTIC_PHONEAPI_ACCESS_CONTROL is enabled - bypassing the redaction already applied to mqtt/network/security config. Gate the payload copy on getAdminAuthorized(), mirroring the mqtt case; unauth clients now receive an empty MeshBeaconConfig. clod helped |
||
|
|
3becaf2d95 | emdashes begone (#10847) | ||
|
|
fd62322876 |
Log tidy (#10798)
* clarify channel number formatting and fix the fixed-width padded nodeIDs * use the explicit 0x%08x for packets and nodeIDs throughout * LLM instructions |
||
|
|
0488a46a3c |
fix: stop unexpected NodeNum regeneration from PKI key loss (#10808)
* fix: stop unexpected NodeNum regeneration from PKI key loss A node's NodeNum is derived from its PKI public key (my_node_num == crc32(public_key)), so it changes only when the keypair is regenerated -- which happens only when a keygen path runs while config.security.private_key.size != 32. Two paths could trigger that without the user intending a new identity: 1. Boot: loadFromDisk() collapsed every non-success loadProto() result for the config file into installDefaultConfig() (preserveKey=false), which memset()s config and wipes the private key. loadProto() distinguishes DECODE_FAILED (file present but undecodable/undecryptable) from OTHER_FAILURE (absent), so a transient/corrupt read silently minted a new identity. A new configDecodeFailed flag now gates the boot keygen and the boot config-save directly (not via region, so it survives the userprefs/region overrides that run later in loadFromDisk): identity is frozen, the unreadable file is left intact for a later clean boot to recover, and the node boots radio-silent (region UNSET, TX off). A genuinely-absent config still gets defaults + keygen. 2. AdminModule security SET: config.security = c.payload_variant.security wholesale-clobbered the keypair, then regenerated whenever the incoming private_key.size != 32. A client editing an unrelated security field without round-tripping the private key would re-mint identity. The existing keypair is now preserved when the incoming config omits it; first provisioning and key import are unchanged. Intentional reset goes through factory_reset. Native PlatformIO unit suite (Docker): 499/499 test cases pass. * test: cover security keypair preservation; clarify load-failure comment Addresses PR review feedback: - Add native unit tests (test_admin_radio) asserting handleSetConfig preserves the existing identity keypair when a security SET omits the private key, and applies a full supplied keypair (key import). Guards against reintroduced NodeNum/keypair regeneration. AdminModuleTestShim (now a friend) defers saves so the lightweight harness doesn't saveToDisk an uninitialized node database. - Clarify the non-DECODE_FAILED config-load comment: OTHER_FAILURE / NO_FILESYSTEM cover an absent or unopenable file, not just first boot. |
||
|
|
9cef69a9d3 |
feat(gps): Do not start gps if lora region is unset (#10386)
* Disable gps thread on startup if lora region is unset There is little reason to waste battery on the gps if the data cannot yet be used. * fix goobered merge Refactor GPS enabling logic and remove duplicate code. * trunk --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> |
||
|
|
ec5d230305 |
Feat/mesh beacon (#10618)
* Tips robot virtual node / relayer to different LoRa modes & channels
Note that this commit has details hardcoded for the Wellington (NZ)
mesh, and also requires the following patch to the protobufs:
-----
diff --git a/meshtastic/mesh.proto b/meshtastic/mesh.proto
index 03162d8..ec54c99 100644
--- a/meshtastic/mesh.proto
+++ b/meshtastic/mesh.proto
@@ -1393,6 +1393,21 @@ message MeshPacket {
* Set by the firmware internally, clients are not supposed to set this.
*/
uint32 tx_after = 20;
+
+ /*
+ * The modem preset to use fo rthis packet
+ */
+ uint32 modem_preset = 21;
+
+ /*
+ * The frequency slot to use for this packet
+ */
+ uint32 frequency_slot = 22;
+
+ /*
+ * Whether the packet has a nonstandard radio config
+ */
+ bool nonstandard_radio_config = 23;
}
/*
-----
* fix: repair mesh tips CI build
* feat: add MeshBeacon module (Phase 1 — proto + generated code + initial stub)
* feat(beacon): implement broadcaster + listener (phases 2-5)
* feat(beacon): wire RadioLibInterface hooks + admin validation (phases 6-7)
* fix(beacon): fix LocalModuleConfig flat access (no payload_variant), add localonly proto field
* feat(beacon): fix broadcaster inheritance, add preset/region validation + proto cache
- MeshBeaconBroadcastModule now inherits ProtobufModule<meshtastic_MeshBeacon>
(alongside private MeshBeaconModule + OSThread), giving it allocDataPacket()
and setStartDelay() without extra includes.
- Payload cache: rebuildCache() encodes the MeshBeacon protobuf once and stores
it in payloadCache[]/payloadCacheSize; sendBeacon() only calls rebuildCache()
when payloadCacheDirty==true. AdminModule calls invalidateCache() after saving
new config so the next broadcast picks up changes.
- Region/preset validation in handleSetModuleConfig (mesh_beacon_tag):
broadcast_on_preset is validated against the device's current region via
RadioInterface::validateConfigLora(); broadcast_offer_region is validated via
RadioInterface::validateConfigRegion(). Invalid values are zeroed with a
LOG_WARN before saving.
* feat(beacon): add unit tests for MeshBeaconModule and AdminModule configuration validation
* remove old meshtips
* more validation in NodeDB and AdminModule, and userprefs for baked in goodness
* copilot is my gravity
* mmmmm... beacon
* oops
* Enhance unit tests for MeshBeaconModule with detailed validation checks and output formatting
* new lines. Why not?
* finally
* legacy mode activate!
* Update protobufs (#17)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* better logic, fixed a test
* updated for packet signing
fixed a test
added guards for licensed/ham mode
* channel numbers
* beacon: encrypt on the beacon channel PSK; fix split note
When broadcast_on_channel overrides the primary channel's name/PSK, the
beacon was encrypted with the PRIMARY PSK: perhapsEncode keys encryption
off the primary slot, but the radio-thread channel switch happens only
after encryption. sendBeaconPacket() now installs the beacon channel into
the primary slot for the synchronous duration of send() (cooperative
threading => no interleaving) so encryption/hash use the beacon channel,
then restores it. A shared beaconChannelSettings() helper builds the
channel for both the encrypt-time swap and the RF-time swap so the
key+hash cannot drift.
Also: correct the legacy-split comments (both packets go out on the same
beacon radio settings, not the normal config) and merge the two
consecutive `if (hasText)` blocks in the listener (cppcheck
duplicateCondition).
Tests: add channelPskOverride_swapsBeaconChannelAndRestores and
noChannelOverride_doesNotSwapPrimary; MockRouter snapshots the primary
channel at send() time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test/beacon: drain toPhoneQueue in tearDown to fix LSan leak abort
The listener delivers received text via MeshService::sendToPhone(), which
enqueues the packet into toPhoneQueue and takes ownership. Nothing dequeues
it in tests, so the three listener tests carrying message text stranded a
MeshPacket each — 1272 bytes / 3 allocations that LeakSanitizer flagged at
process exit, aborting the coverage run (surfaced by pio as [ERRORED] /
SIGHUP even though all 40 assertions passed).
Drain the phone queue in tearDown (getForPhone()/releaseToPool) so the
packets return to packetPool. Suite is now GREEN with no sanitizer abort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* legacy hop override for zero-hoppers
* ever more beacons
* beacon: comment out broadcast_send_as_node pending further review
Functionality preserved in comments with full signing/has_bitfield notes
for when it is re-enabled. Proto tag 3 retained on the wire.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test/beacon: fix fromIsCustomNodeWhenSet now that send-as-node is disabled
broadcast_send_as_node is commented out; from is always the local node.
Update the test assertion and doc comment to match current behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Update protobufs (#21)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* flags for beacons
* beacon: do more with less — slot-index targets + validation
Multi-target beacons embedded a full ChannelSettings in every BroadcastTarget,
blowing ModuleConfig past the 512-byte BLE FromRadio budget so the firmware would
not compile. Targets now reference an existing channel-table slot by channel_index
and the broadcaster resolves it via channels.getByIndex() at TX time. Net effect:
the same multi-target capability for a fraction of the bytes —
FromRadio 609 -> 510 B, MeshBeaconConfig 596 -> 324 B, AdminMessage 615 -> 511 B.
- proto: BroadcastTarget.channel (embedded) -> channel_index (uint32 ref); regen all
generated headers (size constants propagate to admin/localonly/deviceonly/mesh).
- broadcaster: resolve channel_index from the channel table; an out-of-range or blank
slot falls back to the default channel for the target preset rather than borrowing
the primary's name/PSK.
- AdminModule: validate broadcast_targets entries on write (region/preset sanitised
like the single-target fields; channel_index range-checked).
- userPrefs: TARGET_<n>_CHANNEL_{NAME,NUM,PSK} collapse to a single CHANNEL_INDEX.
- docs: two-step (set_channel -> set_module_config) multi-target setup, inline-vs-
reference distinction, and single-/multi-target are equal (not "legacy") options.
- tests: target validation + channel-index resolution incl. blank-slot fallback
(47/47 green on `./bin/run-tests.sh -e native -f test_mesh_beacon`).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRAF5csgsMn6p1zEcFL8Qz
* throttling after reboot
* address copilot review
* simplify
* fix(beacon): use 0x%08x for node/packet IDs in logs; register test suite
The %#08lx log specifiers passed uint32_t (NodeNum/PacketId) to a %lx
length modifier — undefined behaviour on 64-bit (native test) targets and
non-standard width. Switch to the project-standard 0x%08x. Also bump
test/native-suite-count to 25 for the added test_mesh_beacon suite.
clod helped too
* copilot & clarity
clod helped too
* refactor(beacon): use auto for the sanitized config copy
clod helped too
* fix(beacon): guard empty-payload sends; gate has_mesh_beacon on build flag; document ISR_TX pre-switch
clod helped too
---------
Co-authored-by: Steve Gilberd <steve@erayd.net>
Co-authored-by: Darafei Praliaskouski <me@komzpa.net>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
98fa4a67db |
[feature] Free a little memory used by bluetooth if wifi is enabled or bluetooth is disabled (#10398)
* esp32: release BTDM heap when Bluetooth inactive Release ESP-IDF BTDM memory after config load when Bluetooth is disabled or WiFi is enabled, recovering heap on ESP32 targets where BLE won’t be used for this boot. * Address BT memory release review comments --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
c51c01607d |
Traffic Management module: dedup, rate limiting, role-aware policing (#10706)
Adds the Traffic Management module (TMM) plus the NodeDB/warm-store and next-hop foundations it builds on: - Unified per-node cache (flat array, 8-bit relative ticks) shared by all features; role-aware throttles for tracker / lost-and-found. - Position deduplication: drop unchanged position rebroadcasts within a configurable interval; precision driven off the channel ceiling (clamped to the public-key max on well-known channels). Enabled by default at 11h. - Per-node rate limiting and unknown-packet filtering (config-driven; a non-zero companion field enables each feature -- no bool toggles). - NodeInfo direct response from cache with role-based hop clamps. - Persistent next-hop overflow store: confirmed hops have no TTL, are seeded from NodeInfoLite at boot, and survive hot-store eviction. - Three-tier sender-role resolution (hot NodeInfoLite -> warm store -> TMM cache). Role is cached write-time (seeded on first track, refreshed from NodeInfo), pins its cache entry like a next-hop hint, and is evicted last. - Warm store caches device role + protected category across reboot/eviction. - PositionModule stationary floor for tracker / lost-and-found. - PSRAM gating for warm/satellite/TMM cache sizes; STM32WL excluded. Protobufs: TrafficManagementConfig trimmed to the five uint32 fields actually used; submodule repointed to protobufs develop. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
79a7dcc46c |
Pr1 nodedb warmstore (#10705)
* NodeDB: 3-tier node store with persistent warm tier (long-tail identity retention)
Introduces a tiered NodeDB so the device retains identity (public key,
last_heard) for far more nodes than fit in the full-record hot store,
without growing heap or the persisted nodes.proto unboundedly.
- Hot store: full NodeInfoLite, MAX_NUM_NODES (120 on nRF52).
- Satellite maps: position/telemetry/environment/status capped at
MAX_SATELLITE_NODES (40 freshest); eviction via enforceSatelliteCaps /
evictSatelliteOverCap.
- Warm tier (WarmNodeStore): 40 B {num,last_heard,public_key} records for
evicted nodes so DMs to/from long-tail nodes keep encrypting/decrypting.
Persisted to /prefs/warm.dat, or on nRF52840 a dedicated 12 KB raw-flash
record-ring below LittleFS (3x4 KB pages; see linker scripts + the
nrf52_warm_region.py post-link guard).
NodeDB::getOrCreateMeshNode now demotes evicted nodes into the warm tier and
re-admits them (restoring key/last_heard). Router PKI decrypt/encode resolve
the peer key via NodeDB::copyPublicKey (hot store, then warm tier).
NodeInfoLite gains snr_q4 (sint32, Q4-encoded dB); the float snr is zeroed on
disk. NodeInfoLite grows 105 -> 112 B; backup 2432 -> 2468 B.
Note: the snr_q4 .proto change still needs to land in the protobufs submodule
(generated header is updated here; submodule pointer left at upstream).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* NodeDB: robust receive + retention for blocked (ignored) nodes
Hardens how ignored/favourite nodes are received over admin and retained,
closing paths where a block could be lost or accidentally cleared.
- Blocking keeps the node's public key (admin set_ignored_node and
addFromContact no longer zero it / drop the warm-tier key), so a blocked
peer stays a verifiable identity.
- set_ignored_node creates the node if absent, so a block by node ID sticks
even for a node we've never heard from (e.g. pushed by a remote admin) with
no NodeInfo or key.
- Eviction protection (favourite/ignored/manually-verified) now also applies to
the load-time hot-store migration and is never undone by cleanupMeshDB, which
previously purged ignored nodes that lacked user info.
- The hot-store migration leaves our own node (index 0) in place and prefers to
demote non-protected nodes, like the runtime eviction scan.
Caps the protected set (favourite + ignored + verified) at MAX_NUM_NODES-2 via
NodeDB::setProtectedFlag(), so at least two evictable slots always remain and
getOrCreateMeshNode can always make room — replacing the previous unconditional
append that could run off the end of the node vector when every node was
protected. A locally-set favourite/ignore that hits the cap reports back to the
phone via a ClientNotification.
Adds test_nodedb_blocked covering the migration, favourite/ignored eviction
protection, ignored-survives-cleanup, and the protected-node cap. The
maintenance methods stay private in production; the test reaches them through a
PIO_UNIT_TESTING-guarded friend shim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
# src/mesh/NodeDB.h
* fix copilot comments
* once again
* WarmNodeStore: fix cppcheck warnings (uninitvar, constVariablePointer)
Zero-initialise `stranded[]` and `seqs[]/order[]` VLAs so cppcheck can
verify there are no unguarded reads of uninitialised memory (the guards
exist but are not visible to static analysis). Mark two local pointers
`const` where the pointed-to entry is never mutated after assignment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* self-care added to assist 2.7 and 2.8 nodedb migration
* Tidy warm-store/self-care: comments, guards, log + flash cleanup
Style/cleanup pass over the branch (no behavior change except the noted
preprocessor simplifications, which are semantically identical):
- Comments: move function descriptions to the headers, cap in-function
comments at ~3-4 lines, drop leading-number step markers, label stacked
#endif blocks, de-decorate banner comments.
- dumpToLog: fully gate decl + definition + AdminModule call site behind
MESHTASTIC_NODEDB_MIGRATION_VERBOSE so it compiles out when disabled
(~1.2 KB when off).
- mesh-pb-constants: drop the dead nRF52832 WARM_NODE_COUNT branch and trim
the macro docs.
- WarmNodeStore: simplify the redundant `ARCH_NRF52 && NRF52840_XXAA` guards
to `NRF52840_XXAA`, add a kNoPage sentinel for the ring page state.
- Shorten the always-on LOG_WARN strings (~120 B flash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* more tidying up, aligning with docs and undoing other-arch regressions
* Update protobufs (#19)
Co-authored-by: NomDeTom <116762865+NomDeTom@users.noreply.github.com>
* made the migration pathway cleareer
* address copilot review
* fixed a copilot review on a downstream PR.
* Address Copilot review comments for PR #10705 (warmstore/nodedb)
- WarmNodeStore.h: default MIGRATION_VERBOSE to 0 (suppress info-level
chatter on production builds; opt in with =1)
- WarmNodeStore.cpp load(): move memset to top of function so all
failure paths (header-read fail, invalid header) leave entries clear
- WarmNodeStore.cpp save(): replace manual spiLock lock/unlock around
mkdir with LockGuard covering the full SafeFile sequence, matching
the lock discipline in load()
- Router.cpp: memcpy(&p->public_key.bytes, ...) -> memcpy(p->public_key.bytes,
...) — pass decayed uint8_t* rather than pointer-to-array
- AdminModule.cpp: check setProtectedFlag return for PKC auto-favorite;
log cap-refusal warning instead of unconditional "auto-favoriting"
- nrf52_warm_region.py: error message references both v6.ld and v7.ld
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* NodeDB: formatting cleanup (blank lines after preprocessor blocks)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Lukewarm store
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
|
||
|
|
d878c81ce8 |
Clamp position precision on public / known-keys (#10665)
* Clamp position precision on public / known-keys (Compliance) * Fix review comments: bounds check, all-channel precision clamp, disabled channel guard * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refactor position precision comments for clarity and update channel configuration handling in AdminModule * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
b938b63e8a |
fix a long-running CI bug that overran a lot (#10707)
* fix the fix * Address Copilot review: add EXIT trap and clarify PKC comment Add `trap` to kill meshtasticd on any early exit (python harness failure, socket timeout) so CI never leaks a background process. Reword the ARCH_PORTDUINO comment to make explicit that pki_encrypted=true causes the from==0 plain-admin branch to be skipped, routing into the PKC key-check — the underlying logic was correct all along. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Update PORTDUINO comment to reflect from==0 auth fix The from==0 branch no longer requires !pki_encrypted (fixed upstream in this branch), so update the simulator comment to reflect the actual remaining reason for the early intercept: is_managed could still block exit_simulator even for local packets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
bbcc35e209 |
Stm32 general (#10700)
* Attempt to generalize ARCH_STM32 * Trunk * One More ARCH_STM32 * Whoops, one snuck in there * Fix comment to reflect define change Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
8267bb22bd |
Packet Signing via XEdDSA (#10478)
* Test commit for XEdDSA support * Update to Crypto lib in Meshtatic org * Generate a new node identity on key generation (#7628) * Generate a new node identity on key generation * Fixes * Fixes * Fixes * Messed up * Fixes * Update src/modules/AdminModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mesh/NodeDB.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Figured it out! * Cleanup * Update src/mesh/NodeDB.h Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/mesh/NodeDB.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/modules/AdminModule.cpp Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update crypto commit hash * Some fixes for xeddsa pr (#9610) * fix: add null check for getMeshNode() in NodeInfoModule getMeshNode() can return nullptr for unknown nodes. Dereferencing without a check crashes the firmware when receiving NodeInfo from a node not yet in the database. * fix: enforce XEdDSA signature verification and prevent stripping Previously, failed signature verification still allowed the packet through, making signatures purely cosmetic. Now: - Failed verification drops the packet (DECODE_FAILURE) - Successfully verified nodes get HAS_XEDDSA_SIGNED bitfield set - Unsigned packets from previously-signing nodes are rejected - Log levels reduced from WARN/ERROR to DEBUG/WARN as appropriate * fix: include packet metadata in XEdDSA signature The signature now covers [fromNode | packetId | portnum | payload] instead of just the payload bytes. This prevents: - Replay attacks (different packetId fails verification) - Reattribution (different fromNode fails verification) - Portnum redirection (different portnum fails verification) Also adds a key initialization check to xeddsa_sign (returns false if XEdDSA keys are all zeros) and checks the return value in the encode path. * fix: handle existing key pair in AdminModule security config When a user provides both a valid private key and public key via admin config, the crypto engine's DH private key and owner public key were never loaded. DMs and XEdDSA signing would silently break. Add an else branch to load both keys into the crypto engine. * perf: cache Ed25519 public key conversion in xeddsa_verify curve_to_ed_pub() performs field element parsing, inversion, and multiplication on every call. Since packets from the same node tend to arrive in bursts, a single-entry cache avoids repeating this expensive conversion for consecutive packets from one sender. * fix: skip identity cleanup when node number is unchanged createNewIdentity() was called on every generateCryptoKeyPair(), including normal boots where the same key is regenerated. This caused unnecessary NodeDB writes and old-node cleanup logic to run when the node number hadn't actually changed. Also fixes only zeroing byte[0] of the old node's public key instead of clearing the entire array. * fix: replace hardcoded 120 with derived XEDDSA_SIGNATURE_SIZE constant The payload size check for XEdDSA signing used a magic number (120). Replace with a derivation from DATA_PAYLOAD_LEN and XEDDSA_SIGNATURE_SIZE so the limit adjusts automatically if constants change. This also increases the max signable payload from 120 to 169 bytes, which is still safe since the actual encoded size is checked after pb_encode. * fix: add const qualifiers to XEdDSA verify and curve_to_ed_pub inputs pubKey, payload, and signature parameters in xeddsa_verify are input-only and should not be modified. Same for curve_pubkey in curve_to_ed_pub. * chore: remove commented-out old Crypto dependency in portduino.ini * Leave out the admin module change for now --------- Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> * trunk * protobuf re-update * Protobufs * Merge resolution fix * Put XEDDSA on the right bit * NodeDB update to new nodeInfoLite accessors, etc * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Refine unsigned packet rejection logic in Router (#10534) * use hardware random to fill the first 32 signature bytes with entropy prior to signing. * Add XEdDSA packet-signing policy tests and update dependencies for macos * Minor fixes * integrate XEdDSA support and update dependencies across multiple modules --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Wessel <github@weebl.me> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> |
||
|
|
07a87a8254 |
security: runtime-toggleable MESHTASTIC_LOCKDOWN hardening for nRF52 (#10349)
* security: add MESHTASTIC_LOCKDOWN hardened build option
Meshtastic nodes ship with secrets on flash (channel PSKs, the device
private key, admin keys, wifi PSK) and over-the-wire access to admin
APIs that can re-key the mesh. Lose the device, at a border crossing,
in a raid, off a backpack, and an attacker reads everything in 30s
with a USB cable. There's no at-rest encryption, no client auth, the
screen leaks contents, and SWD is wide open. This adds an opt-in
hardened build for users who care.
-DMESHTASTIC_LOCKDOWN=1 on nRF52 (CC310) turns on:
DEBUG_MUTE silence USB/serial logs
MESHTASTIC_ENCRYPTED_STORAGE AES-128-CTR + HMAC-SHA256 on
LocalConfig / channels / NodeDB.
Passphrase-gated DEK, TTL/boot
unlock token, failed-attempt
backoff (within-boot, wall-clock,
persisted bootsSinceFail).
MESHTASTIC_PHONEAPI_ACCESS_CONTROL per-connection auth gate. Secrets
emitted as empty proto structs
to unauthenticated clients.
MESHTASTIC_ENABLE_APPROTECT one-way UICR APPROTECT, reset
applied same boot. Recoverable
only via \`nrfjprog --recover\`,
which also wipes the DEK.
LockdownDisplay screen shows "LOCKED" when locked
or idle 30s. OLED only; InkHUD /
niche / device-ui not yet wired.
Wire format is the LockdownAuth / LockdownStatus pair from
meshtastic/protobufs#911 (AdminMessage tag 104, FromRadio tag 18).
Access-control state is a file-scope 6-slot table in PhoneAPI.cpp
keyed by \`this\`, not class members. Adding *any* per-instance field
to PhoneAPI breaks USB-CDC enumeration on the current nRF52 Adafruit
framework, one volatile bool was enough. Out-of-line side-steps it.
lockdown_auth is handled synchronously in PhoneAPI::handleToRadioPacket
rather than routed through the mesh Router into AdminModule. Two
reasons: the passphrase never travels through a routed MeshPacket
queue, and per-connection authorization runs while \`this\` is still on
the call stack. The previous async-via-router design lost connection
identity (g_currentContext was null by the time AdminModule processed
the auth), so per-connection unlock never actually took effect on the
originating client.
Non-nRF52: #warning, only DEBUG_MUTE activates. tools/lockdown_provision.py
drives provision / unlock / lock-now / watch over USB.
Display privacy is a screen-lock latch separate from storage-lock
state: shouldRedactDisplay() is true when storage is locked OR the
latch is set. Screen::setOn(false) sets the latch when the stock idle
timeout powers the display off (reusing config.display.screen_on_secs,
no second timer); it is cleared only when a client authenticates with
the passphrase. A device idling on the mesh keeps routing but hides
its screen until re-auth; button input wakes the backlight to the
LOCKED frame, not content. The earlier lockdown-specific 30s idle
timer is removed — it duplicated PowerFSM idle detection and showed a
misleading LOCKED screen on a merely-idle device.
Unlock-token TTL fix: a token carrying both a boot-count and a
wall-clock TTL is no longer destroyed when the RTC is invalid at cold
boot. The boot count is independently verifiable without a clock, so
the token falls back to boot-count enforcement instead of being
deleted. A token is only hard-rejected when its wall-clock TTL can be
evaluated and is found expired.
NodeDB::reloadFromDisk() after unlock is deferred to the main loop via
lockdownReloadPending rather than run inline on the transport callback
stack — the reload is too heavy for the BLE/serial task stack and was
resetting the device immediately after a successful unlock.
The screen-lock latch also swallows local input events in
InputBroker::handleInputEvent while it (or storage-locked) is set.
Without that, a blind operator could drive on-device menus, fire
canned messages, or change settings through the joystick/buttons even
though the screen content was hidden. PowerFSM is still triggered
first so the backlight wakes to the LOCKED frame; the event is dropped
before reaching the UI observers.
The screen-lock latch is initialised to true at boot, so even a
token-auto-unlocked cold boot comes up redacted. Otherwise an attacker
holding a screen-locked device could power-cycle it (the RAM latch
resets) and recover a content screen. After any boot, the operator
must authenticate from a client to reveal screen content.
MyNodeInfo.device_id is also redacted for unauthenticated clients —
it is a stable hardware identifier useful to an attacker for
fingerprinting / correlating the device across observations. The
public mesh fields (my_node_num, owner short/long name, public key,
hw model) are left as-is because they are already broadcast on-mesh.
ModuleConfig.mqtt is also redacted for unauthenticated clients —
MQTTConfig carries broker username, password, server address, and
root_topic. The empty MQTTConfig is emitted via the same zero-init
pattern as the other gated sections.
Uptime-based session limit (MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS)
caps how long a single auto-unlocked session can hold storage open,
measured in firmware millis() since unlock. 0 = unlimited (existing
token-only behavior, suitable for tower/infra nodes); non-zero arms a
timer on every passphrase unlock and on every token-auto-unlock that
inherits the value, since the cap is persisted in the token (token
format bumped to v2: adds sessionMaxSeconds, body 56→60 bytes).
On expiry the device revokes per-connection auth, re-engages the
screen-lock latch, and reboots WITHOUT deleting the token. Next boot
auto-unlocks via the boot count (decrementing it) and arms a fresh
session window. Hard exposure ceiling: bootsRemaining * sessionMaxSeconds.
Explicit user Lock Now still deletes the token (passphrase required to
recover); only session expiry preserves it.
Why uptime, not wall-clock: getValidTime() is fed by GPS/RTC/client
time pushes — all manipulable by an attacker with the device (GPS
spoof to roll the clock back, pull the RTC backup cell, Faraday-cage
the whole thing). millis() comes off the Cortex-M's internal cycle
counter, sealed inside the chip; the only way to reset it is a reboot,
which costs a boot from the on-flash token counter. APPROTECT remains
the load-bearing defense against forging higher boot counts via SWD.
A future LockdownAuth.max_session_seconds proto field will let the
client set this per-token; until that lands the build-time
MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS macro is the only source.
Session expiry now decrements the on-flash boot count in place and
re-arms the uptime timer WITHOUT rebooting, while budget remains.
Mesh routing keeps running across session boundaries; the device only
reboots when bootsRemaining reaches zero (rollback budget exhausted),
at which point it hard-locks and forces passphrase re-entry.
Each session boundary still: revokes per-connection admin auth so
clients must re-authenticate to see content, re-engages the screen
lock latch, and emits LockdownStatus{LOCKED, needs_auth, boots=N}
so connected clients see the decremented count and know to re-auth.
Storage stays unlocked (DEK in RAM) for continuity.
The boot count's role as the rollback ledger is unchanged — it
decrements monotonically once per session boundary, whether the
session ends in a reboot or an in-place roll. Attacker who power-
cycles to dodge the session timer still pays a boot via the existing
readAndConsumeToken decrement-at-load path. APPROTECT remains the
only defense against forging higher counts.
Net effect for an unattended/tower node with bootsRemaining=50,
sessionSeconds=3600: 50 hours of continuous mesh service, one
reboot at the end, vs. the previous design's 50 reboots over the
same period. Same exposure ceiling, far better uptime.
LockdownAuth.max_session_seconds (proto tag 5) is now consumed: when
non-zero the client value wins; 0 falls back to the firmware-side
MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS, matching the boots_remaining
sentinel convention. Protobufs submodule pin bumped to develop tip
which contains meshtastic/protobufs#916 (merged).
* security: drop dead is_managed allowlist for set_config(security).private_key
The 'isLockdownSecurityCmd' allowlist in handleReceivedProtobuf dates
from the pre-LockdownAuth design when the passphrase was smuggled
through SecurityConfig.private_key. With lockdown_auth handled
synchronously in PhoneAPI::handleToRadioPacket before any admin message
reaches the Router, this allowlist now serves no legitimate purpose
and lets an unauthenticated local client mutate security settings on
a managed device by setting private_key.size>=1 — including
potentially disabling is_managed itself.
Remove the allowlist. Managed-mode local admin now requires a
PhoneAPI connection that has already authenticated via lockdown_auth
(or, on the pki_encrypted branch below, a valid PKC admin key).
Resolves Copilot review feedback on src/modules/AdminModule.cpp:105.
* security: protect lockdown-status drain slot from concurrent writers
g_pendingLockdownStatus / g_hasPendingLockdownStatus are written from
multiple call sites (PhoneAPI::handleLockdownAuthInline on the BLE/USB
transport callback, AdminModule on the Router thread, main loop session
expiry) and read in getFromRadio() on whichever transport is draining
FromRadio. The struct read/write was unprotected, so a writer could
corrupt the slot mid-encode. Same pattern as nodeInfoMutex — wrap
both the queue path and the drain in a small lock. Drain re-checks
the bool under the lock to handle the case where another reader
grabbed the slot first.
Resolves Copilot review feedback on src/mesh/PhoneAPI.cpp:1560.
* security: derive readAndDecrypt size cap from caller buffer, not a hardcoded 64 KB
The MAX_PROTO_FILE_SIZE = 65536 + OVERHEAD ceiling was an absolute
constant chosen against a since-outdated assumption that 'meshtastic
proto files are well under 64 KB'. On variants where MAX_NUM_NODES
pushes the serialised NodeDatabase past 64 KB the legitimate file gets
rejected at load and the device treats its own real config as corrupt.
The caller already knows the maximum plaintext it expects (outBufSize).
Cap the ciphertext at outBufSize + OVERHEAD instead — this is the tightest
sound bound (anything larger could not possibly decode into the caller's
buffer), still defends against OOM / integer overflow, and scales with
the platform's actual NodeDB size rather than an arbitrary constant.
Resolves Copilot review feedback on src/security/EncryptedStorage.cpp:1327.
* docs: fix stale 'passphrase delivery via AdminModule' references in configuration.h
The lockdown overview comment block was written when passphrase delivery
ran through AdminModule's handleReceivedProtobuf. With the synchronous
refactor that path now lives in PhoneAPI::handleLockdownAuthInline,
called before the admin message reaches the Router. Update both the
nRF52 feature list and the non-nRF52 degraded-mode rationale to point
at the current code path.
Resolves Copilot review feedback on src/configuration.h:578 (and :604).
* docs: refresh unlock-token format doc to match v2 layout
The header comment for the UTOK file still described v1 (version 0x01,
no session_max_seconds, 71 bytes) even after the in-flight bump to
TOKEN_VERSION=0x02 and TOKEN_TOTAL_SIZE=75. The inline body-size
breakdown comment was also wrong (claimed 39 bytes and mismatched the
real NONCE_SIZE/AES_KEY_SIZE constants). Rewrite both to match the
actual on-flash layout and note how v1 tokens are handled on upgrade
(rejected via the version byte; passphrase re-entry mints a v2).
Resolves Copilot review feedback on src/security/EncryptedStorage.h:50.
* docs: correct session-limit comment re: token-auto-unlock behavior
The s_sessionMaxMs comment block claimed 'token-auto-unlocked
sessions have no session timer (the session feature is a
passphrase-unlock-only knob)'. Stale: readAndConsumeToken() now
persists sessionMaxSeconds in the token file and re-calls
setSession() from the token-load path, so token-auto-unlocked
sessions DO inherit the same cap (and consumeSessionBoot() re-arms
in place between sessions on a single boot). Update the comment to
match.
Resolves Copilot review feedback on src/security/EncryptedStorage.cpp:72.
* docs: clarify input-swallow gate re: screen-lock latch vs storage state
The previous comment said input is swallowed 'until a client authenticates
and unlockScreen() clears the latch (or storage is unlocked)'. The
parenthetical was misleading: storage being unlocked is not in itself
enough to clear the latch — the latch persists across the
storage-unlocked-but-screen-locked steady state, and only an explicit
unlockScreen() (called from a successful passphrase auth path) clears
it. Reword so the only-passphrase-clears-the-latch invariant is
explicit and local input is named as something that does NOT clear it.
Resolves Copilot review feedback on src/input/InputBroker.cpp:134.
* docs: fix reloadFromDisk() trigger comment in NodeDB.h
The header still claimed reloadFromDisk() is called by AdminModule
after a successful passphrase op. With the synchronous PhoneAPI
refactor the actual trigger is PhoneAPI::handleLockdownAuthInline
setting lockdownReloadPending, with main.cpp's loop() dispatching
the heavy reload on the main thread (the transport callback stack
isn't large enough). Update the comment to point at the real path
and explain why the deferral exists.
Resolves Copilot review feedback on src/mesh/NodeDB.h:393.
* style: clang-format lockdown sources
Apply trunk clang-format (16.0.3) to satisfy the format check.
* style: black-format lockdown_provision.py
Satisfy the trunk black formatter check.
* security: drop unused v1 EncryptedStorage formats and migration
This storage layer has never shipped, so there are no v1 DEK files,
v1 unlock tokens, or v1 backoff records anywhere to stay compatible
with. Remove the dead compatibility machinery:
- legacy init() (FICR-only KEK, no passphrase) — had no callers
- deriveKEKv1() / loadDEKv1() and the v1->v2 DEK migration paths in
provisionPassphrase() and unlockWithPassphrase()
- the 5-byte v1 backoff file format
Also drop the now-pointless version byte from the on-disk MENC, MDEK,
and UTOK formats. Each is identified by its 4-byte magic (and, for the
keyed formats, its HMAC); with only one version that will ever exist,
the version field added nothing. Sizes shrink by one byte each
(overhead 54->53, DEK 66->65, token 75->74).
Rename the surviving helpers to drop the _v2 suffix (deriveKEK,
loadDEK, saveDEK, KEK_DOMAIN). No behavioral change for provisioning,
unlock, token consumption, or session handling.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): harden auth-table and lockdown_auth handler (audit)
Audit findings addressed:
C3 — `~PhoneAPI()` now clears its auth slot unconditionally. The previous
slot-clear in `close()` was gated on `state != STATE_SEND_NOTHING`, so a
PhoneAPI that never reached config (or that already closed) left
`slot.who` pointing at freed memory; a future PhoneAPI heap-allocated at
the same address would inherit the prior session's authorization through
`findOrAllocSlot`.
C4 — All access to `g_authSlots`, `g_authEpoch`, and `g_currentContext` is
now serialised through `g_authSlotsMutex`. Previously these were touched
without locking from BLE/USB/TCP/Router tasks, so two parallel slot scans
could hand out the same slot and mid-update reads could observe
authorized=true alongside a stale epoch. Granularity is fine — every
critical section is a short linear scan over six entries, and getFromRadio
(which calls `getAdminAuthorized()` per redaction check) tolerates the
brief blocking.
A4 / H1 — `lock_now` now requires the originating connection to be
already authorized. Previously any unauthenticated client (BLE/USB/TCP)
could submit `lockdown_auth { lock_now=true }` and force a reboot,
which was a trivial local-presence DoS — an attacker near the radio
could brick-loop it indefinitely. The original "panic button without
auth" property is dropped; panic now requires the operator to have
passphrase-unlocked the connection.
H2 — Empty-passphrase `lockdown_auth` (with `lock_now=false`) used to
silently return success. The client received no feedback distinguishing
that case from a real success, and an attacker could probe lockdown
state for free. Now emits UNLOCK_FAILED with no backoff increment
(empty-passphrase is more likely a client bug than an attack, but the
honest signal still lets the client correct itself).
H14 — `la.boots_remaining > 255` previously truncated silently
(256 → 0 → mapped to TOKEN_DEFAULT_BOOTS=50; 257 → 1). Honest clients
could not detect the misbehavior. Now rejected explicitly with
UNLOCK_FAILED.
L1 — The `to == nodeDB->getNodeNum()` allowance in the unauth ToRadio
gate now also requires `getNodeNum() != 0`. During the locked-default
boot path `getNodeNum()` returns 0, so a packet with `to=0` could
otherwise satisfy the equality and bypass the gate.
L2 — Comment added on `g_authEpoch` wrap. Practically unreachable
(2^32 lockNow events on one boot), but worth recording the behavior.
M17 — `findOrAllocSlot_LH` now evicts the first unauthorized stale slot
when the table is full of non-nullptr entries, rather than failing
closed. Authorized slots are never evicted — they represent live
operator sessions. Fail-closed (with LOG_WARN) only when every slot
holds a different live authorized PhoneAPI, which would require seven
simultaneous authed connections.
M18 — `s_screenLocked` is now `std::atomic<bool>` with relaxed ordering.
Plain bool happened to work on single-core Cortex-M4 today but breaks
silently if lockdown ports to ESP32 / RP2040, or under LTO whole-
program elision.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): gate every admin op on per-connection auth + storage unlock
Audit findings addressed:
H6 — Unauthenticated local clients could previously set_config / set_module_config /
set_channel etc. on a lockdown device whenever is_managed was unset.
The previous gate inside AdminModule's is_managed branch consulted
PhoneAPI::isLocalAdminAuthorized(), which reads a global g_currentContext
set during synchronous PhoneAPI dispatch — but AdminModule runs on the
Router task, by which time the dispatch task has exited and the global is
unrelated to the originating connection. The check was both broken (always
false on Router, so even authed clients were rejected) and unsafe (when it
did fire, the wrong connection could be authorized).
The fix relocates the gate to PhoneAPI::handleToRadioPacket, where dispatch
is synchronous and getAdminAuthorized() can be trusted. The admin payload
is already decoded there to extract lockdown_auth; extend the same branch
so that any non-lockdown_auth admin variant from an unauthorized connection
is dropped before ever reaching the Router queue.
H7 — Same root cause: get_config_request / get_module_config_request /
get_channel_request handlers returned full security/network/mqtt content
to unauthorized local clients. With the H6 gate in PhoneAPI, these
requests never reach AdminModule, so handleGetConfig / handleGetModuleConfig
/ handleGetChannel are only callable from authorized connections.
H9 — Remote admin (PKC-authorized peers, mesh-relayed admin) bypassed
lockdown entirely. If admin_keys were baked in via USERPREFS or set on a
prior unlocked boot, a remote attacker could drive factory_reset /
set_config against a locked device before the operator ever unlocked it.
Added an EncryptedStorage::isUnlocked() early-return at the top of
AdminModule::handleReceivedProtobuf. The local lockdown_auth path is
unaffected because PhoneAPI handles it synchronously before AdminModule
runs.
H10 — Removed g_currentContext, the ContextGuard, authorizeLocalAdmin(),
and isLocalAdminAuthorized() entirely. The audit's race (Router-thread
reads a pointer set by an unrelated parallel dispatch and authorizes the
wrong PhoneAPI) and the always-false-on-Router behavior both disappear
with the code that produced them. The PKC-admin auto-authorize path is
gone — PKC admin and the per-connection lockdown auth are now
independent: clients using PKC admin from a local app must also send
lockdown_auth to unlock the redacted FromRadio stream.
Cleaned up AdminModule's is_managed branch: under lockdown the
PhoneAPI-layer gate has already done its job, so no additional check
is needed; without lockdown the legacy is_managed-blocks-plain-admin
semantics are preserved.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): hold radio silent until storage is unlocked
Audit finding H8: while locked, the device beaconed nodeinfo and
telemetry on the public LongFast default PSK and routed incoming default-
channel packets through the locked router. The locked-default boot path
in NodeDB::loadFromDisk installs config via installDefaultConfig, which
honours USERPREFS_CONFIG_LORA_REGION (the common shape for managed
deployments) and synthesises the default LongFast channel. So a locked
device on managed firmware came up TX-enabled on a well-known PSK
before any operator interaction.
Force config.lora.region = UNSET in the locked-boot block.
RadioLibInterface gates both TX (startSend) and RX (readData) on
region != UNSET — locked devices no longer initialise the SX12xx for
either direction. Also set tx_enabled = false for any code path that
checks the flag directly without consulting region.
reloadFromDisk() restores the persisted lora config once the operator
unlocks. Note: until the audit's M8 (radio re-init after reload, the
upcoming commit 5 in this remediation series) lands, an unlocked
device may need to reboot before its radio fully comes up under the
real config; this is no worse than the pre-fix state, where the radio
was already running on the wrong (default) config and any real config
change required an explicit reconfigure or reboot anyway.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): per-connection status queue, redaction expansion, log/banner mute (audit)
M14 — Replaces the single file-scope LockdownStatus slot with a per-
PhoneAPI table keyed by PhoneAPI*, parallel to the auth-slot table and
sharing g_authSlotsMutex. Previously a status produced for connection
A (UNLOCKED with the active TTL, or UNLOCK_FAILED with a backoff)
could be drained by connection B before A read it, leaking A's auth
state to B. queueLockdownStatus is now a per-instance method writing
to this->slot. A new static broadcastLockdownStatus exists for the
main-loop session-expiry callers that have no PhoneAPI* in hand —
those want every connected client to learn about the session roll,
which is the only legitimate broadcast use case. hasPendingLockdownStatus
is a const helper for the FromRadio available()/drain check.
M13 — buildStatus_LH (the single point where lock_reason crosses into
the on-wire LockdownStatus) collapses any token_* reason to a generic
"locked" before emission. The specific reasons (token_hmac_fail,
token_wrong_size, token_bad_magic, token_boots_zero, token_expired,
token_dek_fail, token_missing) still go to local logs, but no longer
tell an unauthenticated client that the firmware noticed their
tampering / rollback / corrupt-file attempt.
M15 — Extended the STATE_SEND_MY_INFO redaction (previously device_id
only) to also wipe pio_env and min_app_version for unauth clients —
both are pure build-fingerprint vectors that tell an attacker which
known issues to probe. Kept my_node_num (broadcast on the mesh anyway)
and nodedb_count (clients need it post-unlock to decide whether to
pull the node DB). Added equivalent redaction for STATE_SEND_METADATA:
the whole DeviceMetadata struct is wiped for unauth clients
(firmware_version, device_state_version, hw_model, hw_model_string,
has_bluetooth/has_wifi/has_ethernet, role, position_flags,
excluded_modules). Clients re-fetch after authenticating.
M16 — LoRa config is now whitelisted for unauth clients to the set
that is intrinsically observable on the air anyway: region,
modem_preset, use_preset, channel_num, hop_limit. Operator-private
knobs (ignore_incoming, override_duty_cycle, override_frequency,
sx126x_rx_boosted_gain, tx_power, ignore_mqtt, fem_lna_mode,
config_ok_to_mqtt) are zeroed. The whitelist is built as a fresh
LoRaConfig stack copy rather than masked in place to avoid touching
the persisted struct.
M12 — Skip the DEBUG_MUTE "we are muted, FYI" banner under
MESHTASTIC_LOCKDOWN. The banner spilled APP_VERSION / APP_ENV /
APP_REPO over USB CDC even with all other logging suppressed, which
defeats the muting in lockdown builds and gives a USB-attached
attacker a free firmware-fingerprint primitive.
L9 — Removed the numeric backoff value from the LOG_WARN unlock-
failed message. The client receives backoff_seconds via the
UNLOCK_FAILED status; printing it again to USB serial under
non-DEBUG_MUTE builds (i.e. MESHTASTIC_LOCKDOWN_DEBUG dev builds)
was the only place it appeared in logs.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): atomic post-unlock reload with corruption surface (audit)
Closes M6, M7, M8, M9 from the lockdown security audit.
M6 — handleLockdownAuthInline no longer flips the connection to
authorized or emits UNLOCKED on the cold-unlock path (the first
successful passphrase verify after a locked boot). The client keeps
seeing LOCKED until reloadFromDisk has actually populated config /
channelFile / nodeDatabase with the operator's real values. Without
this, the window between the auth call and the main-loop reload
exposed two race-friendly bugs: (a) the client could read the
locked-default placeholders as if they were the real config, and (b)
a set_config in the window would silently overwrite a corrupted
baseline once the reload swapped values in.
A new per-status-slot bool pendingUnlockAfterReload records that the
connection is mid-unlock. The re-verify path (storage already
unlocked) is unchanged and authorizes immediately — there is nothing
to reload.
M7 — reloadFromDisk now holds a new file-scope mutex
(g_reloadFromDiskMutex) against itself, parks the radio in sleep
mode before swapping config / channelFile, and reconfigures the
radio with the now-real settings after. Other readers of config.lora
/ channelFile / nodeDatabase do not take this lock today; closing
those races is a wider locking-discipline change outside the audit's
M7 scope. The radio standby+reconfigure prevents the SX12xx from
sitting in a half-old/half-new register set across the swap, which
otherwise required a reboot to recover from.
M8 — RadioInterface::reconfigure() is now called at the end of a
successful reload, so the SX12xx register set actually reflects
the unlocked operator settings (region, modem preset, channels)
rather than staying on the locked-default placeholder. Routed through
a new Router::getRadioIface() accessor — the radio interface is
owned by Router as a unique_ptr and was not exposed.
M9 — NodeDB::loadProto now sets a NodeDB::storageCorruptThisLoad
flag whenever an encrypted file fails to decrypt or proto-decode.
reloadFromDisk consumes the flag and returns false on any failure
instead of silently falling back to defaults. main.cpp's reload
service then calls EncryptedStorage::lockNow() and
PhoneAPI::revokeAllAuth(), and the new
PhoneAPI::completePendingUnlocks(false) emits LOCKED(storage_corrupt)
to every pending connection — they stay unauthorized so any
set_config they send is dropped at the existing unauth gates.
The lock_reason string passes through buildStatus_LH's M13
redaction unchanged because it does not start with token_.
The success path goes through PhoneAPI::completePendingUnlocks(true)
which authorizes each pending connection, emits UNLOCKED with the
current TTL, and clears the screen-lock latch once. Snapshots the
target PhoneAPI* list outside the auth-table lock to avoid re-entry
when setAdminAuthorized takes the same lock.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): UI/pairing fixes for first-pair + content-flash + e-ink (audit)
Closes H13, M19, M20, L4 from the lockdown audit. (L3 dropped per
explicit decision — battery level is not a meaningful security side
channel.)
H13 — BLE pairing PIN was suppressed by the lockdown lock screen on
locked devices. Screen.cpp updateUiFrame's lockdown short-circuit
intercepts before ui->update() runs, so the pairing-PIN overlay
banner that NRF52Bluetooth::onPairingPasskey queued never painted.
Net effect: a freshly-locked device on first BLE pair could not be
unlocked over BLE because the operator could never see the PIN —
chicken and egg.
Adds a new notificationTypeEnum::pairing_pin value and special-cases
it in the short-circuit: paint the LOCKED frame first (so the
underlying background remains the redacted view, never dashboard
content) then let ui->update() composite the PIN banner overlay on
top. The PIN itself is an ephemeral pair-handshake artifact
(regenerated per attempt, dies on banner timeout) and is not
operator content, so this does not regress the redaction guarantee.
NRF52Bluetooth::onPairingPasskey switches from showSimpleBanner to
showOverlayBanner with notificationType = pairing_pin so the
short-circuit's lookup matches.
M19 — Brief content-visible window on Screen::handleSetOn(true)
wake. OLED GDDRAM physically retains the last-rendered frame while
the panel is powered off; the next ui->update() after displayOn() is
async, so an observer (or shoulder-surfer) could see the previous
frame's content for 16-50 ms on every wake. Under MESHTASTIC_LOCKDOWN
we now paint the LOCKED frame into GDDRAM in handleSetOn(false)
before calling displayOff(). On wake the only thing the panel can
flash is the redacted view. Gated on lockdown only — non-lockdown
builds keep the previous frame as a UX cue.
M20 — E-ink panels physically retain the last-rendered image
without power. A power-cycled lockdown handheld kept showing
operator-identifying content (position, messages, nodeinfo) until
the firmware's first natural refresh — which on e-ink can be
seconds into boot. Now, under MESHTASTIC_LOCKDOWN && USE_EINK, the
panel init path in Screen::setup() paints the LOCKED frame and
forces a full refresh (forceDisplay) immediately after ui->init()
and before any other rendering. Persistent pixels are wiped to the
redacted view before an observer can see them. Build-tested on
seeed_wio_tracker_L1_eink; hardware-verified visual confirmation
is pending a T-Echo session.
L4 — Screen::blink() bypasses the normal ui->update() path that
the lockdown short-circuit gates. It draws arbitrary geometry, not
node data, so it does not actually leak today; but any future
change that puts content into blink would silently leak past
redaction. Added an early-return on shouldRedactDisplay() to make
the function honor the redaction contract.
Verified with nRF52 lockdown builds on both rak4631 (OLED) and
seeed_wio_tracker_L1_eink (e-ink).
* fix(lockdown): refuse APPROTECT on vulnerable silicon, gate on provision (audit)
Closes M22 and M23 from the lockdown audit.
M22 — APPROTECT lockout on nRF52840 is publicly known to be bypassable
on every silicon revision shipping in current Meshtastic hardware
(AAB0..AAF0) via SWD glitching, per LimitedResults' published research
on the nRF52 series. Engaging APPROTECT on these revisions has two
bad properties: (1) the lockout is irreversible without a destructive
nrfjprog --recover, and (2) it gives the operator a false sense of
security because the lockout itself can be defeated by anyone with
ten minutes and a glitcher.
enableAPProtect() now reads FICR.INFO.VARIANT (encoded as a 4-byte
ASCII word) and refuses to engage on any known-vulnerable revision,
logging the variant so the operator knows their device's specific
build code. To override (e.g. for end-to-end testing of the engage
path on hardware that's known affected), rebuild with
-DMESHTASTIC_APPROTECT_OVERRIDE_VULNERABLE_SILICON=1.
The vulnerable list is explicit and easy to update: any future
revision shown to be fixed can be removed from the list and APPROTECT
will engage on it as before.
M23 — APPROTECT engagement moved from very early in setup() to
after fsInit() + EncryptedStorage::initLocked(), and gated on
EncryptedStorage::isProvisioned(). A misconfigured CI build of a
lockdown variant flashed to a dev board would otherwise burn SWD on
first boot before the operator had set any passphrase, taking the
board out of the development/recovery workflow with zero real
security benefit (there is no DEK to protect on an unprovisioned
device). Engagement now follows operator intent: SWD locks only
once they've committed to lockdown via passphrase provisioning.
The SWD-attachable window between boot and APPROTECT engagement
widens slightly from this reorder (now ~hundreds of ms while fsInit
runs) but APPROTECT remains effective on the only payload it could
protect (the in-RAM DEK loaded by initLocked which now runs *after*
APPROTECT for already-provisioned devices).
Verified with an nRF52 lockdown build (rak4631).
* tools: harden lockdown_provision.py (audit)
Closes M26-M30 and addresses L7.
M26 — passphrase input. --passphrase on argv now requires
--insecure-passphrase-on-cmdline as an explicit acknowledgement;
without it the tool refuses and points at --passphrase-file or the
interactive prompt. --passphrase-file refuses to read anything that
isn't mode 0600 (so a passphrase another user can read off the
filesystem doesn't silently succeed). With neither, the tool reads
the passphrase via getpass.getpass — and on 'provision' double-prompts
with a confirm.
M27 — provision now requires an explicit 'yes' confirmation unless
--yes is passed, after printing the warning that the passphrase
cannot be recovered. The double-passphrase prompt is built into
gather_passphrase(confirm=True). Reduces the chance of a typo
binding a device to an unrecoverable passphrase.
M28 — 'lock' subcommand gains a 'lock-now' alias, matching how the
audit and wire docs refer to it everywhere. Both forms now require
'yes' confirmation unless --yes is set, so an accidental command
doesn't immediately reboot the device into a locked state.
M29 — the 4-second sleep is gone. Replaced with a StatusFuture
single-shot that the FromRadio interceptor signals when the next
LockdownStatus arrives. provision/unlock/lock wait up to --wait
seconds (default 8) for the actual reply and exit non-zero with the
device's reason on UNLOCK_FAILED, surfacing backoff_seconds in the
error line. Exit codes are now meaningful:
0 = UNLOCKED
1 = no status / unexpected
2 = NEEDS_PROVISION (or a precondition fault: missing pkg, bad args)
3 = LOCKED (ambiguous: device reported locked rather than the
expected unlocked result)
4 = UNLOCK_FAILED
This lets ops scripts decide what to do without parsing stdout.
M30 — top-of-file docstring gained an explicit SECURITY MODEL block
that names the threat model (USB-only, passphrase cleartext on the
cable) and forbids extension to TCP/BLE/UDP without a redesign. A
runtime banner reprints the headline on every invocation. --port
values starting with tcp:/tcp://, ble:/ble://, udp:/udp://, ws:/wss:
are rejected at argument parse before any connection attempt; a
copy-paste of an example into a context with a different --port
cannot silently leak credentials to the wire.
L7 — private meshtastic APIs (_handleFromRadio, _sendToRadio,
_generatePacketId) are still in use because the lib does not yet
dispatch LockdownStatus on a public pubsub topic and there is no
public seam for raw ToRadio. Their use is now wrapped in
getattr-with-clear-error so a future lib version that removes them
produces an actionable error instead of an obscure traceback. The
top-of-file note explains why we're on the private surface.
Verified end-to-end on hardware (R1-Neo + Seeed Wio Tracker L1)
during the audit-remediation hardware test pass:
- provision (interactive, with confirm and double-prompt)
- unlock (success returns UNLOCKED + boots TTL)
- watch (passive listener emits LockdownStatus events)
- lock-now (with --yes)
* fix(lockdown): H13 — render pairing PIN steady over LOCKED frame
Two bugs in the H13 fix from commit 614b7f001:
1. NotificationRenderer::drawBannercallback's switch had no case for
the new notificationTypeEnum::pairing_pin. The function fell through
to no-op so the banner never rendered. Added pairing_pin alongside
text_banner so it dispatches to drawAlertBannerOverlay (same
rendering, distinct type so the lockdown short-circuit in Screen.cpp
can recognise it).
2. updateUiFrame's lockdown short-circuit called ui->update() to
composite the banner. That redraws the current carousel frame
(the dashboard) into the host framebuffer BEFORE the overlay
paints, so the panel flashed dashboard content under the banner
on every cycle. Replaced with a direct call to drawBannercallback
so only the banner box is painted on top of the LOCKED pixels.
Also: drawLockdownLockScreen used to commit to the panel
(display->display()) at its end. With the banner overlay then
painting and committing a second time, the panel visibly flickered
between 'just LOCKED' and 'LOCKED + banner' on every render cycle.
Split into drawLockdownLockScreenIntoBuffer (no commit) for the
lockdown short-circuit, and a thin drawLockdownLockScreen wrapper
that calls Buffer + display() for the other call sites that don't
composite anything on top. The short-circuit now commits exactly
once per frame after both LOCKED + any overlay are in the buffer.
Verified end-to-end on hardware (Seeed Wio Tracker L1, OLED):
fresh BLE pair against a locked device now shows the pairing PIN
steadily on top of the LOCKED frame, no flicker, no dashboard
leak, and pair completes normally.
* fix(lockdown): backoff MAC + atomic writes + fault wipe + size cap (audit)
Closes H3, H4, H12, M10, M11, M25 from the lockdown audit. Non-format-
breaking: existing devices keep their .dek and .unlock_token but their
old plaintext .backoff file (6 bytes, no MAC) is silently rejected as
tampered on first read and reseeded with the MAC'd 38-byte format on
the next failed-attempt OR successful unlock.
H3 — Pre-increment the failed-attempt counter BEFORE running the HMAC
verify in unlockWithPassphrase. The previous order wrote the counter
only after a failed verify, so an attacker glitching the chip between
verify and write could skip the increment and bypass backoff. The
slot is now reserved atomically up front; the success path writes
attempts=0 to clear the reservation. Worst case for a legitimate user
who power-cycles mid-success is one phantom attempt — backoff
recovers next try.
H4 — .backoff file is now MAC'd with HMAC-SHA256(ephemeralKEK,
"backoff-auth" || body) (32-byte tag), and written atomically via
SafeFile (tmp + readback verify + rename). readBackoff treats
missing / wrong-size / MAC-fail uniformly as max-attempts (255) so an
attacker who deletes or rewrites the file can only INCREASE the wait,
never decrease it. clearBackoff() now writes an attempts=0 sentinel
instead of removing the file, so 'missing == tamper' is unambiguous
post-provision. bumpBootsSinceFailOnBoot() skips on un-provisioned
devices to avoid false 'tamper' detection during the legitimate fresh
window between fsInit and provisionPassphrase.
H12 — saveDEK and writeUnlockToken now write via SafeFile in
fullAtomic mode (tmp file + readback verify + atomic rename) instead
of remove-then-open-then-write. Power loss during a DEK or token
write previously left the device unable to unlock — the encrypted
prefs files are unreadable without a valid DEK. The atomic path
rolls back to the previous file on partial write.
M10 — readAndConsumeToken's 74-byte stack buffer (entire wrapped
DEK + HMAC, explicitly called out by the audit as never wiped before
return) is now a meshtastic_security::ZeroizingBuffer that the
destructor scrubs on every return path. Same treatment for the
computedHmac stack array next to it, and for the new backoff state
buffers in readBackoff / writeBackoff / computeBackoffHmac. Removes
the manual secure_zero calls those buffers had on success paths and
fixes the missing wipes on the failure-return paths.
M11 — Added EncryptedStorage::secureWipeKeys() public API that
zeros dek/kek/ephemeralKek in BSS without touching flash, no
logging, no locks (safe from interrupt context). HardFault_Impl now
calls it as the very first thing on entry, before the diagnostic
print / coredump path runs, so a hard-fault crash dump won't capture
the DEK / KEK material that the rest of the module leaves in RAM.
M25 — migrateFile now refuses to allocate a buffer for any file
larger than 64 KiB. The legitimate ceiling is well under that on
every supported variant; anything larger is either corrupt or a
DFU-injected OOM attempt.
Verified with an nRF52 lockdown build (rak4631).
* fix(lockdown): MENC header MAC + token rollback counter (audit)
Closes M2 and M4 from the lockdown audit. **FORMAT-BREAKING** — devices
provisioned with prior lockdown firmware must factory-erase /prefs and
reprovision; the previous tokens and encrypted prefs files will not
decrypt under the new HMAC/body layouts.
M2 — The HMAC on MENC encrypted proto files now covers the full on-disk
header (4-byte magic + 13-byte nonce + 4-byte plaintext_len + ciphertext)
instead of just (nonce + ciphertext). Without this, magic and
plaintext_len were integrity-protected only by the equality check
`plaintextLen == ciphertextLen` — which holds today (no padding /
compression / AAD) but would silently produce length-oracle and
downgrade vulnerabilities the instant any of those got added. Putting
the header inside the MAC closes that pre-condition cleanly. The
verify side in readAndDecrypt and the compose side in encryptAndWrite
update in lockstep.
M4 — UTOK gains a 4-byte monotonic counter field inside its MAC'd
body. The highest counter ever issued is persisted to a new
/prefs/.tokmono file MAC'd with HMAC-SHA256(ephemeralKEK,
"tokmono-auth" || counter). On every readAndConsumeToken, any
token whose counter is less than the persisted value is rejected as
a rollback attempt and deleted. Defeats the audit's threat: an
attacker who once captured a token (e.g. bootsRemaining=255 from
before the operator lowered the policy) tries to write it back to
disk later. Counter is incremented monotonically across the device's
lifetime so any captured snapshot loses to the persisted max-seen.
Self-heal: a token whose counter exceeds the persisted value (e.g.
the .tokmono write itself failed after the token committed, or the
.tokmono got wiped via factory-erase) is accepted AND the counter
file is promoted to match. This avoids spuriously rejecting valid
tokens after partial-update recovery.
Threat model caveat (consistent with C2 acceptance): an attacker who
has both flash extraction AND FICR can recompute the .tokmono MAC
and restore a matching pair (.unlock_token + .tokmono) from an
earlier capture. M4 raises the bar to that combined capability;
the flash-write-only attacker is now blocked.
Verified with an nRF52 lockdown build (rak4631).
MIGRATION: devices already provisioned with the prior lockdown
firmware will fail to auto-unlock at boot (token format mismatch),
fall back to LOCKED(needs_auth), and every passphrase attempt will
fail because the encrypted /prefs files are HMAC'd against the old
input. Recovery is: factory-erase via the bootloader UF2 then
re-provision via lockdown_provision.py or the Android app.
* feat(lockdown): make lockdown a runtime client-toggleable setting
Converts MESHTASTIC_LOCKDOWN from a per-variant compile-time flag that
forced lockdown ON into an internal capability that is ALWAYS compiled
in for nRF52 and gated purely at runtime by whether a passphrase has
been provisioned. A device that has never been provisioned (or that the
operator disabled) behaves exactly like stock firmware.
Build/config:
- configuration.h auto-defines MESHTASTIC_LOCKDOWN (+ ACCESS_CONTROL,
ENCRYPTED_STORAGE, APPROTECT-capable) for ARCH_NRF52 unconditionally.
No variant sets -DMESHTASTIC_LOCKDOWN anymore. Flash-constrained
variants can opt out with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1. DEBUG_MUTE
is no longer coupled to lockdown (a capable-but-off device must log
normally). rak4631 lands at 96.2% flash with lockdown always-in.
Runtime predicate:
- EncryptedStorage::isLockdownActive() == isProvisioned() (.dek exists)
is the single source of truth for active/inactive.
- PhoneAPI::getAdminAuthorized() returns true when lockdown is inactive,
so every existing redaction gate no-ops on a capable-but-off device
with no per-site changes. The locked-boot defaults path (NodeDB), the
AdminModule storage-locked gate, the screen-redaction predicate, and
the plaintext->encrypted migrate block are all additionally gated on
isLockdownActive() so an un-provisioned device loads/serves plaintext
normally.
- sendConfigComplete emits LockdownStatus{DISABLED} when capable-but-off
so the client renders its toggle OFF.
Enable (off->on): client provisions a passphrase. provisionPassphrase
generates the DEK; the existing reload path encrypts the plaintext
config in place (migration runs live with the DEK in RAM) and authorizes
the connection -> UNLOCKED. No reboot.
Disable (on->off): LockdownAuth{passphrase, disable=true}. PhoneAPI
verifies the passphrase (loads DEK), sets lockdownDisablePending; the
main loop runs NodeDB::disableLockdownToPlaintext() which decrypts every
pref via EncryptedStorage::migrateFileToPlaintext() then
removeLockdownArtifacts() deletes the DEK/token/counter/backoff (the
.dek delete is the atomic commit), then reboots into normal mode.
Power-loss safe and re-runnable without a persistent marker — and the
crypto runs live with the operator's passphrase in RAM rather than via
a boot-time marker an attacker could plant to trigger an unprompted
decrypt. APPROTECT is NOT reversed (sticky; permanent on silicon where
it engaged).
Generated bindings (admin.pb.h / mesh.pb.h) regenerated against
protobufs#927 (LockdownAuth.disable, LockdownStatus.State.DISABLED).
Submodule pointer stays at the pinned develop commit; the bindings are
ahead until #927 merges and the submodule is bumped, same flow as the
max_session_seconds work.
Builds clean: rak4631 with no flags now auto-includes lockdown.
NOTE: this changes the LockdownStatus the firmware emits and adds the
disable path; pairs with protobufs#927 and the upcoming Android client
toggle work.
* fix(lockdown): re-lock per-connection auth on BLE reconnect
A provisioned device reused a single BLE PhoneAPI instance, and the
per-connection auth slot (keyed by that instance) was only cleared on the
!isConnected() disconnect transition. A fast disconnect/reconnect could
begin a new config burst while state was still STATE_SEND_PACKETS, so the
reconnected client inherited the prior session's authorization: it received
SecurityConfig in the clear and no LockdownStatus, and never re-authenticated.
Reset the auth slot in NRF52Bluetooth onConnect(), which fires once per
physical link, so every new connection starts locked regardless of whether
the previous link's close() raced the new handshake. handleStartConfig keeps
its !isConnected() reset (do NOT reset on a same-connection want_config: the
post-unlock re-fetch is the client pulling now-unredacted config and must keep
the auth it just earned, otherwise config comes back redacted and set_config
writes get dropped).
* fix(lockdown): persist config on a lockdown-capable but disabled device
saveProto always called encryptAndWrite when encrypted storage was compiled,
and saveToDiskNoRetry skipped every save when !isUnlocked(). On a disabled
(never provisioned) device there is no DEK and isUnlocked() is always false,
so both paths fired and NO config ever persisted: a LoRa region set before
enabling lockdown lived only in RAM, then provisioning migrated the UNSET
default from disk and the region was lost.
Gate both on isLockdownActive(): when lockdown is inactive the device writes
plaintext exactly like stock firmware; the reloadFromDisk migrate pass then
re-saves those plaintext files encrypted once the device is provisioned.
Verified on hardware: region set while disabled now survives enable, reboot,
and unlock.
* fix(lockdown): suppress LoRa region picker under the lock screen
A locked-boot lockdown device installs region=UNSET as a deliberate RAM
placeholder (the real region is in encrypted storage, restored on unlock).
Screen.cpp popped the region picker / onboard message whenever region==UNSET,
so it rendered over the lock screen and trapped input with no way out. Skip it
while the display is being redacted for lockdown.
* fix(lockdown): silence cppcheck void* false positive + ruff docstring lints
The nRF52 `check` (cppcheck --fail-on-defect=low) flagged
arithOperationsOnVoidPointer on EncryptedStorage.cpp buffers. These are
false positives: make_zeroizing_array() returns unique_ptr<uint8_t[], ...>
so .get() is uint8_t*, not void* — cppcheck just can't resolve the
custom-deleter alias. File-scoped suppression, matching the existing
crypto-code convention in suppressions.txt.
Trunk flagged 5 ruff docstring issues in lockdown_provision.py: D301
(backslashes need a raw docstring) and D405/D407/D411/D413 (the EXAMPLES
heading was being parsed as a numpydoc section). Made the docstring raw
and renamed the heading to USAGE to dodge section detection while keeping
the ASCII-box formatting.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(lockdown): resolve cppcheck const/null-deref defects
The nRF52 `check` job (pio check --fail-on-defect=low) flagged seven
real cppcheck defects in the lockdown code:
- EncryptedStorage.cpp: nonce/encDek are read-only views into the
token buffer -> const uint8_t *.
- NodeDB.cpp: segments[] lookup table is never mutated -> const.
- PhoneAPI.cpp: clearStatusSlot_LH's p is only compared; the auth-check
slot and the hasPendingLockdownStatus loop var are read-only -> const.
- Screen.cpp: the MESHTASTIC_LOCKDOWN drawLockdownLockScreen() guard
introduced a redundant null check (nullPointerRedundantCheck) since
dispdev->displayOff() right below derefs it unguarded, as does the
rest of the file. Dropped the guard.
Verified with cppcheck 2.21 locally against the project suppressions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(lockdown): const-qualify clearAuthSlot_LH param (cppcheck cascade)
Making clearStatusSlot_LH take const PhoneAPI* let cppcheck propagate the
same to clearAuthSlot_LH, whose p is only compared and forwarded. The
remaining PhoneAPI* params (findOrAlloc*Slot_LH) store p into the slot
table, so they correctly stay non-const.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(lockdown): wire runtime-toggle disable flow into provision tool
Addresses Copilot review on tools/lockdown_provision.py — the reference
tool advertised the runtime-toggle disable lifecycle but couldn't exercise
it:
- _STATE_NAMES: map LockdownStatus.DISABLED so a capable-but-off boot
prints DISABLED instead of an opaque state=<num>.
- build_lockdown_auth(): add a disable param that actually sets
la.disable, failing loudly on pre-runtime-toggle bindings instead of
silently sending a plain unlock.
- cmd_disable() + 'disable' subcommand: send LockdownAuth{disable=true,
passphrase=...} and wait for the resulting LockdownStatus. Mirrors the
firmware: non-empty passphrase required, DISABLED broadcast precedes
the reboot, TTL/session fields ignored.
- _exit_code_for_status(): treat DISABLED as a success (exit 0) like
UNLOCKED.
All DISABLED/disable references are hasattr-guarded so the tool still
imports and runs the lock/unlock/provision paths against the currently
released meshtastic package (verified: it has LockdownAuth but not yet
disable/DISABLED). Verified with ruff 0.15.13 and black.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ed52e3019d | Change handleSetOwner parameter to const reference and improve long name handling | ||
|
|
c2bcec93d0 | Fix long name clamping and adjust related structures for compatibility | ||
|
|
ab882c5619 |
EU regions merge (#10675)
* stronger together * validate 2.4ghz regions * less noise * you're right, and that shapens the analysis significantly * sassy rejoinder |
||
|
|
e028663658 |
BaseUI: First attempt at Ham Mode implementation (#10663)
* First attempt at Ham Mode implementation * Simplify licensedOnly check * Move related code closer together * TX Disabled if N0CALL, enabled if properly set * Only disable if callsign is N0CALL, don't enable at this stage. * Allow users back to Normal mode if they don't pick an ITU region |
||
|
|
c7748a1602 | Fix update neighbor_info before checking update_interval in handleSetModuleConfig | ||
|
|
5e69bc6c3f |
Enable Narrow and Lite regions for EU (#10120)
* Enable Lite and Narrow regions and introduce getEffectiveDutyCycle for Lite profiles * Add TrafficType enum and extend getConfiguredOrDefaultMsScaled to manage based on regionProfile settings * Refactor telemetry modules to include TrafficType in getConfiguredOrDefaultMsScaled calls * Update submodule protobufs to latest commit * Add support for new region presets and modem presets in menu options * Add new LoRa region codes and modem presets for EU bands * boof * Add modem presets for LITE and NARROW configurations * Update subproject commit reference in protobufs * Update protobufs * Refactor modem preset definitions to use macro for consistency and clarity * Refactor modem preset cases to use PRESET macro for consistency * fix: update LoRa region code for EU 868 narrowband configuration Co-authored-by: Copilot <copilot@github.com> * Fix test suite failure Co-authored-by: Copilot <copilot@github.com> * Add override slot override - for when one override isn't enough. Co-authored-by: Copilot <copilot@github.com> * address copilot comments --------- Co-authored-by: Copilot <copilot@github.com> |
||
|
|
a541957480 |
ESP32: Migrate to Arduino 3.x (pioarduino) (#9122)
* Migrate esp32 families to pioarduino platform * ESP32c6 align text.handler_execute same as C3 * Use pioarduino `develop` The latest fixes and the latest bugs! * preliminary esp32p4.ini * pioarduino: Update LovyanGFX Includes Manuel's recent commit * pioarduino 3.3.6 * pioarduino 3.3.6 *release* chasing the release * pioarduino: Fix OG ESP32 duplicate libs * pioarduino: T-Beam 1W CDC mode * pioarduino: disable network provisioning (wifiprov) * pioarduino: use legacy esptoolpy naming (forward-compatible) * Update lovyangfx from `develop` commit to 1.2.19 * fix esp32p4.ini * check for esp32 w/ wifi * esp32-p4 specific adaptations * Switch to meshtastic/esp32_https_server fork (idf5 branch) * don't ignore esp_lcd * config for MUI * fix/workaround SDMMC * revert a6f6175, update to 3.3.8 * enable esp_hosted for esp32-p4 (experimental) * Pioarduino 55.03.38-1 * NimBLE-Arduino -> Arduino "BLE" (3.3.x) migration (#10164) * NimBLE-Arduino -> Arduino "BLE" (3.3.x) migration * More NimBLE * Fix Device Name in ATT Read Request (0x2A00). Device Name is exposed in two places: - Advertisement data: this is set properly in startAdvertising. - GATT attribute Device Name (0x2A00). This one is handled internally in NimBLE and comes from ble_svc_gap_device_name_set. This is set initially, but then BLEDevice::createServer calls ble_svc_gap_init which resets the device name. This causes the device to apparently "change name after pairing": < ACL Data TX:... flags 0x00 dlen 7 #113 [hci0] 14.241149 ATT: Read Request (0x0a) len 2 Handle: 0x0003 Type: Device Name (0x2a00) > ACL Data RX: Handle 2048 flags 0x02 dlen 11 #115 [hci0] 14.269050 ATT: Read Response (0x0b) len 6 Value[6]: 6e696d626c65 # "nimble" Workaround this by setting the device name once again after BLEDevice::createServer. * Temporarily lower CORE_DEBUG_LEVEL to INFO to avoid triggering an apparent ESP-IDF Bluetooth bug when re-connecting to Pixel 8 Android devices. Initial pairing works, but after ESP32 is rebooted, phone fails to reconnect. Meshtastic app shows it as disconnecting immediately. LightBlue shows a more detailed error "Peripheral Connection - Warning: onConnectionStatusChange: status 61" (0x3D - MIC Failure). Bug report to Espresssif: https://github.com/espressif/esp-idf/issues/18126#issuecomment-4286197744 * Temporarily disable ble_gap_set_data_len, causes crash with Pixel 8 Android reconnect. Crash looks like this: [ 11966][E][BLEAdvertising.cpp:341] setScanResponseData(): ble_gap_adv_rsp_set_data: 22 [ 11975][E][BLEAdvertising.cpp:1554] start(): Host reset, wait for sync. ERROR | ??:??:?? 11 BLE failed to start advertising Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled. Core 0 register dump: PC : 0x420e6190 PS : 0x00060730 A0 : 0x820e158b A1 : 0x3fce50c0 A2 : 0x00000000 A3 : 0x3fcb8600 A4 : 0x3fcb85cc A5 : 0x00000000 A6 : 0x00000000 A7 : 0x00000c03 A8 : 0x00000000 A9 : 0x3fce50b0 A10 : 0x0000000e A11 : 0x00000000 A12 : 0x00000010 A13 : 0x3fce50e0 A14 : 0x00000c03 A15 : 0x00000001 SAR : 0x0000001e EXCCAUSE: 0x0000001c EXCVADDR: 0x00000000 LBEG : 0x400570e8 LEND : 0x400570f3 LCOUNT : 0x00000000 Backtrace: 0x420e618d:0x3fce50c0 0x420e1588:0x3fce5110 0x420dfe87:0x3fce5200 0x420dfefb:0x3fce5220 0x420dff3f:0x3fce5240 0x4219602b:0x3fce5260 0x4037b0e5:0x3fce5280 0x4201edf3:0x3fce52a0 Connection seems fast enough even without this. We'll investigate the reason for the crash and re-enable once it's safe. --------- Co-authored-by: Catalin Patulea <cronos586@gmail.com> * Add extension from pioarduino nag "Jason2866.esp-decoder" * Cleanup after merge * ESP32: Disable classic bluetooth * Cleanup: Fix ADC channels on new variants * InkHUD: Fix type casting for message size in saveToFlash method inkhud compiles again! * update p4 esp_hosted for BT * I thought I fixed this * fix linker error using response file (p4 only) * fix infinite loop * Fix Power.cpp check warning Local variable 'config' shadows outer variable [shadowVariable] * Build ESP32 original with NimBLE ('custom_sdkconfig' approach). (#10235) * Re-enable littlefs json manifest This works locally again :) Not sure what changed * Re-add tool-mklittlefs * sensecap indicator fixes after upgrade arduino-esp & lovyanGFX libs * hackaday fix * robot tbeam cache error fix Co-authored-by: Copilot <copilot@github.com> * trunk fmt * ignore trunk * BLEDevice::deinit() added Co-authored-by: Copilot <copilot@github.com> * platformio-custom: Modify mtjson target dependency to prevent fake-success. (#10291) Co-authored-by: Copilot <copilot@github.com> * Fix ESP32-C6 linker errors. Align .text.handler_execute section to 4 bytes and update watchdog timer core mask configuration Co-authored-by: Copilot <copilot@github.com> * tlora-c6: Disable Screen MESHTASTIC_EXCLUDE_SCREEN=1 on tlora-c6. It doesn't have a screen, and this gets it compiling again (saving flash). * Use mverch's iram_memset hack for all OG-ESP32 * Refactor watchdog timer initialization and handling * use adc_channel_t in variant.h * Fix variant headers * More idiomatic default ethernet that doesn't break the build * Elecrows: Delete problematic variant.cpp Not needed after USE_ETHERNET_DEFAULT --------- Co-authored-by: mverch67 <manuel.verch@gmx.de> Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com> Co-authored-by: Catalin Patulea <cronos586@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vidplace7 <1779290+vidplace7@users.noreply.github.com> Co-authored-by: Copilot <copilot@github.com> Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
4a1ff18f57 |
feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa) (#10193)
* feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)
Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.
Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
host stack, exposing the Meshtastic GATT service with legacy
connectable advertising, just-works pairing, dynamic MTU exchange
(up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
immediate mode, newlib-nano heap sized to leave room for the GATT
pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
issue where Zephyr's two-pass linker script generation does not run
automatically; the post-script parses build.ninja and invokes the
gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
build_src_filter so the shared platform tree does not leak between
targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
rtt_*.txt).
Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
guards / helpers so the Zephyr build compiles alongside the Arduino
targets. Behavior on existing boards is unchanged.
Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.
Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.
Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.
* fix(nrf54l15): use atomic fs_rename instead of copy fallback
Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): expand storage_partition from 36KB to 700KB
LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.
Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): drop USERPREFS_LORACONFIG_* so LoRa config stays mutable
NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): enforce MITM passkey pairing on GATT service
- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
on all mesh service characteristics so clients must complete passkey
exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
SC pairing with reason 0x01 within 150ms).
* fix(nrf54l15): resolve develop-merge conflict + cppcheck warnings
The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR #10254 (which independently applied the same UAF guard this PR was
carrying) and the out-of-line version this PR introduced. GCC rejects
the duplicate, breaking every platform build. Drop the out-of-line
declaration + definition; keep upstream's inline form.
Also silence the 13 cppcheck low warnings introduced by the new
nrf54l15 Arduino shim — Arduino's `String`/`SPISettings` API contract
relies on implicit single-arg constructors used pervasively by
existing Meshtastic code, so suppress `noExplicitConstructor` inline
with a comment instead of breaking the API. The few mechanical wins
(`const tmp[2]`, `const uint32_t *sp`) are applied directly.
* fmt: fix Trunk Check lint issues on nrf54l15-port
- extra_scripts/nrf54l15_linker.py: move regular imports above
Import("env") to silence E402, add trunk-ignore-all(F821) for the
PIO/SCons SConstruct injection (matches esp32_pre.py / nrf52_extra.py
convention)
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clang-format 16.0.3
- boards/nrf54l15dk.json + variants/nrf54l15/nrf54l15dk/README.md:
prettier 3.8.3 (also resolves markdownlint MD060 on README tables)
No behavior change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address Copilot review comments + correct clang-format style
Six review threads from the 2026-04-30 Copilot review:
- src/platform/nrf54l15/nrf54l15_main.cpp: validate PSP against the nRF54L15
SRAM range (0x20000000..0x20040000) and 4-byte alignment before walking the
faulting thread's stack, and clamp the walk so it never reads past the end
of RAM. Prevents a second fault inside the fatal handler when PSP is
corrupted (common in real faults).
- src/platform/nrf54l15/nrf54l15_arduino.cpp: gate the bring-up printk traces
in digitalWrite/digitalRead (CS/NRESET toggle log, BUSY-before-NRESET
snapshot, BUSY periodic timeline) behind a new -DNRF54L15_GPIO_DEBUG flag
that is off by default. The "dev NOT READY" message stays unconditional —
it indicates a genuine hardware/DTS misconfig.
- src/modules/AdminModule.cpp: don't mutate config.device.output_gpio_enabled
from handleGetConfig(). Reflect the live pin state in the response payload
only — a getter must not write back to disk-persisted state.
- src/platform/nrf54l15/InternalFileSystem.h: derive totalBytes() from
FIXED_PARTITION_SIZE(storage_partition) at compile time so it tracks the
DK overlay's ~700 KB partition instead of the stale 36 KB hard-coded value.
Updated the file header comment accordingly.
- extra_scripts/nrf54l15_linker.py: make _extract_gcc_command() handle the
POSIX Ninja COMMAND format (no `cmd.exe /C "..."` wrapper) in addition to
the Windows form, so the script doesn't hard-fail on Linux/macOS hosts.
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clamp NO_PIN to RANDOM_PIN
with a one-shot LOG_WARN. The mesh GATT permissions are declared with
BT_GATT_PERM_*_AUTHEN and prj.conf sets CONFIG_BT_SMP_ENFORCE_MITM=y, so
NO_PIN with no auth callbacks would leave every characteristic returning
BT_ATT_ERR_AUTHENTICATION. Falling back to RANDOM_PIN keeps the link
usable instead of silently broken. Also re-formatted this file with the
project's .trunk/configs/.clang-format (Linux braces, 4-space indent,
130-col) — the previous lint-fix commit a2aca3234 accidentally used the
default LLVM style here, which CI's clang-format would have rejected.
Build verified: pio run -e nrf54l15dk passes, RAM 47.4%, Flash 28.6%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): address remaining Copilot review threads
Round 2/3 review fixes — bugs first, then docs/portability:
BLE concurrency (NRF54L15Bluetooth.cpp):
- onNowHasData / sendLog / BleDeferredThread / shutdown: acquire
active_conn under ble_mutex via new acquire_active_conn() helper so
disconnected_cb can't free the conn between the null check and
bt_conn_ref/bt_gatt_notify (use-after-unref).
- write_toradio: reject writes that exceed MAX_TO_FROM_RADIO_SIZE with
ATT_ERR_INVALID_ATTRIBUTE_LEN instead of returning success and silently
dropping the payload (would hide failed config writes from the phone).
- start_advertising: truncate the device name to fit the 31-byte legacy
scan-response limit and switch to BT_DATA_NAME_SHORTENED so
bt_le_adv_start() doesn't reject the payload when the name approaches
CONFIG_BT_DEVICE_NAME_MAX=32.
Linker / portability:
- main.h: drop the rp2040Loop() forward declaration that had no
definition and no callers — would surface as a link error if any RP2040
build added a call to the symbol.
- nrf54l15_arduino.cpp: transfer16() now uses static __aligned(4) DMA
buffers (matching transfer()), removing the EasyDMA-reachability hazard
of caller-stack buffers on this part.
Filesystem (InternalFileSystem.h):
- usedBytes(): return real usage from fs_statvfs() instead of 0 so OTA
/ range-test free-space guards work.
- rewindDirectory(): close the dir before reopening — Zephyr fs_dir_t has
no rewind, and re-fs_opendir on an open handle leaks LittleFS state.
Crash handler (nrf54l15_main.cpp):
- After the stack walk, busy-wait 50 ms to flush RTT/printk and call
sys_reboot(SYS_REBOOT_COLD) directly so the saved_crash record is
actually reported on the next boot. Default Zephyr config has
RESET_ON_FATAL_ERROR=n, so the previous k_fatal_halt() spun forever.
Generalization / config:
- PhoneAPI.cpp: replace the NRF54L15_DK ifdef with a
MESHTASTIC_EXCLUDE_FILES_MANIFEST capability flag (defined in the
nrf54l15dk env) so future variants can opt in/out without touching
shared code.
- variants/nrf54l15/nrf54l15.ini: parameterize libdeps include paths via
${PIOENV} so additional nRF54L15 envs sharing nrf54l15_base don't break.
- prj.conf: drop the stale "36 KB storage_partition" comments — the DK
overlay reclaims slot1 to ~700 KB and runtime size comes from
FIXED_PARTITION_SIZE.
- nrf54l15dk overlay: remove the zephyr,console / zephyr,shell-uart
chosen entries that conflicted with CONFIG_UART_CONSOLE=n + RTT
console; keep uart30 enabled so swapping the console is one Kconfig
flip away.
Build: nrf54l15dk SUCCESS (flash 28.6%, RAM 47.4%); wiznet_5500_evb_pico2
SUCCESS (verifies the shared main.h change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15dk): use PRIVATE_HW (255) for custom_meshtastic_hw_model
Per @thebentern's review: the nRF54L15-DK is a development kit, not a
canonical Meshtastic SKU, so it falls under HardwareModel::PRIVATE_HW
(255) — the same enum value already used at runtime via HW_VENDOR. The
placeholder 132 is removed; no dedicated enum number will be assigned
for DK boards. Slug stays NRF54L15_DK as a human-readable identifier.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(nrf54l15): unstarve bt_long_wq so SC pairing completes
bt_pub_key_gen() runs the ECC P256 key generation on bt_long_wq. At default
prio=10 (preemptible) and stack=1400 it gets starved by Meshtastic app
threads at boot — sc_public_key stays NULL for minutes, smp_public_key()
defers with SMP_FLAG_PKEY_SEND, and every SC pairing attempt stalls right
after the public-key exchange. iOS shows "Connecting…" forever with no
PIN prompt; bleak/CLI fails the first CCC notify write with
"Protocol Error 0x05: Insufficient Authentication".
Set CONFIG_BT_LONG_WQ_PRIO=0 (highest preemptible, ties with main) and
CONFIG_BT_LONG_WQ_STACK_SIZE=4096 (margin for the P256M driver frames).
Validated E2E with iOS Meshtastic app: bt_smp_pkey_ready fires within ~40 s
of boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm,
encrypt 0x01 / sec_level 0x04 (Authenticated MITM), bonded=1.
* feat(nrf54l15): hardware I2C bus via TWIM30 + sensor telemetry
Adds the Arduino TwoWire layer for the nRF54L15-DK so Meshtastic's
sensor drivers can talk to external I2C devices over the hardware
TWIM30 peripheral.
Bus binding:
- &uart30 disabled in the board overlay (peripheral instance 30 is
shared between UARTE30 / TWIM30 / SPIM30 — pick one). Console stays
on RTT via CONFIG_RTT_CONSOLE.
- New i2c30_default / i2c30_sleep pinctrl with SDA=P0.03 / SCL=P0.04.
External 4.7 kOhm pull-ups required on both lines.
- &i2c30 enabled at I2C_BITRATE_FAST (400 kHz).
- button_3 (SW3, P0.04) deleted from DTS so the pad can be claimed by
i2c30 pinctrl; SW3 is still wired to the pad on the DK, do not press
it during I2C use or it will short SCL to GND.
Arduino layer:
- src/platform/nrf54l15/Wire.cpp resolves the DT node at compile time
via DEVICE_DT_GET(DT_NODELABEL(i2c30)) and dispatches Arduino's
beginTransmission / write / endTransmission / requestFrom to
i2c_write / i2c_write_read / i2c_read. Buffer is sized to 256 bytes
for forward compatibility with the SE050 secure element on the
custom PCB.
- Wire.h drops the prior compile-only stubs and exposes the real
TwoWire surface.
- Arduino.h: BitOrder becomes an enum (not #define) so Adafruit_BusIO's
`typedef BitOrder BusIOBitOrder;` compiles.
Variant + build flags:
- nrf54l15.ini flips HAS_WIRE / HAS_SENSOR / HAS_TELEMETRY from 0 to 1
and cherry-picks the sensor libs Meshtastic needs (BusIO, Sensor,
BMP280, BME280, INA219/226/260/3221, SHT4X). The full
environmental_base group is avoided because it pulls
Adafruit_SSD1306 / Adafruit_GFX which rely on Arduino pin macros the
Zephyr shim does not implement.
- nrf54l15dk variant.h defines PIN_WIRE_SDA / PIN_WIRE_SCL for parity
with the Arduino convention used by other variants. The actual bus
wiring is fixed by the overlay pinctrl above.
Validated 2026-05-14/15 on the DK with BMP280 @ 0x76 (temperature +
barometric pressure) and INA3221 @ 0x42 (rail voltage / current);
EnvironmentTelemetry / PowerTelemetry packets transmit successfully
over LoRa.
Footprint cost on nrf54l15dk: +45 KB flash, +1.7 KB RAM.
* feat(nodedb): honor USERPREFS for environment telemetry on first boot
installDefaultConfig() now respects two new compile-time prefs:
USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
The mobile apps enforce a 30 min floor on environment_update_interval
in the settings UI, which makes short-interval bring-up testing of new
sensor hardware painful — you have to wait half an hour for the first
LoRa packet to confirm wiring + driver. With these prefs baked into
the variant, the firmware can ship a freshly-flashed device that
broadcasts on a shorter cadence (e.g. 900 s) the moment storage_partition
is empty.
Both prefs are gated on #ifdef so the behavior is unchanged for any
variant that does not opt in. Documented in userPrefs.jsonc with the
existing telemetry-interval pref block.
* fix(nrf54l15): allow multiple bonded BLE peers
CONFIG_BT_MAX_PAIRED defaults to 1, so once the first peer (e.g. an
iOS phone) has paired and bonded, every subsequent pairing attempt
from a different MAC fails inside bt_keys_get_addr() with no free
key slot — the host returns BT_SECURITY_ERR_KEY_DOES_NOT_EXIST and
the second peer never gets past SMP.
Raise the slot count to 4 so the device can simultaneously hold an
iOS phone, a Windows host, a Linux host, and one spare bond. Add
BT_KEYS_OVERWRITE_OLDEST so that once the table fills, the LRU peer
is evicted on the next pairing rather than rejecting the new peer.
This matches the behavior other Meshtastic ports already provide
(nRF52 uses CONFIG_BT_PERIPHERAL_PRIO_CONN with similar semantics).
Discovered while bringing up the Python CLI on Windows alongside
the existing iOS bond.
* fix(nrf54l15): zero-initialize TwoWire buffers + clang-format Wire
cppcheck on every CI target (esp32s3, rp2040, rp2350, nrf52840, ...) was
failing the build with two `uninitMemberVar` warnings on TwoWire's
constructor: `txBuf` and `rxBuf` (256-byte arrays) were not initialized.
Even though the buffers are only read after txLen/rxLen is set, leaving
them uninitialized is a footgun if any future caller bypasses the
len-set step. Use C++11 value-initialization in the member initializer
list — costs ~512 B of memset at boot, gains a clean cppcheck pass and
defensive-against-future-changes semantics.
Also reformat Wire.{cpp,h} with the project's `.trunk/configs/.clang-format`
config so the Trunk Check Runner passes — clang-format moved the
`<errno.h>` include before the Zephyr-namespaced ones in Wire.cpp and
collapsed two inline overloads to single lines in Wire.h.
* fix(AdminModule): remove dead OUTPUT_GPIO_PIN/GpioOutputModule references
OUTPUT_GPIO_PIN is never defined and modules/GpioOutputModule.h doesn't
exist in the codebase; all #ifdef OUTPUT_GPIO_PIN branches were dead code
introduced by the nRF54L15-DK variant commit. Strips the include, the
output_gpio_enabled OFF→ON/ON→OFF transition logic in handleSetConfig(),
and the digitalRead() reflection in handleGetConfig().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
|
||
|
|
8e99ffbe7e |
ThinkNode M7 (#8077)
* ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * ThinkNode G3, ETH support WIP * rename variant and add guard macros * older G3 operational. M7 next. * Split out G3 and M7 to different variants. Completely new PCB design. The G3 stays on 'PRIVATE_HW' * Define button behaviour and use all of the device flash --------- Co-authored-by: Ben Meadors <benmmeadors@gmail.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: caveman99 <25002+caveman99@users.noreply.github.com> Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz> |
||
|
|
94bb21ecc7 |
2.8: NodeDB shrink, decoupling, and restructuring (#10413)
* 2.8: NodeDB refactor to decouple satellite entries and decrease size * Regen * Refactor node mute handling to use dedicated functions for clarity and consistency * Develop ref * Fix NodeDB review follow-ups Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Address review validation nits Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/6b1d6cf6-ed6b-43b6-95cb-8e141757664e Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Trunk * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Extract legacy NodeDatabase migration * Fix remaining NodeDB review issues Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/c76b9a5a-7244-4fbc-9ef0-98091d8caaea Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Fixes * Trunk * Fix latest review compile follow-ups Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/5198da01-ec4c-4c16-8a09-68b8e6d5d410 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Fix cppcheck style warnings Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/e60287ba-4ece-46e0-83d8-a6d89664c0bb Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Change pointer type for mesh node in set_favorite function * Change pointer types for mesh node references to const in multiple applets * Add NodeDB layout v25 documentation and migration guidelines * Remove tests for uninitialized PacketHistory state due to undefined behavior * Fix code block formatting in copilot instructions --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
1eb860a3fc |
fix(stm32wl,nrf52,fs): flash hardening, FS platform unification, write-behind LFS cache (FORMAT BREAK) (#10171)
* stm32wl: check HAL_FLASH_Unlock() return in _internal_flash_erase _internal_flash_prog already checks HAL_FLASH_Unlock() and returns LFS_ERR_IO on failure. _internal_flash_erase discarded the return value, proceeding to erase even if the flash was not unlocked. Apply the same check for consistency and safety. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: fix _internal_flash_prog to abort on first write error Previously the programming loop continued to the next doubleword after HAL_FLASH_Program() failed, potentially writing to invalid addresses and returning a misleading error code only at the end (last iteration). HAL_FLASH_Lock() was also skipped on the mid-loop early return path. - Move bounds check before the loop (validate full range at once) - Break on first HAL error so subsequent doublewords are not written - Move HAL_FLASH_Lock() after the loop so it always runs Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: clear stale flash SR error flags before erase and program Stale error flags in FLASH->SR from a previous failed operation can cause HAL_FLASH_Program() or HAL_FLASHEx_Erase() to return HAL_ERROR immediately without attempting the operation. Add __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS) after each HAL_FLASH_Unlock() in both _internal_flash_prog and _internal_flash_erase to ensure a clean state before each operation. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: reject flash prog writes not aligned to 8-byte doubleword The STM32WL HAL minimum write unit is one 64-bit doubleword (8 bytes). _internal_flash_prog silently truncated any trailing bytes when size % 8 != 0 because dw_count = size / 8 drops the remainder. Return LFS_ERR_INVAL early so LittleFS sees the error rather than a silent short write. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(nrf52,fs): use atomic SafeFile rename instead of direct write NRF52 was bypassing the .tmp/readback/rename path entirely — openFile() deleted the target file and wrote directly to it, and close() returned true without verifying the write or renaming anything. Adafruit_LittleFS::rename() calls lfs_rename() directly (confirmed at Adafruit_LittleFS.cpp:205). Remove both ARCH_NRF52 guards so NRF52 follows the same write-to-.tmp → readback-hash → rename path used by all other platforms. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(admin): skip uiconfig.proto save on devices without a screen handleStoreDeviceUIConfig() was writing /prefs/uiconfig.proto unconditionally. MenuHandler.cpp is already gated behind #if HAS_SCREEN, so there is no path that populates UI config on screen-less platforms. Guard the save with #if HAS_SCREEN to avoid wasting a flash block on devices that will never use it. The read path (handleGetDeviceUIConfig) does not touch the filesystem and needs no change. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fs: enable format-on-retry for all platforms in saveToDisk The FSCom.format() call on save failure was guarded to ARCH_NRF52 with a comment that other platforms were not ready (bug #4184). STM32WL was added to the guard in a prior commit. All platforms now expose format semantics and the retry logic is identical — remove the guard. To keep NodeDB.cpp platform-agnostic and fix a CI failure on native-tft (portduino's fs::FS has no format() method), introduce fsFormat() in FSCommon as the single call-site for all callers: - Embedded (ESP32, NRF52, STM32WL, RP2040): delegates to FSCom.format() - Portduino: rmDir("/prefs") + FSBegin() (a no-op on portduino). rmDir("/prefs") is already called unconditionally by factoryReset() (NodeDB.cpp:504), so both primitives are proven on portduino. Replace both direct FSCom.format() calls in NodeDB.cpp with fsFormat(). Note: we do not run portduino locally — portduino/native build testers please verify the format-on-retry path. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * DO NOT MERGE: nrf52(fs): add File() default constructor bound to InternalFS Adds File() to the Adafruit LittleFS File class (in the Meshtastic Adafruit_nRF52_Arduino fork), delegating to File(InternalFS). This matches the default-constructible File API on all other platforms. The constructor is implemented in Adafruit_LittleFS_File.cpp rather than inline in the header to avoid a circular include between Adafruit_LittleFS_File.h and InternalFileSystem.h. FOLLOW-UP REQUIRED: nrf52.ini points to a commit SHA on the mesh-malaysia/Adafruit_nRF52_Arduino fork instead of the upstream meshtastic framework. Once meshtastic/Adafruit_nRF52_Arduino#5 is merged, revert nrf52.ini to point back to the upstream meshtastic framework URL. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): add File() default constructor and document LFS tunables Adds File() to STM32_LittleFS_Namespace::File, delegating to File(InternalFS). Implemented in the .cpp to avoid a circular include between STM32_LittleFS_File.h (which cannot include LittleFS.h) and the InternalFS extern declaration. This matches the File API on ESP32/RP2040/Portduino and is a prerequisite for removing the ARCH_STM32WL guard in xmodem.h. No behavior change — the constructor leaves the file in the same closed/unattached state as File(InternalFS) would. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fs: remove arch-specific ifdefs from FSCommon, SafeFile, xmodem Now that NRF52 and STM32WL have File() default constructors and NRF52 has working atomic SafeFile rename, the capability gaps are closed. Remove all per-arch guards across the shared FS layer: FSCommon.cpp — renameFile(): Use FSCom.rename() on all platforms. Adafruit_LittleFS::rename() calls lfs_rename() directly (Adafruit_LittleFS.cpp:205). The copy+delete fallback on NRF52/RP2040 was never necessary. FSCommon.cpp — getFiles(): Replace four ARCH_ESP32 guards with a single filepath pointer at the top of the loop (file.path() on ESP32, file.name() elsewhere). Fix strcpy(fileInfo.file_name, filepath): bounded to sizeof(fileInfo.file_name)-1 with explicit NUL termination to prevent overflow of the 228-byte meshtastic_FileInfo::file_name array. FSCommon.cpp — listDir(): Same filepath pointer approach. NRF52/STM32WL were in an else-branch that only logged but never deleted — now all platforms follow the unified del path. 12 guards → 2. Fix three strncpy(buffer, ..., sizeof(buffer)) calls that did not NUL-terminate when source length >= sizeof(buffer) (255 bytes). Add explicit buffer[sizeof(buffer)-1] = '\0' after each. FSCommon.cpp — rmDir(): Use listDir(del=true) everywhere. The ARCH_NRF52 rmdir_r() path and the ARCH_ESP32|RP2040|PORTDUINO listDir() path collapse to one line. SafeFile.cpp: ARCH_NRF52 bypass removed (handled in preceding commit). xmodem.h: File file; now works on all platforms via default constructors added in the two preceding commits. Remaining #ifdef ARCH_ESP32 in FSCommon.cpp: exactly 4, all for the file.path() vs file.name() API difference (ESP32 Arduino LittleFS returns the full path; all others return only the name). That difference lives in the framework and cannot be closed here. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): add write-behind page cache, reduce virtual block size and FS reservation (FORMAT BREAK) Adds a write-behind (RMW) page cache to the STM32WL LittleFS driver, modelled after the NRF52 Adafruit approach (flash_cache.c). This allows LFS to use 256-byte virtual blocks backed by 2048-byte physical pages: the erase/prog callbacks accumulate changes in a 2 KB RAM buffer; the sync callback (and page eviction on page-change) flushes with a single HAL physical-erase + doubleword-program pass. LFS tunables changed (FORMAT BREAK — superblock parameters): block_size: 2048 B → 256 B (8 virtual blocks per physical page) read_size: 2048 B → 256 B (= block_size) prog_size: 2048 B → 256 B (= block_size; hardware min is 8 B) block_count: 112 → 80 (14 phys pages → 10 phys pages = 20 KiB) Benefits: - Internal fragmentation: max 2047 B/file → max 255 B/file - Heap per open LFS file: ~4 KB → 512 B (prog + read buffers) - Code flash headroom: 6.7 KB → ~14.1 KB (+7.4 KB) - Block budget: 80 virtual blocks, worst-case peak ~20, ~60 free Updates board_upload.maximum_size in wio-e5/platformio.ini from 233472 (256 KB − 28 KB) to 241664 (256 KB − 20 KB) to match the reduced FS reservation. Justification for the format break: the prior STM32WL firmware had several flash write bugs fixed earlier in this series (missing error flag clearing, no abort on first write failure, unaligned write acceptance). These bugs very likely caused silent config corruption on deployed devices. The format break should be treated as an enhancement: it provides a clean, reliably-written starting point. Users will need to reconfigure their device once after this update. Correctness fixes applied to the cache implementation: - alignas(8) on _page_cache: the buffer was uint8_t[] (alignment 1) but _flash_cache_flush casts it to const uint64_t* — undefined behaviour per C++ standard, potential Cortex-M hardfault. alignas(8) guarantees the required alignment for the doubleword cast. - HAL_FLASH_Lock() return value: was discarded. Now assigned to lock_rc and propagated into rc if prior writes succeeded, so LFS sees the error rather than a false success. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): reduce FS reservation from 10 pages to 7 pages (FORMAT BREAK) Reduces LFS_FLASH_TOTAL_SIZE from 10 × 2 KiB pages (20 KiB) to 7 × 2 KiB pages (14 KiB), freeing 6 KiB for firmware. board_upload.maximum_size updated accordingly across all STM32WL variants: 241664 (256 KiB - 20 KiB) → 247808 (256 KiB - 14 KiB) This is a FORMAT BREAK: existing filesystems must be erased before use. Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> * fix(fs): return false in renameFile() when FSCom is not defined Avoids undefined behavior and -Wreturn-type warnings in configurations that compile FSCommon.cpp without a filesystem backend. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Ben Meadors <benmmeadors@gmail.com> |
||
|
|
e7c02da24b |
Merge remote-tracking branch 'origin/master' into develop
Co-authored-by: Copilot <copilot@github.com> |
||
|
|
2cc13a1132 | Sane sanitization |