recovery_handle_info() in core/recovery.c builds its response in a struct that is not __attribute__((packed)) — unlike rcvr_packet_t fifteen lines above it — and never zeroes it. Only the six named members are assigned, then eos_hal_uart_send(&info, sizeof(info)) transmits the whole object.
Measured, not argued:
sizeof(info) == 24 offsetof(flash_size) == 4
so bytes 1–3 are padding the code never writes. RCVR_CMD_INFO is not in cmd_requires_auth(), so any client on the UART can ask, repeatedly, before authenticating. With the struct poisoned to 0xA5 and filled exactly as the firmware fills it:
wire[0..8]: AA A5 A5 A5 00 00 10 00
Three bytes of whatever the previous call left on the stack, to an unauthenticated caller.
The repo's own client, tools/uart_recovery.py, reads 1 + 4*5 = 21 bytes and unpacks '<IIIII' from response[1:21] — it has always expected the packed layout. So it decodes flash_size = 0x00A5A5A5 (the leaked bytes, printed as "Flash size"), every slot address one word off, and leaves three surplus bytes in the serial buffer to desynchronise whatever command follows.
grep -rn "CMD_INFO\|handle_info" tests/ is empty. Nothing has ever exercised this command.
Raised by the automated review of #131 (finding 1, P1, pre-existing). Fix in #139: pack the struct and memset it — which makes the firmware send what the client always read, a repair rather than a format change — plus a test that drives RCVR_CMD_INFO through the sim_ops harness with the output buffer poisoned first.
recovery_handle_info()incore/recovery.cbuilds its response in a struct that is not__attribute__((packed))— unlikercvr_packet_tfifteen lines above it — and never zeroes it. Only the six named members are assigned, theneos_hal_uart_send(&info, sizeof(info))transmits the whole object.Measured, not argued:
so bytes 1–3 are padding the code never writes.
RCVR_CMD_INFOis not incmd_requires_auth(), so any client on the UART can ask, repeatedly, before authenticating. With the struct poisoned to0xA5and filled exactly as the firmware fills it:Three bytes of whatever the previous call left on the stack, to an unauthenticated caller.
The repo's own client,
tools/uart_recovery.py, reads1 + 4*5 = 21bytes and unpacks'<IIIII'fromresponse[1:21]— it has always expected the packed layout. So it decodesflash_size = 0x00A5A5A5(the leaked bytes, printed as "Flash size"), every slot address one word off, and leaves three surplus bytes in the serial buffer to desynchronise whatever command follows.grep -rn "CMD_INFO\|handle_info" tests/is empty. Nothing has ever exercised this command.Raised by the automated review of #131 (finding 1, P1, pre-existing). Fix in #139: pack the struct and
memsetit — which makes the firmware send what the client always read, a repair rather than a format change — plus a test that drivesRCVR_CMD_INFOthrough thesim_opsharness with the output buffer poisoned first.