Skip to content

fix: defensive buffer-write hardening (sprintf, mt_sprintf, NMEA checksum) - #11236

Closed
ndoo wants to merge 1 commit into
meshtastic:developfrom
meshmy:fix/defensive-buffer-write-hardening
Closed

fix: defensive buffer-write hardening (sprintf, mt_sprintf, NMEA checksum)#11236
ndoo wants to merge 1 commit into
meshtastic:developfrom
meshmy:fix/defensive-buffer-write-hardening

Conversation

@ndoo

@ndoo ndoo commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Problem

A full-tree sweep for unsafe buffer writes (as part of an ongoing STM32WL memory-safety audit) found the codebase in good shape overall — this is the small remaining handful of loose ends. None are demonstrated as exploitable today; all are latent landmines worth closing while in this area.

  • src/modules/DropzoneModule.cpp: 4 raw sprintf() calls into fixed-size buffers, safe today only because of current nanopb field-size invariants (no defense-in-depth if those change).
  • src/platform/stm32wl/STM32_LittleFS.cpp: dbg_strerr_lfs's sprintf(errcode, "%ld", err) into a 10-byte buffer — INT32_MIN needs 12 bytes (debug-build-only code path).
  • src/RedirectablePrint.cpp: mt_sprintf() had a strcpy() immediately overwritten by the vsnprintf() right after it — pure dead code, and on an empty format string it underflows, writing 1 byte past a 0-size allocation.
  • src/gps/NMEAWPL.cpp: printWPL()/printGGA() use snprintf()'s return value (which can exceed bufsz on truncation) directly as an offset/size for a second snprintf() call and a checksum loop — both need the length clamped first, or bufsz-len underflows into a huge size_t. Safe today only because of current field-size bounds.

Fix

  • Swap the remaining raw sprintf() calls for snprintf().
  • Remove the dead/unsafe strcpy() in mt_sprintf()vsnprintf() alone already produces the correct result.
  • Clamp len to bufsz (or 0) after each snprintf() call in NMEAWPL.cpp before using it as an offset or in the checksum loop.

Test plan

  • pio run -e wio-e5 / pio run -e rak3172 — build clean, confirms all four touched files actually compile on the memory-constrained target these were audited for.
  • Hardware (wio-e5): flashed, confirmed clean boot.
  • None of the specific overflow conditions were reproduced live (all require values outside current field-size invariants to trigger) — this is defensive hardening, not a fix for a demonstrated live bug.

🤝 Attestations

  • I have tested that my proposed changes behave as described.
  • I have tested that my proposed changes do not cause any obvious regressions on the following devices:
    • Heltec (Lora32) V3
    • LilyGo T-Deck
    • LilyGo T-Beam
    • RAK WisBlock 4631
    • Seeed Studio T-1000E tracker card
    • Other (please specify below): wio-e5 — build + hardware verified per test plan above. rak3172 build-verified only.

…ksum)

None of these are demonstrated as exploitable today, but are latent
landmines worth closing while auditing this area:

- DropzoneModule.cpp: 4 raw sprintf() calls into fixed-size buffers,
  safe today only because of current nanopb field-size invariants.
  Switched to snprintf().
- STM32_LittleFS.cpp: dbg_strerr_lfs's sprintf("%ld", err) into a
  10-byte buffer - INT32_MIN needs 12. Switched to snprintf() into a
  13-byte buffer.
- RedirectablePrint.cpp: mt_sprintf() had a dead strcpy() (immediately
  overwritten by the vsnprintf() right after) that underflows on an
  empty format string, writing 1 byte past a 0-size allocation. Removed
  it - vsnprintf() alone already produces the correct result.
- NMEAWPL.cpp: printWPL()/printGGA() use snprintf()'s return value
  (which can exceed bufsz on truncation) directly as an offset/size for
  a second snprintf() call and a checksum loop, both of which need it
  clamped first or bufsz-len underflows into a huge size_t. Safe today
  only because of current field-size bounds; clamped defensively.

Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Andrew Yong <me@ndoo.sg>
@github-actions

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Note

Building this pull request… the flash button, badges and supported-board
list will appear here automatically once CI finishes.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b9400ee-d5cd-4364-984e-2946bdb85326

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

caveman99 added a commit that referenced this pull request Jul 30, 2026
snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>
@caveman99

Copy link
Copy Markdown
Member

Thanks for this @ndoo, and sorry to close it out from under you.

#11293 started as the NMEA checksum offset fix from #11242, which lands on the same three lines in NMEAWPL.cpp that this PR clamps. CodeRabbit then raised your truncation finding there too. Rather than have two PRs editing the same statements and conflicting with each other, I pulled your remaining hunks into #11293: the two sprintf calls in sendConditions(), the dead strcpy in mt_sprintf(), and the errcode buffer size. That keeps it to one CI matrix run instead of two, and avoids whichever merged second having to rebase through the other.

You are credited as co-author on the commit. The two matchCompare sprintf calls you flagged in DropzoneModule.cpp were already converted in #11286, so only the two in sendConditions() were still outstanding.

Your analysis was right on every point, including the empty-format case where n = fmt_str.size() * 2 is 0 and the strcpy writes past a zero-size allocation.

@caveman99

Copy link
Copy Markdown
Member

Superseded by #11293, which carries these changes with @ndoo credited as co-author.

@caveman99 caveman99 closed this Jul 30, 2026
@ndoo

ndoo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for this @ndoo, and sorry to close it out from under you.

#11293 started as the NMEA checksum offset fix from #11242, which lands on the same three lines in NMEAWPL.cpp that this PR clamps. CodeRabbit then raised your truncation finding there too. Rather than have two PRs editing the same statements and conflicting with each other, I pulled your remaining hunks into #11293: the two sprintf calls in sendConditions(), the dead strcpy in mt_sprintf(), and the errcode buffer size. That keeps it to one CI matrix run instead of two, and avoids whichever merged second having to rebase through the other.

You are credited as co-author on the commit. The two matchCompare sprintf calls you flagged in DropzoneModule.cpp were already converted in #11286, so only the two in sendConditions() were still outstanding.

Your analysis was right on every point, including the empty-format case where n = fmt_str.size() * 2 is 0 and the strcpy writes past a zero-size allocation.

No worries. Tbh most of the groundwork was Claude analysis. Nonetheless just happy to see the fix ship. Cheers

caveman99 added a commit that referenced this pull request Jul 30, 2026
snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>
caveman99 added a commit that referenced this pull request Jul 30, 2026
snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>
caveman99 added a commit that referenced this pull request Jul 31, 2026
snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of #11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>
NomDeTom pushed a commit to NomDeTom/MeshtasticFirmware that referenced this pull request Jul 31, 2026
…eshtastic#11293)

* Checksum NMEA sentences from the $ delimiter

The PositionLite printWPL() format begins with a CRLF, so the fixed start offset of 1 folded the newline and the $ into the checksum and every sentence went out with a wrong value. Locate the $ instead and stop at the terminator or a \*.

* Clamp truncated writes and harden the remaining fixed buffers

snprintf returns the length it would have written, so a truncated NMEA sentence
made buf + len point past the buffer and bufsz - len underflow into a huge size
for the checksum append. Clamp after each write.

Also pulls in the rest of meshtastic#11236: the two remaining Dropzone sprintf calls, the
dead strcpy in mt_sprintf that wrote one byte past a zero-size allocation for an
empty format, and the 10-byte errcode buffer that INT32_MIN overflows.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Bail out on a zero-sized buffer and cast err for %ld

snprintf writes nothing at all when bufsz is 0, not even a terminator, so the
checksum helper would run strchr over whatever the buffer already held. Return
before touching it.

int32_t is not long on every target, so cast before formatting with %ld.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Add NMEA sentence regression tests

Covers checksum computation from the $ delimiter for both printWPL
overloads and printGGA, zero-sized buffers, and truncated buffers down
to one byte.

Co-Authored-By: Andrew Yong <me@ndoo.sg>

* Tighten checksum parsing and pin the WPL fixture checksum

Require exactly two hex digits followed by the sentence terminator, and
assert both WPL overloads against a known checksum instead of comparing
them to each other.

* Bump native suite count to 43

---------

Co-authored-by: Andrew Yong <me@ndoo.sg>
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