Repository navigation
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
CLI_REPLY_MAX currently ignores the 3-byte reply-prefix reservation done by some callers before delegating to CommonCLI::handleCommand(), which can reintroduce a small out-of-bounds write.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This PR hardens the sensor list [start] CLI command against integer wraparound and reply-buffer overruns that can lead to crashes/reboots (ESP32-S3) or hangs (nRF52), including when triggered via remote admin. It adds bounded parsing for the paging start index, switches the listing output to bounded writes, and documents the paging reply shape.
Changes:
- Add
parseStartIndex()to clampstartduring parsing (avoids_atoi()wrap and negativeintstart values). - Make
sensor listoutput safer viasnprintf, pre-flight space checks per row, and NULL-guarding accessor results. - Update CLI documentation to describe the header/continuation marker behavior and the out-of-range reply.
File summaries
| File | Description |
|---|---|
| src/helpers/CommonCLI.cpp | Adds bounded parsing and safer paging/formatting for sensor list, plus explicit reply-size assumptions. |
| docs/cli_commands.md | Documents sensor list reply shape, paging marker, and out-of-range behavior. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot on upstream meshcore-dev#3433: every wrapper that reaches CommonCLI::handleCommand() -- simple_repeater, simple_room_server and simple_sensor -- reflects an optional 3-byte "xx|" companion-radio prefix back into the reply and does `reply += 3` before delegating, so the pointer this function receives can be 3 bytes short of the buffer it was cut from. `lim = reply + 160` then points past the end of the serial CLI's char[160], and the snprintf bounds meant to stop an overrun allow up to 3 bytes of one instead. Upstream's `dp-reply < 134` happened to be conservative enough to hide this; an exact bound is not, so the constant has to be the smallest buffer *after* a prefix may have been taken off it: 157. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
508e65b to
ccff85f
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The changes directly address the described crash/hang path by preventing index wrap, bounding formatting/writes, and documenting the resulting output behavior.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
`sensor list <start>` parsed start with _atoi(), whose uint32_t goes negative as an int: a start of 4294967295 is -1 and 2147483648 is INT_MIN. Both passed the `start >= end` guard, so the loop asked the sensor manager for a negative index and passed the NULL it returned to "%s=%s", undefined behaviour that picolibc happens to print as "(null)". Rejecting a negative start closes it: ZephyrSensorManager has one setting, so only index 0 is ever requested. The same defect as meshcore-dev/MeshCore#3433. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sensor list <n>` passes two NULL pointers to sprintf("%s=%s\n", ...)
for any n in [2147483648, 4294967295]. On the C libraries these targets
link against, that faults rather than printing "(null)".
_atoi() returns uint32_t, and its result goes straight into an int
start, so `sensor list 4294967295` gives start == -1. The guard
`start >= end` can't catch that: -1 >= end is false for every setting
count, end == 0 included. The loop then runs from a negative index,
where getSettingName() and getSettingValue() return NULL.
The branch sits behind no #if, and the base SensorManager accessors
always return NULL, so a plain repeater with no sensors is affected
too. The command also works over remote admin. The effect depends on
the target: an ESP32-S3 panics (LoadProhibited at address 0) and
reboots, while an nRF52840 hangs until someone resets it by hand.
Add `start < 0` to the guard, so a wrapped start gets the existing
"no custom var" reply. This is the same line as liquidraver/ZephCore#100.
Every sensor manager on dev exposes at most one setting, `gps`, so the
paging loop's fixed 134-byte bound can't overrun today. That bound is
left as it is.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ccff85f to
bbacf47
Compare
sensor list start index and its paging reserve 🤖🤖sensor list start index 🤖🤖

Summary
sensor list <n>passes twoNULLpointers tosprintf("%s=%s\n", ...)for anynin[2147483648, 4294967295]. The command works over remote admin as well as the serial console,and the branch sits behind no
#if, so a plain repeater with no sensors is affected too. This PRis a one-line guard.
Scope reduced on 2026-10-05. The earlier revision also bounded the paging loop against the
reply buffer, clamped the start index while parsing it, and documented the reply format. The
ZephCore owner asked whether the same change was over-engineered there, and it was cut down to
this one line in liquidraver/ZephCore#100,
which has since merged. The same argument holds here. Every sensor manager on
devexposes atmost one setting,
gps, so the paging bound guards nothing that can currently happen. That partis left out, and the "Not fixed here" section below explains it.
The bug
Links are to
devat3e3150c8._atoi()returns
uint32_t, and the resultgoes straight into
int start. Sosensor list 4294967295givesstart == -1, and any valuefrom 2^31 up wraps negative.
if (start >= end).-1 >= endis false for every setting count,end == 0included, so the guard never fires on a wrapped value.
i.getSettingName(i)andgetSettingValue(i)returnNULLthere: the baseSensorManageralways returnsNULL, and every override returns aname only for a matching index.
NULLs go into%s, which is undefined behaviour. On the C libraries these targets linkagainst, it faults rather than printing
(null).What that does on hardware (stock build of the earlier base
cdd04077, serial CLI):Guru Meditation Error: Core 1 panic'ed (LoadProhibited),EXCVADDR: 0x00000000,then a reboot. Any admin client can repeat that remote reset as fast as it can send the command.
nothing, and the 1200-baud DFU touch isn't serviced either. Someone has to reset the board by
hand.
The fix
if (start < 0 || start >= end).A wrapped start now answers
no custom var, the existing reply for a start past the end. It is thesame line as liquidraver/ZephCore#100.
Not fixed here
dp-reply < 134reserves a fixed 26 bytes for the... next:Nmarker. A long row that starts near byte 133 could run past the reply buffer,which is 157 bytes once a wrapper has stripped its 3-byte
xx|prefix. The longest row today isgps=1, so nothing reaches that. If a sensor manager ever lists longer settings, the bound willneed the change the earlier revision of this PR had.
_atoi()itself, sosensor list 4294967296listsfrom index 0. That's harmless (it returns the first page), and
_atoi()is shared with thetimestamp commands, so it is left alone.
Verification
cdd04077sensor list 2147483648,sensor list 4294967295ec05125f(same tree asbbacf47a)sensor list 2147483648,4294967295no custom var, and the board kept answeringsensor list,0,1,2,4294967296,18446744073709551616,99999999999,-1,abcno custom varfor all of them, as ondevwith zero settingsThe board runs a
simple_repeaterbuild, driven over the serial CLI. On the boots tested, its GPSsent nothing during boot detection, so these runs cover the zero-setting path, which is what a
plain repeater has. The negative-start guard doesn't depend on the setting count. The
one-setting listing itself is unchanged from
dev.Builds:
RAK_4631_repeaterandheltec_v4_repeatersucceed.Iteration history, including the earlier revision's off-target harness and hardware tables:
ptr727/meshcore-dev-MeshCore#8.