Commit graph

209 commits

Author SHA1 Message Date
Matthew Wolter
2bf3f1b9dd fix: BATTERY handler drops frames shorter than 3 bytes (level field guard)
A BATTERY frame with len(data) < 3 caused dbuf.read(2) to return short
bytes; int.from_bytes(b"", ...) silently yielded 0, propagating a bogus
level=0 to HA sensors.  Same silent-zero class as N07 (storage fields).

Option B: early-return with debug log, matching the NEW-C pattern for
STATUS_RESPONSE.  No BATTERY event is dispatched for malformed frames.

Not in the original forensics report — discovered during G1 N07 work and
logged in issues_log.md.  Resolved here because no later branch touches
this handler.

Files changed:
- src/meshcore/reader.py: add `if len(data) < 3: return` guard before
  the level read in the BATTERY branch
- tests/unit/test_reader.py: add test_g1_battery_too_short_for_level —
  sends a 1-byte frame (type only), asserts no BATTERY event dispatched
  and debug log emitted
2026-04-11 19:24:26 -07:00
Matthew Wolter
d98523cddb G1: N08 + F12 + N10 — cosmetic cleanup in reader.py
Three small cleanup fixes bundled per proposal §4.1 commit order.

N08 — CONTROL_DATA empty payload guard. The handler reads
`payload = dbuf.read()` then immediately dereferences `payload[0]`
without checking length. A zero-length payload (firmware truncation
or garbled frame) raises IndexError. Pre-F06 the IndexError would
escape; post-F06 it would log and skip the dispatch via the umbrella.
Adding an explicit `if len(payload) == 0: return` after the read
short-circuits the empty case before it touches `payload[0]`, with a
debug log noting the empty payload. The `return` exits handle_rx
cleanly without engaging the F06 umbrella's parse-error path, which
is the correct behavior — an empty CONTROL_DATA frame is not a parse
error, it's an unusable frame.

F12 — print(res) leftover debug. The RAW_DATA handler had a stray
`print(res)` polluting stdout. Replaced with `logger.debug(res)` to
match the surrounding `logger.debug("Received raw data")` line.

N10 — magic numbers 16 and 17. Two `elif packet_type_value == 16/17`
branches hardcoded the integer values for CONTACT_MSG_RECV_V3 and
CHANNEL_MSG_RECV_V3, both already declared in packets.py:94-95.
Replaced with `PacketType.CONTACT_MSG_RECV_V3.value` and
`PacketType.CHANNEL_MSG_RECV_V3.value` to eliminate drift risk if
the enum is ever renumbered.

Findings: N08 (Info), F12 (Info), N10 (Info)
File: src/meshcore/reader.py
2026-04-11 18:33:20 -07:00
Matthew Wolter
1a3a665b17 G1: R02 — fix txt_type empty-slice (uncrypted[4:4] -> [4:5])
`uncrypted[4:4]` is the empty slice. `int.from_bytes(b"", "little")`
returns 0, so `txt_type` was always 0 for every decrypted channel
message — silently masking the upper 6 bits of byte 4. The line
immediately above (`attempt = uncrypted[4] & 3`) already proves byte
4 is in range, so widening the slice to `[4:5]` is safe.

This is a one-character fix and changes the observable value of
`txt_type` for all callers. Existing consumers that branched on
`txt_type` were effectively dead code; this restores the intended
behavior.

Finding: R02 (Info)
File: src/meshcore/meshcore_parser.py
2026-04-11 18:30:54 -07:00
Matthew Wolter
865c1b21b4 G1: NEW-B — wrap parsePacketPayload ADVERT branch in try/except
The ADVERT branch in MeshcorePacketParser.parsePacketPayload reads the
flags byte with `pk_buf.read(1)[0]`, which IndexErrors on a short
advert payload (the minimum advert is 32 + 4 + 64 + 1 = 101 bytes
before any optional fields). Pre-F06, the IndexError would escape as a
swallowed task exception. With F06's umbrella now in place it would
log and skip the dispatch, but the proposal §4.1 NEW-B asks for a
narrower local guard so a malformed advert doesn't poison the rest of
the parse path.

The optional `lat/lon/feat1/feat2` reads after the flags byte also
silently produce zeros on short reads (`int.from_bytes(b"", ...)`
returns 0), which would propagate bogus zero coordinates upstream.
Wrapping the whole branch limits the blast radius to a single
malformed advert.

Wrap the entire body of the ADVERT elif (from `pk_buf = io.BytesIO(...)`
through the final `log_data["adv_feat2"]` assignment) in
`try/except (IndexError, ValueError)` and log a debug message with the
exception type, message, and `pkt_payload` length on failure. This
matches the defensive pattern the proposal specifies.

Finding: NEW-B (S3)
File: src/meshcore/meshcore_parser.py
2026-04-11 18:29:41 -07:00
Matthew Wolter
c984a78e70 G1: R01 — guard parsePacketPayload against short payloads
Add a 2-byte hard-floor length check at the top of
MeshcorePacketParser.parsePacketPayload. The minimum viable payload is 1
header byte + 1 path_byte for a direct route; anything shorter would
crash on `path_byte = pbuf.read(1)[0]` (IndexError on the empty buffer).

The reader.py LOG_DATA branch only requires `len(data) > 3`, so a 4-byte
LOG_DATA frame produces a 1-byte payload here — that path is reachable.

The caller in reader.py dereferences log_data['route_type'],
['payload_type'], ['path_len'], and ['path'] immediately after the
parse, so an empty log_data would KeyError on the dispatch (caught by
the F06 umbrella, but the dispatch would still be skipped). Populate
sentinel values matching the existing route_typename = "UNK" pattern in
the function before the early return so the caller's downstream lookups
don't KeyError, then return early with a debug log.

Finding: R01 (S2)
File: src/meshcore/meshcore_parser.py
2026-04-11 18:26:37 -07:00
Matthew Wolter
a571eff4ce G1: NEW-C — add length guard to STATUS_RESPONSE push handler
Why: parse_status with offset=8 reads up through data[56:60]
(the rx_airtime field), so a full STATUS_RESPONSE push frame is
60 bytes: 1 type + 1 reserved + 6 pubkey + 52 status fields. The
push handler in handle_rx previously called parse_status with no
length check at all, so a short frame would slice through empty
data and silently produce zeros for every missing field. HA sensor
telemetry would silently report all-zero status — same class as N07.

The BINARY_RESPONSE STATUS path at the bottom of handle_rx already
gates parse_status with `len(response_data) >= 52` on its
offset-stripped buffer; this commit adds the equivalent gate for
the push path: `if len(data) < 60: log + return`. The `return`
short-circuits cleanly out of the umbrella try block without
dispatching a STATUS_RESPONSE event for the bogus parse.

Refs: Forensics report finding NEW-C (S3)
2026-04-11 18:21:27 -07:00
Matthew Wolter
3273c3489c G1: N07 — tighten BATTERY storage-field length guard
Why: The BATTERY handler previously gated the used_kb / total_kb
reads on `len(data) > 3`, which is wrong. The full
RESP_CODE_BATT_AND_STORAGE frame is 11 bytes (1 type + 2 level +
4 used_kb + 4 total_kb), so a 4-10 byte truncated frame would pass
the guard, and io.BytesIO.read(4) silently returns short bytes
instead of raising. int.from_bytes(b"", ...) returns 0, so HA
sensor telemetry silently reports zero storage on a truncated frame.

Tighten the guard to `len(data) >= 11` so the storage fields are
only parsed when the full frame is present. Inline comment added
to document the expected frame layout.

Note: the unconditional 2-byte `level` read at the top of the
handler has the same class of issue (no guard, silent zero on a
1-byte frame). That is out of scope for finding N07 and has been
logged in issues_log.md as a separate item.

Refs: Forensics report finding N07 (S3)
2026-04-11 18:19:52 -07:00
Matthew Wolter
a7e257c78d G1: F11 — replace broken except e: in ALLOWED_REPEAT_FREQ handler
Why: The ALLOWED_REPEAT_FREQ branch in handle_rx had `except e:` —
syntactically valid Python only if `e` happens to be bound to an
exception class, which it isn't. The first time the inner read loop
actually raised, the except clause itself would raise NameError
("name 'e' is not defined") and propagate out of the handler. The
proposal correctly notes this is unreachable in practice today
because `int.from_bytes(b"", ...)` returns 0 so the loop terminates
cleanly, but it is a latent footgun. Replace with the standard
`except Exception as e:` form and swap the `print(e)` for a proper
`logger.warning(...)` call to match the rest of the file (which uses
the module logger, not stdout).

Refs: Forensics report finding F11 (S3)
2026-04-11 18:17:17 -07:00
Matthew Wolter
2025cb5326 G1: F10 — fix pbuf NameError in PUSH_CODE_LOGIN_FAIL handler
Why: The LOGIN_FAILED handler in handle_rx referenced an undefined
identifier `pbuf` instead of the local BytesIO `dbuf`. Firmware emits
PUSH_CODE_LOGIN_FAIL as a fixed 8-byte frame, which trivially
satisfies the `len(data) > 7` guard, so every remote auth failure
raised NameError. The sibling LOGIN_SUCCESS handler a few lines above
already uses `dbuf.read(6).hex()` correctly; this commit aligns the
LOGIN_FAILED branch with the same pattern.

Refs: Forensics report finding F10 (S1)
2026-04-11 18:15:29 -07:00
Matthew Wolter
d9197faf3a G1: F06 — wrap handle_rx dispatch in catch-all try/except
Why: handle_rx is invoked from a detached task in MessageReader, so any
exception escaping its ~850-line if/elif dispatch is silently swallowed
by asyncio as "Task exception was never retrieved." The only crash
guard previously was a single try/except IndexError around the first
byte read; everything past line 73 was unguarded. This commit adds an
umbrella try: ... except Exception as e: around the entire dispatch
body that logs the exception class, message, raw frame hex, and full
traceback via logger.error. The umbrella neutralizes the crash surface
of F10, F11, N07, N08, R01, NEW-B, and NEW-C, which the next commits
will then fix individually now that they are observable.

Refs: Forensics report finding F06 (umbrella crash protection)
2026-04-11 18:06:53 -07:00
Alex Wolden
5e9cb559e7 Use firmware suggested_timeout for login and path discovery sync methods
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:42:21 -07:00
Alex Wolden
20f3bccb58 Deprecate fire-and-forget mesh request methods
Add warnings to send_login, send_statusreq, send_telemetry_req, and
send_path_discovery pointing users to their _sync counterparts. The
fire-and-forget versions bypass the mesh request lock and can cause
silent response drops due to firmware clearPendingReqs() behavior.
2026-04-05 18:38:06 -07:00
Alex Wolden
ab3e507e1f Add mesh request lock to serialize firmware-bound mesh commands
The companion firmware can only track one outstanding mesh request at a
time — clearPendingReqs() zeros all pending response flags before each
outgoing mesh request. Overlapping mesh commands cause silent response
drops.

Adds _mesh_request_lock to CommandHandlerBase and wraps all _sync
methods with it. Also adds send_login_sync and send_path_discovery_sync
for complete round-trip serialization of those commands.

Local commands (get_bat, get_channel, set_time, send_msg, etc.) are
unaffected — they don't trigger clearPendingReqs() on the firmware.
2026-04-04 23:18:21 -07:00
Florent
40a70222c8 don't put chan_name in log_rx if we don't know it 2026-03-29 10:53:43 -04:00
Florent
be3aa103c5 adds more min_timeout when fetching lots of neighbours 2026-03-29 07:57:08 -04:00
Florent
fe5096eb9e add hashtag to scope if absent 2026-03-27 20:12:15 -04:00
Florent
eca375dc8a apply frame header fix to tcp as well 2026-03-22 12:51:01 -04:00
fdlamotte
52ad5c201c
Merge pull request #67 from jkingsman/respect-found-idx
Use the frame start once we've found it
2026-03-22 12:48:11 -04:00
Jack Kingsman
4df3655752
Use the frame start once we've found it 2026-03-21 21:08:04 -07:00
Jack Kingsman
3ad77d364d
Fix three byte path packets 2026-03-18 17:31:17 -07:00
Florent
5bfe63912c set decrypt_channel_logs to False by default 2026-03-11 10:21:29 -04:00
Florent
3c81f67608 uploading missing file 2026-03-09 20:58:43 -04:00
Florent
18528f2ed3 make a class and module for parsing meshcore packets 2026-03-09 18:22:02 -04:00
Florent
f3fce820fc fix error 2026-03-08 15:11:24 -04:00
Florent
5e4663d058 there is still a strange bug with path_len 2026-03-08 08:21:56 -04:00
Florent
01471c0d24 fix nasty bug when updating contact flags 2026-03-08 07:04:33 -04:00
Florent
cda44ae0a0 and if error message does not exist yet 2026-03-07 21:13:30 -04:00
Florent
fe2239a8c6 add code_string to error event 2026-03-07 21:05:00 -04:00
Florent
462c4311d3 implement advert_path 2026-03-07 17:42:41 -04:00
josh
3358916e4c f-string quote fix 2026-03-07 13:58:03 +11:00
Florent
0bfa8003d5 remove some debug printfs 2026-03-06 11:11:54 -04:00
Florent
c378319252 some work on multibytes 2026-03-06 10:40:14 -04:00
Florent
f57cb66277 fix silly bug 2026-03-06 08:27:26 -04:00
Florent
563cbfbade complet channel log rx and use timestamp to calculate hashes 2026-03-05 21:32:24 -04:00
Florent
a83956ec1f some optimizations 2026-03-05 15:37:54 -04:00
Florent
322736024a fix 2026-03-05 14:53:43 -04:00
Florent
91be955044 error when msg_hash was not here 2026-03-05 14:41:19 -04:00
Florent
9f66885594 verify if channels has hashes ... 2026-03-05 13:56:29 -04:00
Florent
3d47d6d8b2 if possible, add path and rssi to channel messages 2026-03-05 11:50:41 -04:00
Florent
b1abb8e4d3 extract some info from log_rx 2026-03-05 09:32:19 -04:00
Florent
3716ebf77e handle multibytes in path discovery 2026-02-27 08:59:17 -04:00
Florent
f2def83f30 fix path len issue when getting contacts 2026-02-27 08:33:27 -04:00
Florent
ee75ed12dd add get_path_hash_mode 2026-02-27 07:01:20 -04:00
Florent
dd6d6350d9 multibyte trace support 2026-02-26 22:51:52 -04:00
Florent
32907bb5c1 handle path_hash_size in change_path 2026-02-26 16:48:23 -04:00
Florent
26730d1efa support path_hash_mode 2026-02-26 14:24:02 -04:00
fdlamotte
2c259b9743
Merge pull request #60 from agessaman/fix-sync-callbacks
Fix: Sync callbacks called inline in `_process_events` to eliminate `no_event_received` race
2026-02-21 07:30:25 -04:00
agessaman
a9f68c2971 modify event callback handling to call synchronous callbacks inline, ensuring futures are resolved before waiting. This prevents race conditions when scheduling callbacks asynchronously. 2026-02-19 19:31:56 -08:00
Florent
3f56190423 Revert PR 45 2026-02-18 06:50:44 -04:00
Florent
e51838177f fix lpp values for voltage and current as signed 2026-02-17 08:52:56 -04:00