← Back to Security Research Portal (ccsimplyspolit.github.io)

Anatomy of Unsigned Integer Underflows (CWE-191) & Sticky-State Stack Overflows (CWE-121) in C/C++ Parsers

Author: Serhii Shunko (@sshunko / @ccsimplyspolit) • ORCID: 0009-0002-4183-0361 • Category: Memory Safety & Static Analysis

0x00 // Why Staged Validation Fails in Binary Parsers

Binary telemetry and embedded radio protocols frequently separate structural frame validation (Pass 1) from semantic payload extraction (Pass 2). A recurring vulnerability pattern arises when Pass 1 validates container boundaries (such as 32-bit KLV alignment or maximum hop-path byte length), and Pass 2 assumes that inner sub-headers or sticky state transitions are implicitly bounded by Pass 1.

0x01 // Pattern 1: Unsigned Remainder Wraparound (uint8_t rem = len - i, CWE-191)

In embedded packet routers, outer frame parsers verify that the variable-length routing path fits inside the received radio frame:

// Pass 1 (Frame Demux): Accepts any packet where i + path_byte_len <= len
// Consequently, pkt->payload_len = len - i can legitimately be 0..8 bytes.
pkt->payload_len = len - i;

When a specialized handler in Pass 2 consumes a fixed 9-byte sub-header without re-verifying pkt->payload_len ≥ 9, subtracting the cursor i = 9 in 8-bit unsigned arithmetic wraps modulo 256:

Memory / Register State Visualization (pkt->payload_len = 0, i = 9):
+-------------------------------------------------------------------+
| uint8_t i          = 0x09 (9 bytes of sub-header read)            |
| uint16_t pay_len   = 0x0000                                       |
| uint8_t rem        = (uint8_t)(pay_len - i) = 0xF7 (247 bytes!)   |
+-------------------------------------------------------------------+
| Packet::payload[184] buffer boundary:                             |
| [0x00 .. 0x08: trace header] [0x09 .. 0xB7: payload] [0xB8.. OOB] |
|                              |<----- rem = 247 reads to 0x100 --->|
+-------------------------------------------------------------------+

Defensive Rule: Never compute a remaining length via subtraction (len - i) before asserting i ≤ len, and never check multi-byte lookaheads via offset + step_size ≤ len using narrow unsigned integers that can wrap.

0x02 // Pattern 2: Cross-Record Sticky State Desynchronization (CWE-121)

In stream-oriented KLV telemetry parsers, calibration metadata records persist in the parser context across subsequent data records. Even if each KLV record is 32-bit aligned and fits within the file buffer, a state-dependent transformation in Pass 2 that computes a Cartesian product strlen(orin) * strlen(orio) into a fixed 64-element stack matrix double mtrx_buffer[64] must enforce:

if (in_len != out_len || in_len == 0 || in_len > 8 || in_len * out_len > 64) {
    return GPMF_ERROR_BAD_STRUCTURE;
}

0x03 // Pattern 3: Zero-Count Group Underflow & 64-Bit Varint Offset Arithmetic

In binary framing and base-encoding helpers, two additional boundary conditions frequently arise when zero-length payloads or 64-bit varint offsets interact with unsigned arithmetic:

// Defensive Pattern A: Guard (nGroups - 1) when input length is 0
size_t nGroups = (input_len + 2) / 3;
size_t out_required = 1 + nGroups * 4 + (
    (line_break_str != nullptr && nGroups > 0)
        ? strlen(line_break_str) * (1 + (nGroups - 1) / kGroupsPerLine)
        : 0
);

// Defensive Pattern B: Guard 64-bit unsigned varint delta addition against INT64_MAX wrap
if (raw_delta > static_cast<uint64_t>(INT64_MAX - base_seq_num)) {
    return false; // Reject overflowing sequence offset before state update
}