Fix out-of-bounds reads in TLV parsing and v1 header creation#32
Conversation
There was a problem hiding this comment.
Pull request overview
This PR addresses two ASan-confirmed memory-safety issues in the PROXY protocol implementation: (1) a 1-byte out-of-bounds read while parsing US-ASCII TLVs at the end of a v2 header buffer, and (2) potential buffer overflows when creating v1 headers from long (non-canonical) IPv6 textual forms by normalizing addresses into their canonical inet_ntop() representation.
Changes:
- Fix US-ASCII TLV parsing to allocate
length + 1but copy onlylengthbytes, then append the trailing NUL. - Fix v1 header creation to normalize
src_addr/dst_addrwithinet_pton()+inet_ntop()to ensure canonical bounded textual forms. - Add regression tests covering both issues.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/proxy_protocol.c |
Fixes TLV copy semantics for US-ASCII values and normalizes v1 addresses via inet_ntop() to prevent buffer overflow. |
tests/test.c |
Adds regression tests for the US-ASCII TLV-at-end parsing case and v1 creation/round-trip with long IPv4-mapped IPv6 inputs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b0a5607 to
62d8d21
Compare
Two memory-safety bugs, both confirmed with AddressSanitizer: 1. Parsing a US-ASCII TLV copied length + 1 bytes from the value into the new TLV, reading one byte past the input buffer when the TLV was the last thing in the buffer. tlv_new() now takes a separate copy_length (clamped to length) so it allocates length + 1 for the trailing NUL but copies only length bytes, then appends the NUL. 2. Creating a v1 header copied the addresses into fixed 40-byte buffers and sprintf'd them. A 45-char IPv4-mapped IPv6 address (e.g. ffff:...:255.255.255.255) overflowed those buffers and the line buffer, and produced a header too long for the v1 parser to read back. Addresses are now normalised via inet_ntop() to the canonical (<= 39 char) form. The line length is bounded before formatting (the non-Windows _sprintf() uses unbounded vsprintf, as ANSI C has no snprintf) and the _sprintf() return value is validated so a sprintf_s failure on Windows cannot drive an oversized malloc/memcpy. Added regression tests for both, passing under ASan/UBSan. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62d8d21 to
b83f5d5
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
src/proxy_protocol.c:816
- In the v2 create padding logic,
pp2_hdr_len_paddedis computed in auint16_t. For large headers withalignment_power == 15, the next multiple can be 65536, which wraps to 0 inuint16_tand can lead to incorrectpadding_bytes/lencalculations and potentially out-of-bounds writes. Compute the padded length in a wider type and reject results > UINT16_MAX (also after the extra+= alignmentfor the minimum NOOP TLV size).
uint16_t alignment = 1 << pp_info->pp2_info.alignment_power;
if (*pp2_hdr_len % alignment)
{
uint16_t pp2_hdr_len_padded = (*pp2_hdr_len / alignment + 1) * alignment;
/* The NOOP TLV needs to be at least 3 bytes because a TLV can not be smaller than that */
alignment_power is a caller-set uint8_t. Values >= 16 made the 1 << alignment_power shift overflow the uint16_t alignment: 1 << 16 wraps to 0 (division by zero in the % and / that follow) and 1 << 31 is signed-integer overflow (UB). Both are reachable from the public pp_create_hdr() API and were found by fuzzing the create path under UBSan. Cap the power at 15 (a header length is a uint16_t, so a larger alignment could never fit anyway) and return -ERR_PP2_LENGTH otherwise. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
46858f3 to
75ad900
Compare
Stored TLV length no longer counts the trailing NUL, so getters report the true value length and re-serialised headers stop growing per hop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
edb168a to
3e5d5ae
Compare
1 << alignment_power shifts a signed int literal, which is undefined behavior on platforms where int is 16-bit (1 << 15 is unrepresentable as a signed int). Use 1U to keep the shift well-defined across C89 targets.
16ad2c4 to
65ac3f1
Compare
65ac3f1 to
cb91bb4
Compare
- Add explicit uint16_t cast to the alignment shift - tlv_new(): treat NULL value with non-zero length as an error so a TLV with uninitialized value bytes can never be serialized; skip memcpy() for zero-length values - pp2_create_hdr(): compute padded length with all operands cast to uint32_t so the arithmetic is not done in a (possibly 16-bit) promoted int that could wrap and defeat the overflow check - pp2_create_hdr(): emit the NOOP TLV whenever padding is applied, including the valid zero-length (3-byte) case, so the alignment is satisfied without inflating the header by a full alignment block - test: use NUM_ELEMS(powers) instead of sizeof(powers) for the loop bound
cb91bb4 to
e33d61c
Compare
Fixes four memory-safety / robustness bugs found with AddressSanitizer + UBSan (some via fuzzing the create path):
Parsing (
pp_parse_hdr) — a US-ASCII TLV (e.g. NETNS, SSL sub-TLVs) copiedlength + 1bytes from its value, reading one byte past the input buffer when the TLV was the last thing in the buffer.tlv_new()now takes a separate copy length so it allocateslength + 1for the trailing NUL but copies onlylengthbytes. Reachable from a crafted network header.Parsing (US-ASCII TLV length inflation) — the trailing NUL added for the getters was also written into the on-wire
lengthfield, so parsed US-ASCII TLVs reported alengthone byte too big and, on re-serialisation, emitted the NUL on the wire and grew the header — inflation that compounded by one byte on every parse → create hop. The stored length is now the true value length (NUL no longer counted), matching the documented getter contract; the buffer stays NUL-terminated.Creating (
pp_create_hdr, v1) — addresses were copied into fixed 40-byte buffers andsprintf'd. A 45-char IPv4-mapped IPv6 address (e.g.ffff:...:255.255.255.255) overflowed those buffers and the line buffer, and produced a header too long for the v1 parser to read back. Addresses are now normalised viainet_ntop()to the canonical (<=39-char) form; the line length is bounded before formatting (the non-Windows_sprintfuses unboundedvsprintf, as ANSI C has nosnprintf) and the_sprintfreturn value is validated so asprintf_sfailure on Windows cannot drive an oversizedmalloc/memcpy.Creating (
pp_create_hdr, v2 alignment) —pp2_info.alignment_poweris a caller-setuint8_t. Values >= 16 made1 << alignment_poweroverflow theuint16_talignment (1 << 16wraps to 0 → division by zero;1 << 31is signed-integer overflow). Separately, rounding the header length up to the alignment could exceedUINT16_MAXeven for an in-range power, wrapping theuint16_tpadded length to a tiny value and driving an undersizedmallocfollowed by a TLVmemcpypast the allocation (heap overflow, confirmed under ASan withalignment_power = 12). The power is now capped at 15 and the padded length is computed in a wider type and rejected with-ERR_PP2_LENGTHif it would exceedUINT16_MAX.Also tidied the v1 header code: the repeated family → IP-error-code ternary is now a small helper, the redundant post-
sprintfbound check was dropped (the length is already bounded before formatting), and the v1 IPv4-mapped IPv6 regression asserts against hard-coded RFC 5952 canonical literals instead of recomputing them with the sameinet_pton/inet_ntopcalls under test.Added regression tests for all of the above; the full suite passes under ASan/UBSan and the library compiles clean under
-std=c89 -pedantic-errors.