Bounded Binary Frame Parsing in Embedded C: An Allocation-Free Architecture Note
External bus frames are untrusted. This architecture note covers a static-buffer, allocation-free parser with length and CRC checks before internal state is updated.
25 Jan 2026, 05:01 UTC

The Problem: Untrusted Bytes on a Deterministic System
When an embedded device receives binary frames from an external bus—UART, SPI, CAN, or a custom link—those bytes arrive with no guarantees. A length field might claim 65,535 bytes when the buffer holds 128. A cyclic redundancy check (CRC) trailer might be missing. A malformed frame might arrive mid-stream after a previous frame was truncated. If the parser calls malloc or trusts the length header, the system loses determinism: latency spikes, fragmentation accumulates, and a single bad frame can corrupt internal state or crash the device.
The useful takeaway: parse external frames with a static buffer and a small state machine, validate length and CRC before any data enters internal state, and keep the parser allocation-free so worst-case timing remains bounded.
Requirements
- Deterministic memory: No heap allocation in the parse path. Fixed maximum frame size known at compile time.
- Bounded work: A hard cap on fields and frame length so malformed input cannot force unbounded loops or stack growth.
- Trust boundary: Bytes from the bus are untrusted until length and integrity checks pass.
- Portability: Fixed-width types and explicit endianness handling; no raw
memcpyinto packed structs.
Smallest Suitable Design
The smallest design that meets these requirements is a static byte buffer plus a byte-oriented state machine. The machine reads a fixed-size header containing a length field, validates that length against a compile-time maximum, accumulates payload bytes, checks a trailing CRC, and only then decodes fields into a separate validated struct.
Illustrative frame layout and buffer definition:
/* Compile-time limits */
#define MAX_FRAME_LEN 128u
#define MAX_PAYLOAD_LEN (MAX_FRAME_LEN - HEADER_LEN - CRC_LEN)
/* Untrusted input buffer */
static uint8_t rx_buf[MAX_FRAME_LEN];
/* Validated output */
typedef struct {
uint16_t field_a;
uint8_t field_b;
/* explicit fields, no padding reliance */
} validated_frame_t;
The state machine has four states: READ_HEADER, READ_PAYLOAD, READ_CRC, and VALIDATE. Each incoming byte advances the machine. If the length byte exceeds MAX_PAYLOAD_LEN, the machine transitions to RESYNC rather than continuing.
Length and Integrity Checks
Before accepting any payload byte, compare the received length against MAX_PAYLOAD_LEN. After the final payload byte, compute CRC over header and payload and compare it to the received CRC. Only a match permits field extraction. Endianness conversion uses explicit helpers—shift-and-mask or le16toh-style functions—so the parser behaves identically across big- and little-endian targets.
Trust and Data Boundaries
The only trusted data are bytes that survive length validation and CRC verification. External bytes must never become pointers, array indices, loop bounds, or enum values without range validation. The parser writes into a validated_frame_t that is separate from the raw buffer. Internal state consumers read only this validated struct, never rx_buf directly.
This separation matters because a CRC detects accidental corruption, not malicious forgery. If the threat model includes an active attacker, CRC alone is insufficient; add authentication and consider an encrypted transport. This architecture assumes accidental corruption from bus noise, not adversarial input.
Operational Checks
Instrument the parser with counters for CRC failures, length violations, and resync events. Track a high-water mark for buffer occupancy. These counters turn silent parser failures into observable diagnostics.
In host-based unit tests, feed a known-good frame, then mutate it: truncate the payload, inflate the length field, flip a CRC byte, and flip payload bits. Assert that the parser rejects each case and that the validated struct remains unchanged after rejection. On target, inject bus errors and measure worst-case parse time and stack usage under maximum frame rate.
Build the parser translation unit with strict flags:
# Host or target build (adjust for your toolchain)
gcc -Wall -Wextra -Werror -c parser.c -o parser.o
Required permissions: standard user for host builds; target flashing permissions depend on your debug probe. Expected check: zero warnings. Risk: -Werror turns warnings into build failures, which is intentional for this component.
Inspect the map file to confirm the buffer and validated struct are statically allocated with no heap references:
# After linking
grep -E 'rx_buf|validated_frame' build/output.map
Expected check: symbols appear in .bss or .data, not resolved to heap allocator calls. Risk: aggressive linker garbage collection may remove unused statics; verify the parser is actually referenced.
Failure Modes
- Buffer overflow: Trusting the length field without comparing it to
MAX_FRAME_LEN. - Integer overflow: Computing offsets with insufficient width; use
size_tor fixed-width unsigned types and check before addition. - Partial-frame corruption: An error mid-frame leaves the state machine in an unknown state. Always return to
READ_HEADERon error. - Uninitialized padding: A raw
memcpyinto a struct can copy padding bytes. Decode field-by-field into the validated struct instead.
Conditions That Change the Design
- Throughput: If frames arrive faster than byte-at-a-time parsing allows, move to block reads with direct memory access (DMA), but preserve the same validation order.
- Multiple masters: Concurrent writers require a single-writer discipline or brief interrupt masking; ad-hoc
volatileaccess is not a concurrency rule. - Authentication or encryption: Add a message authentication code (MAC) or authenticated encryption tag after CRC, and treat unauthenticated frames as untrusted.
- DMA and cache: If DMA writes the buffer, ensure alignment and cache-coherency requirements are met before the parser reads.
- Real-time operating system (RTOS) or memory protection unit (MPU): Task isolation and separate stacks change buffer ownership; the parser may need to run in a privileged task or use an MPU-guarded region.
Limitations and Verification
This architecture assumes a single writer to the receive buffer and a link where CRC is sufficient for the threat model. It does not cover fragmentation, reassembly across multiple frames, or flow control. Static analysis and warning-free builds help but do not replace boundary tests.
A practical check: after integration, monitor the resync counter over a sustained run. A rising resync count indicates the bus is delivering malformed frames or the parser is losing synchronization—both require investigation before trusting internal state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.