Skip to content

Fix the NMEA checksum offset and harden the buffer writes around it - #11293

Merged
caveman99 merged 6 commits into
developfrom
fix-nmea-checksum
Jul 31, 2026
Merged

Fix the NMEA checksum offset and harden the buffer writes around it#11293
caveman99 merged 6 commits into
developfrom
fix-nmea-checksum

Conversation

@caveman99

@caveman99 caveman99 commented Jul 30, 2026

Copy link
Copy Markdown
Member

Fixes #11242. Supersedes #11236, whose changes are included here with credit to @ndoo.

fd98e9f put a leading CRLF in the PositionLite printWPL() format string, but the checksum loop still started at a fixed offset of 1, so it folded the newline and the $ into the XOR and those sentences carried a wrong checksum. nmeaChecksum() locates the $ and accumulates to the terminator or a \*. Verified against a reference implementation: the CRLF-prefixed sentence changes from 0x42 to 0x6C, matching the same sentence without the prefix, and the other printWPL() and printGGA() are byte-identical to before.

Reading to the terminator also keeps the checksum in bounds when snprintf truncated. The other half of that, raised by CodeRabbit on this PR and originally by #11236, is the append itself: buf + len points past the buffer and bufsz - len underflows when len >= bufsz. nmeaClamp() now bounds the length after each write. Checked at bufsz 128, 40, 20, 8, 2 and 1 with a sentinel-filled buffer: nothing is written past bufsz and the returned length always stays in bounds.

Remaining pieces from #11236:

  • the two sprintf calls left in DropzoneModule::sendConditions()
  • the dead strcpy in mt_sprintf(), immediately overwritten by the following vsnprintf and writing one byte past a zero-size allocation for an empty format string
  • dbg_strerr_lfs's 10-byte errcode, which INT32_MIN needs 12 bytes for

Summary by CodeRabbit

  • Bug Fixes
    • Improved NMEA WPL/GGA sentence building to safely handle zero-length and truncated destination buffers, with clamped offsets and consistent checksum generation.
    • Hardened formatted status/debug strings to use bounded formatting and safer unknown-error handling.
  • Refactor / Code Quality
    • Reduced unnecessary format-string copying when producing formatted output.
  • Tests
    • Expanded NMEA WPL checksum validation (including stricter hex parsing and fixed expected checksum behavior) and updated the native suite count.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

NMEA WPL and GGA builders now guard zero-sized buffers, clamp sentence offsets, and use the shared checksum helper. Tests cover checksums, CRLF handling, truncation, and empty buffers. Dropzone and LittleFS formatting use bounded writes, while RedirectablePrint removes an unnecessary format-string copy.

Changes

Buffer safety hardening

Layer / File(s) Summary
NMEA sentence bounds
src/gps/NMEAWPL.cpp
Adds nmeaClamp(), handles zero-sized buffers, and bounds lengths during WPL and GGA checksum and trailer construction.
NMEA output validation
test/test_nmea_wpl/test_main.cpp, test/native-suite-count
Adds Unity coverage for checksums, CRLF behavior, zero-sized buffers, truncation, and updates the native suite count.
Bounded formatting paths
src/RedirectablePrint.cpp, src/modules/DropzoneModule.cpp, src/platform/stm32wl/STM32_LittleFS.cpp
Removes an unnecessary format-string copy and replaces unbounded formatting with bounded writes in Dropzone and LittleFS paths.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: needs-review

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits the repository's required attestation checklist and template structure. Replace the tips block with the repo template, add the attestations checklist, and include the required testing/device details.
Out of Scope Changes check ⚠️ Warning The PR also changes unrelated cleanup files not needed to satisfy #11242. Split the RedirectablePrint, DropzoneModule, and STM32_LittleFS cleanup into separate PRs unless they are covered by a linked issue.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main NMEA checksum fix and the added buffer-safety hardening.
Linked Issues check ✅ Passed The checksum fix dynamically finds '$' and excludes terminators, matching #11242's requirements.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-nmea-checksum

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gps/NMEAWPL.cpp`:
- Around line 42-43: Update the NMEA formatting paths around nmeaChecksum and
the corresponding writes at lines 51-56 and 93-101 to check each snprintf return
value before advancing len or appending the checksum. Detect negative results
and truncation (return value meeting or exceeding the remaining capacity), then
return the existing truncation/error result; only compute buf + len and bufsz -
len after confirming the prior write fit.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e6fb466f-0c2f-4494-ab26-6d721645dbb2

📥 Commits

Reviewing files that changed from the base of the PR and between 9a37250 and d0ceb8f.

📒 Files selected for processing (1)
  • src/gps/NMEAWPL.cpp

Comment thread src/gps/NMEAWPL.cpp Outdated
@caveman99 caveman99 changed the title Checksum NMEA sentences from the $ delimiter Fix the NMEA checksum offset and harden the buffer writes around it Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/gps/NMEAWPL.cpp`:
- Around line 49-51: Add an early return for bufsz == 0 before body formatting
in both printWPL() overloads and printGGA() in src/gps/NMEAWPL.cpp, preventing
nmeaChecksum from reading an uninitialized buffer; preserve existing formatting
behavior for nonzero buffer sizes.

In `@src/platform/stm32wl/STM32_LittleFS.cpp`:
- Around line 275-276: Update the errcode formatting in the surrounding STM32
LittleFS error-handling code to use a type-correct portable representation for
int32_t: include the appropriate integer-format header and format err with
PRId32, or explicitly cast it to long while retaining %ld. Preserve the existing
errcode buffer and snprintf behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a119e11-547c-4dc0-b53c-df87f2877617

📥 Commits

Reviewing files that changed from the base of the PR and between d0ceb8f and 1acc75c.

📒 Files selected for processing (4)
  • src/RedirectablePrint.cpp
  • src/gps/NMEAWPL.cpp
  • src/modules/DropzoneModule.cpp
  • src/platform/stm32wl/STM32_LittleFS.cpp
💤 Files with no reviewable changes (1)
  • src/RedirectablePrint.cpp

Comment thread src/gps/NMEAWPL.cpp
Comment thread src/platform/stm32wl/STM32_LittleFS.cpp Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/gps/NMEAWPL.cpp (1)

43-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression coverage for buffer-boundary cases.

Please test zero-sized buffers, one-byte/truncated buffers, CRLF-prefixed WPL sentences, and checksum exclusion of trailing */line endings.

Also applies to: 60-71, 98-120

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/gps/NMEAWPL.cpp` around lines 43 - 54, Extend regression coverage for the
WPL formatting functions around the zero-size and truncated-buffer paths,
including one-byte buffers, CRLF-prefixed sentences, and checksum validation
that excludes the trailing '*' and CRLF. Cover both relevant WPL formatting
implementations identified by the shared ranges, and verify safe output length
handling without buffer overrun.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/gps/NMEAWPL.cpp`:
- Around line 43-54: Extend regression coverage for the WPL formatting functions
around the zero-size and truncated-buffer paths, including one-byte buffers,
CRLF-prefixed sentences, and checksum validation that excludes the trailing '*'
and CRLF. Cover both relevant WPL formatting implementations identified by the
shared ranges, and verify safe output length handling without buffer overrun.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 505de7ba-a0fc-46e7-8fb2-f961c7702446

📥 Commits

Reviewing files that changed from the base of the PR and between 1acc75c and 18b7400.

📒 Files selected for processing (2)
  • src/gps/NMEAWPL.cpp
  • src/platform/stm32wl/STM32_LittleFS.cpp

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

⚡ Try this PR in the Web Flasher

Flash this PR in the Web Flasher

firmware commit boards expires

Warning

This is an automated, unreviewed CI test build. Back up your device configuration
before flashing, and only flash devices you are able to recover.

Supported boards built by this PR (31)
Device Board Platform
Crowpanel Adv 3.5 TFT elecrow-adv-35-tft esp32-s3
Heltec HT62 heltec-ht62-esp32c3-sx1262 esp32-c3
Heltec Mesh Node 096 heltec-mesh-node-t096 nrf52840
Heltec Mesh Node T1 heltec-mesh-node-t1 nrf52840
Heltec Mesh Node T114 heltec-mesh-node-t114 nrf52840
Heltec V3 heltec-v3 esp32-s3
Heltec V4 heltec-v4 esp32-s3
Meshnology W10 meshnology_w10 esp32-s3
Meshnology W12 meshnology_w12 esp32-s3
Raspberry Pi Pico pico rp2040
Raspberry Pi Pico W picow rp2040
RAK WisMesh Pocket V3 rak_wismesh_pocket nrf52840
RAK WisMesh Pod rak_wismesh_pod nrf52840
RAK WisMesh Repeater Mini V2 rak_wismesh_repeater_mini nrf52840
RAK WisMesh Tag rak_wismeshtag nrf52840
RAK WisBlock 11200 rak11200 esp32
RAK WisBlock 11310 rak11310 rp2040
RAK3312 rak3312 esp32-s3
RAK WisBlock 4631 rak4631 nrf52840
Seeed SenseCAP Mesh-Tracker-X1 seeed_mesh_tracker_X1 nrf52840
Seeed Wio Tracker L1 seeed_wio_tracker_L1 nrf52840
Seeed Xiao NRF52840 Kit seeed_xiao_nrf52840_kit nrf52840
Seeed Xiao ESP32-S3 seeed-xiao-s3 esp32-s3
Station G2 station-g2 esp32-s3
Station G3 station-g3 esp32-s3
LILYGO T-Deck t-deck-tft esp32-s3
LILYGO T-Echo t-echo nrf52840
LILYGO T-Echo Plus t-echo-plus nrf52840
LILYGO T-Impulse Plus t-impulse-plus nrf52840
LilyGo T3-C6 tlora-c6 esp32-c6
Seeed SenseCAP T1000-E tracker-t1000-e nrf52840

Build artifacts expire on 2026-08-29. Updated for 6eabbfb.

@caveman99 caveman99 added the bugfix Pull request that fixes bugs label Jul 30, 2026
@caveman99

Copy link
Copy Markdown
Member Author

Added test/test_nmea_wpl covering the requested cases: checksum computed from the $ delimiter for both printWPL overloads and printGGA, CRLF-prefixed sentences, zero-sized buffers, and truncated buffers down to one byte.

Verified the tests discriminate. Against the pre-fix NMEAWPL.cpp, 4 of the 6 fail:

test_wpl_lite_checksum_skips_leading_crlf: Expected 105 Was 71
test_crlf_prefix_does_not_change_checksum: Expected 105 Was 71
test_zero_sized_buffer_writes_nothing: Expected 0 Was 43
test_truncated_buffers_stay_in_bounds: Expected TRUE Was FALSE

105 ^ 71 is 0x2E, the newline and $ the old loop included. All 6 pass on this branch.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
test/test_nmea_wpl/test_main.cpp (1)

59-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use camelCase for the new test functions.

Rename the six test_* functions and their RUN_TEST references to camelCase equivalents. As per coding guidelines, C++ functions must use camelCase.

Also applies to: 72-72, 83-83, 94-94, 107-107, 122-122, 142-147

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_nmea_wpl/test_main.cpp` at line 59, Rename all six newly added test
functions in test_main.cpp from snake_case to camelCase, including
test_wpl_lite_checksum_skips_leading_crlf and the functions at the referenced
locations, and update each corresponding RUN_TEST reference to use the renamed
symbol.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/test_nmea_wpl/test_main.cpp`:
- Around line 45-51: Update emittedChecksum to require exactly two hexadecimal
characters after the asterisk, validating both characters directly and
confirming the next character is the sentence terminator or NUL; retain the
existing assertion that the asterisk is present and return the parsed checksum
value.
- Around line 94-105: Update test_crlf_prefix_does_not_change_checksum to
isolate the CRLF behavior from overload and fixture differences: generate the
prefixed and unprefixed outputs from the same position type and equivalent
fixture, then compare their sentence bodies or validate each against a known
expected checksum rather than comparing only checksums from PositionLite and
Position.

---

Nitpick comments:
In `@test/test_nmea_wpl/test_main.cpp`:
- Line 59: Rename all six newly added test functions in test_main.cpp from
snake_case to camelCase, including test_wpl_lite_checksum_skips_leading_crlf and
the functions at the referenced locations, and update each corresponding
RUN_TEST reference to use the renamed symbol.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f48a7e2f-d581-458b-aec2-0ac9e1d1c53f

📥 Commits

Reviewing files that changed from the base of the PR and between 18b7400 and a4d386c.

📒 Files selected for processing (2)
  • test/native-suite-count
  • test/test_nmea_wpl/test_main.cpp

Comment thread test/test_nmea_wpl/test_main.cpp
Comment on lines +94 to +105
void test_crlf_prefix_does_not_change_checksum(void)
{
char withPrefix[128];
char withoutPrefix[128];
meshtastic_PositionLite lite = makePositionLite();
meshtastic_Position pos = makePosition();

printWPL(withPrefix, sizeof(withPrefix), lite, "Test", false);
printWPL(withoutPrefix, sizeof(withoutPrefix), pos, "Test", false);

TEST_ASSERT_EQUAL_UINT32(emittedChecksum(withoutPrefix), emittedChecksum(withPrefix));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Isolate the CRLF regression from overload differences.

This test compares PositionLite and Position overloads with different fixtures, then compares only their 8-bit checksums. A formatting difference between overloads—or an XOR collision—can make the result misleading. Compare the same sentence body with and without the prefix, or assert each output against a known expected checksum.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/test_nmea_wpl/test_main.cpp` around lines 94 - 105, Update
test_crlf_prefix_does_not_change_checksum to isolate the CRLF behavior from
overload and fixture differences: generate the prefixed and unprefixed outputs
from the same position type and equivalent fixture, then compare their sentence
bodies or validate each against a known expected checksum rather than comparing
only checksums from PositionLite and Position.

@caveman99

Copy link
Copy Markdown
Member Author

Both addressed in b016dff.

Checksum parsing now requires exactly two hex digits followed by CR or NUL, so *A and *A5X no longer pass.

test_crlf_prefix_does_not_change_checksum now asserts each overload against a known checksum (0x69) rather than comparing the two to each other, which removes the overload-difference and XOR-collision concerns.

Re-verified discrimination against the pre-fix NMEAWPL.cpp: 4 of 6 fail, including the reworked test at 0x69 vs 71. All 6 pass on this branch.

On the camelCase suggestion, declining to keep the surrounding convention: the test tree is 271 snake_case test functions to 1 camelCase.

@caveman99
caveman99 force-pushed the fix-nmea-checksum branch from b016dff to 6838e00 Compare July 30, 2026 13:58
@caveman99
caveman99 enabled auto-merge July 30, 2026 14:16
@caveman99
caveman99 force-pushed the fix-nmea-checksum branch from 6838e00 to c148d99 Compare July 30, 2026 14:17
@ndoo

ndoo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Ha, scope crept into STM32 LittleFS? Though it certainly should be fixed regardless of which PR it lands in.

@caveman99

Copy link
Copy Markdown
Member Author

Fair cop. It came out of the same sweep: the NMEA fix was a checksum offset plus an unbounded write, and grepping for the same pattern turned up the remaining fixed-size buffers written with sprintf, STM32 LittleFS and DropzoneModule among them. They rode along in the "harden the remaining fixed buffers" commit rather than becoming three more PRs against a busy CI queue.

Each one is its own commit, so if you would rather see the STM32 change land separately it splits out cleanly. No objection either way.

@ndoo

ndoo commented Jul 30, 2026 via email

Copy link
Copy Markdown
Contributor

@caveman99

Copy link
Copy Markdown
Member Author

Checked, and you are clear: no file overlap. #11230 touches littlefs/lfs_util.h, main-stm32wl.cpp and variants/stm32/stm32.ini, while this one only touches STM32_LittleFS.cpp. Neither should conflict with the other whichever lands first.

Leaving it here then, since it is self-contained.

@caveman99
caveman99 added this pull request to the merge queue Jul 30, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 31, 2026
caveman99 and others added 6 commits July 31, 2026 09:41
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 \*.
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>
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>
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>
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.
@caveman99
caveman99 force-pushed the fix-nmea-checksum branch from 6eabbfb to 4bdc54c Compare July 31, 2026 07:41
@caveman99
caveman99 added this pull request to the merge queue Jul 31, 2026
Merged via the queue into develop with commit 6367132 Jul 31, 2026
56 checks passed
NomDeTom added a commit to NomDeTom/MeshtasticFirmware that referenced this pull request Aug 1, 2026
Upstream meshtastic#11293 added test_nmea_wpl and took develop's count to 43; this branch
had independently reached 46. Merging develop resolved the counter textually,
keeping 46, while the directory set became the union of both sides at 47.

The suite-count CI gate fails on the mismatch, and it gates the native test jobs,
so the tests themselves were being skipped.
NomDeTom added a commit to NomDeTom/MeshtasticFirmware that referenced this pull request Aug 1, 2026
Upstream meshtastic#11293 added test_nmea_wpl and took develop's count to 43; this branch
had independently reached 46. Merging develop resolved the counter textually,
keeping 46, while the directory set became the union of both sides at 47.

The suite-count CI gate fails on the mismatch, and it gates the native test jobs,
so the tests themselves were being skipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Pull request that fixes bugs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect NMEA Checksum

2 participants