Skip to content

Reject a negative sensor list start index 🤖🤖 - #3433

Open
ptr727 wants to merge 1 commit into
meshcore-dev:devfrom
ptr727:fix/sensor-list-paging
Open

ptr727 wants to merge 1 commit into
meshcore-dev:devfrom
ptr727:fix/sensor-list-paging

Conversation

@ptr727

@ptr727 ptr727 commented Sep 17, 2026 •

Copy link
Copy Markdown

Summary

sensor list <n> passes two NULL pointers to sprintf("%s=%s\n", ...) for any n in
[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 PR
is 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 dev exposes at
most one setting, gps, so the paging bound guards nothing that can currently happen. That part
is left out, and the "Not fixed here" section below explains it.

The bug

Links are to dev at 3e3150c8.

  1. _atoi()
    returns uint32_t, and the result
    goes straight into int start. So sensor list 4294967295 gives start == -1, and any value
    from 2^31 up wraps negative.
  2. The guard is if (start >= end). -1 >= end is false for every setting count, end == 0
    included, so the guard never fires on a wrapped value.
  3. The loop then starts at a negative i. getSettingName(i) and getSettingValue(i) return
    NULL there: the base SensorManager always returns NULL, and every override returns a
    name only for a matching index.
  4. Both NULLs go into %s, which is undefined behaviour. On the C libraries these targets link
    against, it faults rather than printing (null).

What that does on hardware (stock build of the earlier base cdd04077, serial CLI):

  • ESP32-S3: 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.
  • nRF52840: no reboot. The firmware hangs permanently. USB CDC stays enumerated but answers
    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 the
same line as liquidraver/ZephCore#100.

Not fixed here

  • The paging bound. The loop guard dp-reply < 134 reserves a fixed 26 bytes for the
    ... next:N marker. 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 is
    gps=1, so nothing reaches that. If a sensor manager ever lists longer settings, the bound will
    need the change the earlier revision of this PR had.
  • A digit string past 2^32 wraps inside _atoi() itself, so sensor list 4294967296 lists
    from index 0. That's harmless (it returns the first page), and _atoi() is shared with the
    timestamp commands, so it is left alone.

Verification

Build Board Commands Result
stock, on the earlier base cdd04077 ESP32-S3, nRF52840 (GPS detected, one setting) sensor list 2147483648, sensor list 4294967295 panic (ESP32-S3), hang (nRF52840)
this PR, built from ec05125f (same tree as bbacf47a) nRF52840, RAK4631 (GPS not detected at boot, zero settings) sensor list 2147483648, 4294967295 no custom var, and the board kept answering
same same sensor list, 0, 1, 2, 4294967296, 18446744073709551616, 99999999999, -1, abc no custom var for all of them, as on dev with zero settings

The board runs a simple_repeater build, driven over the serial CLI. On the boots tested, its GPS
sent 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_repeater and heltec_v4_repeater succeed.

Iteration history, including the earlier revision's off-target harness and hardware tables:
ptr727/meshcore-dev-MeshCore#8.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 clamp start during parsing (avoids _atoi() wrap and negative int start values).
  • Make sensor list output safer via snprintf, 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.

Comment thread src/helpers/CommonCLI.cpp Outdated
ptr727 added a commit to ptr727/meshcore-dev-MeshCore that referenced this pull request Sep 17, 2026
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>
@ptr727
ptr727 force-pushed the fix/sensor-list-paging branch from 508e65b to ccff85f Compare September 17, 2026 14:49
@ptr727
ptr727 requested a lite review from Copilot September 17, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

ptr727 added a commit to ptr727/liquidraver-ZephCore that referenced this pull request Oct 5, 2026
`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>
@ptr727
ptr727 force-pushed the fix/sensor-list-paging branch from ccff85f to bbacf47 Compare October 5, 2026 20:48
@ptr727 ptr727 changed the title Bound the sensor list start index and its paging reserve 🤖🤖 Reject a negative sensor list start index 🤖🤖 Oct 5, 2026
@ptr727
ptr727 requested a lite review from Copilot October 5, 2026 20:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Copilot was unable to run its full agentic suite in this review.

Copilot review overview

Review effort: Lite
Findings: 1 Medium severity

Open (1)

Comment thread src/helpers/CommonCLI.cpp

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants