Skip to content

Memory fixes - #281

Merged
TMRh20 merged 1 commit into
masterfrom
MemFixes
Oct 29, 2026
Merged

TMRh20 merged 1 commit into
masterfrom
MemFixes

Conversation

@TMRh20

@TMRh20 TMRh20 commented Oct 29, 2026

Copy link
Copy Markdown
Member

Doing some more work with Claude this AM: (sorry/not sorry in advance @2bndy5 )

  1. ESBNetwork::read() (non-Linux path, RF24Network.cpp:645-695) explicitly assumes a frame is queued and the
    available() check is commented out (:651). If a sketch calls read() without a frame queued — easy to do,
    and some of your own examples call read() right after update() in a loop — then:
  • bufsize is read from stale bytes at frame_queue + 8 (:673)
  • next_frame -= bufsize + 10 pushes next_frame below frame_queue (:682)
  • memmove(frame_queue, frame_queue + bufsize + 10 + padding, sizeof(frame_queue) - bufsize) (:690) — sizeof(frame_queue) - bufsize underflows to a huge size_t.

On a fresh node bufsize is 0 so it's benign, but once any frame has passed through, the leftover header
makes bufsize non-zero and next_frame walks off the front of the buffer; a later received frame is then
written out of bounds. AddressSanitizer reported negative-size-param in the memmove at :690, and a
corrupted next_frame that reads before frame_queue.

  1. In enqueue() the final copy of a reassembled frame into the user queue (RF24Network.cpp:503-506) gates
    on:

if ((uint16_t)(MAX_PAYLOAD_SIZE) - (next_frame - frame_queue) >= frag_queue.message_size) {

Two concerns:

  • frame_queue is MAIN_BUFFER_SIZE (MAX_PAYLOAD_SIZE + FRAME_HEADER_SIZE), so the limit is understated by FRAME_HEADER_SIZE here but the unfragmented path at :549-550 correctly uses MAIN_BUFFER_SIZE. Inconsistent.
  • More importantly, MAX_PAYLOAD_SIZE - (next_frame - frame_queue) is unsigned subtraction. If next_frame is ever past that point (which Finding 1 or a full queue can cause), it underflows and the check passes, allowing the memcpy at :504-505 to overflow frame_queue.

ESBNetwork::read() (non-Linux path, RF24Network.cpp:645-695) explicitly assumes a frame is queued and the
  available() check is commented out (:651). If a sketch calls read() without a frame queued — easy to do,
  and some of your own examples call read() right after update() in a loop — then:

  - bufsize is read from stale bytes at frame_queue + 8 (:673)
  - next_frame -= bufsize + 10 pushes next_frame below frame_queue (:682)
  - memmove(frame_queue, frame_queue + bufsize + 10 + padding, sizeof(frame_queue) - bufsize) (:690) —
    sizeof(frame_queue) - bufsize underflows to a huge size_t.

  On a fresh node bufsize is 0 so it's benign, but once any frame has passed through, the leftover header
  makes bufsize non-zero and next_frame walks off the front of the buffer; a later received frame is then
  written out of bounds. AddressSanitizer reported negative-size-param in the memmove at :690, and a
  corrupted next_frame that reads before frame_queue.

In enqueue() the final copy of a reassembled frame into the user queue (RF24Network.cpp:503-506) gates
  on:

  if ((uint16_t)(MAX_PAYLOAD_SIZE) - (next_frame - frame_queue) >= frag_queue.message_size) {

  Two concerns:
  - frame_queue is MAIN_BUFFER_SIZE (MAX_PAYLOAD_SIZE + FRAME_HEADER_SIZE), so the limit is understated by
    FRAME_HEADER_SIZE here but the unfragmented path at :549-550 correctly uses MAIN_BUFFER_SIZE.
    Inconsistent.
  - More importantly, MAX_PAYLOAD_SIZE - (next_frame - frame_queue) is unsigned subtraction. If next_frame
    is ever past that point (which Finding 1 or a full queue can cause), it underflows and the check
    passes, allowing the memcpy at :504-505 to overflow frame_queue.
@TMRh20
TMRh20 requested a review from 2bndy5 October 29, 2026 10:06
@TMRh20
TMRh20 merged commit eaea2f1 into master Oct 29, 2026
22 checks passed
@TMRh20
TMRh20 deleted the MemFixes branch October 29, 2026 23:49
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