fix(micromate): the 0x5A chunk offset is a uint32 -- a 64 KB cap was one event away
UM20147 holds a 72,560-byte event. chunk_params() wrote the offset as a uint16 at params[2:4], because every offset THOR was observed to send fits in two bytes (largest 0x3400 = 13,312), which caps a download at 65,536 B -- so that event could not have been fetched at all. params[0:4] is demonstrably ONE 4-byte field: chunk 0 puts the 4-byte event key there. Writing the offset as a uint32 BE in the same slot is BYTE-IDENTICAL for every offset below 65,536, so nothing verified against THOR's 74 captured frames changes -- the replay test still matches all of them -- and the range extends to 4 GB. Above 64 KB this is inference and the docstring says so: the field's width is established, the device's handling of a non-zero high byte is not. Also newly reachable: at offset 1 MiB params[1] is 0x10, which must go out as `10 10`. Nothing below 64 KB can produce that, so the uint32 change is what first makes the case possible -- and an unescaped 0x10 in 5A params is the exact bug that cost the Series III walk a release. Tested. THIS IS THE SERIES III 64 KB PAGE-BOUNDARY BUG WEARING A DIFFERENT HAT. There, parse_strt_end_offset() discards the key's page byte and the walk crashes once a unit's buffer crosses 64 KB; that one is still open. The transferable lesson: an address field whose high bytes are zero in every capture is not a narrow field, it is an untested one. Same mistake, found twice, in code written years apart. Confirmed in the same run: 0x06 content[0:4] IS the event count -- UM20147 holds 5 events and reads 5, making it three for three across both firmware lines (0->0, 6->6, 5->5). And content[4:8] is a CONSTANT, not a count: it reads 9 on a unit with 6 events and on a unit with 5. list_events() still walks to the sentinel; the count is worth adopting as a pre-check, not a replacement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ru8Lg9HkkYvX9VWWo65SmL
This commit is contained in:
+24
-4
@@ -170,15 +170,35 @@ def chunk_params(key4: bytes, byte_offset: int) -> bytes:
|
||||
"""`0x5A`: the key opens the file, then a byte offset walks it.
|
||||
|
||||
Chunk 0 carries the event key at params[0:4] — that is what says "from the
|
||||
beginning". Later chunks carry a uint16 BE byte offset at params[2:4].
|
||||
beginning". Later chunks carry the byte offset **in that same 4-byte slot**,
|
||||
as a uint32 BE.
|
||||
|
||||
⚠ **Written as a uint32 deliberately, and this matters above 64 KB.** Every
|
||||
offset THOR was observed to send fits in two bytes — the largest was `0x3400`
|
||||
(13,312) — so `params[0:2]` was always `00 00` and the field looks like a
|
||||
uint16 at `params[2:4]`. Reading it that way caps a download at **65,536
|
||||
bytes**, and UM20147 currently holds a **72,560-byte** event, so that cap is
|
||||
not hypothetical.
|
||||
|
||||
A uint32 here is **byte-identical for every offset below 65,536**, so it
|
||||
changes nothing that was verified against THOR's frames (the replay test
|
||||
asserts all 74 of them) and extends the range to 4 GB. Above 64 KB it is
|
||||
**inference**: `params[0:4]` is demonstrably one 4-byte field, because chunk 0
|
||||
puts a 4-byte key in it, but no capture exercises a carry into `params[1]`.
|
||||
|
||||
⚠ This is the **Series III 64 KB page-boundary bug in a new guise** — there,
|
||||
`parse_strt_end_offset()` discards the key's page byte and the `5A` walk
|
||||
crashes once a unit's buffer crosses 64 KB, which is *still open* on that
|
||||
side. The lesson that transfers: an address field whose high bytes are zero
|
||||
in every capture is not a narrow field, it is an untested one.
|
||||
"""
|
||||
if byte_offset == 0:
|
||||
if len(key4) != 4:
|
||||
raise ValueError(f"key4 must be 4 bytes, got {len(key4)}")
|
||||
return key4 + bytes(6)
|
||||
if not 0 <= byte_offset <= 0xFFFF:
|
||||
raise ValueError(f"byte_offset must fit in uint16, got {byte_offset}")
|
||||
return bytes(2) + struct.pack(">H", byte_offset) + bytes(6)
|
||||
if not 0 <= byte_offset <= 0xFFFFFFFF:
|
||||
raise ValueError(f"byte_offset must fit in uint32, got {byte_offset}")
|
||||
return struct.pack(">I", byte_offset) + bytes(6)
|
||||
|
||||
|
||||
# ── Protocol ──────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user