stream: reject over-long paths in pouch_uplink_stream_open - #309
Conversation
| if (strlen(path) > UINT8_MAX) | ||
| { | ||
| return NULL; | ||
| } | ||
|
|
There was a problem hiding this comment.
@fderepas I believe this same issue is present in entry.c with write_entry() -- could you apply the fix there as well? It's also worth considering if this fix may be better suited to write_stream_header() above instead. It would avoid accidental reintroduction by another caller of the write_stream_header(), though it would come at the expense of potentially doing unnecessary work in pouch_uplink_stream_open() (i.e. opening the stream when we could have already determined that the path was invalid). That being said, given that failure here is evidence of a programmer's bug (using too long of a path), I'd be inclined to trade-off the potential performance improvements for safety.
There was a problem hiding this comment.
@fderepas thanks for the update to entry.c! Is it your preference to not move the check here to write_stream_header() as I suggest?
trond-snekvik
left a comment
There was a problem hiding this comment.
Dan has one pending suggestion, but it's a subjective one, so I'm approving and leaving it up to you @fderepas.
Summary
write_stream_header()writes an application-suppliedpathinto a freshlyallocated block with no length check. A path longer than the block's
remaining payload capacity overflows the block buffer; a path longer than 255
bytes additionally corrupts the on-wire length prefix. This PR adds a bounds
check at the public API entry (
pouch_uplink_stream_open) so an over-long pathis rejected cleanly instead of overrunning the buffer.
The problem
src/stream.c:pathoriginates from the public APIpouch_uplink_stream_open(const char *path, …)and flows straight into the header write on a block obtained from
block_alloc_stream(). That block has a fixed payload capacity(
MAX_PLAINTEXT_BLOCK_SIZE,src/block.h), of which the block header(
BLOCK_HEADER_SIZE = 3) plus this stream sub-header (2-bytecontent_type+1-byte length prefix = 3) are already consumed. The
buf_writeof the path istherefore memory-safe only while:
There is no runtime enforcement of this bound. Two distinct issues result:
Buffer overflow (memory safety). An application passing a path longer
than the remaining block payload overflows the block. Impact is bounded to
the uplink write path and the path is application/local-supplied (not
attacker-controlled downlink input), so this is a defensive-robustness /
API-contract gap rather than a remotely triggerable bug — but it is still an
out-of-bounds write on a caller mistake.
Silent length-prefix truncation (protocol correctness). The length is
stored in a single byte (
*buf_claim(block, 1) = path_len,path_lenissize_t). Any path in the 256–509 byte range is memory-safe but silentlytruncates the recorded length, producing a corrupt frame.
The wire format's 1-byte length prefix means paths longer than 255 bytes are
not representable in this protocol at all, so a single cap at 255 addresses
both issues at once (255 ≤ 509, so it also precludes the overflow).
The fix
Guard the public API entry so an unrepresentable/over-long path is rejected
before any allocation, consistent with the function's existing
NULL-on-failurecontract:
UINT8_MAX(255) is the length the 1-byte prefix can hold and is below the509-byte block-capacity bound, so this single check prevents both the
out-of-bounds write and the prefix truncation.
<stdint.h>(forUINT8_MAX)and
<string.h>(forstrlen) are already in the translation unit.Alternative
If longer stream paths are a requirement, the alternative is to widen the
length prefix on the wire (e.g. a 2-byte length) and keep the memory-safety
bound at
MAX_PLAINTEXT_BLOCK_SIZE - BLOCK_HEADER_SIZE - (sizeof(uint16_t) + 2).That is a protocol-format change (broker-side decoder must match) and is out of
scope for this hardening fix; the guard above keeps the current format correct.
How this was found
Surfaced by a deductive proof coupled with Squeeze Loop strategy: proving
write_stream_headerfree of out-of-bounds accesses required the callerprecondition
strlen(path) <= 509, i.e. the body has no such check. A mutationprobe confirmed the bound is load-bearing — removing it re-introduces the
unproved
buf_writepayload-bound obligations (38/38 → 34/38). Under the addedguard,
write_stream_headerandpouch_uplink_stream_openare provenmemory-safe.
Reproducer
ASAN was used to create a self-contained reproducer that reuses the exact
shipped semantics — the
struct pouch_buf { …; size_t bytes; uint8_t buf[]; }layout,
buf_alloc(malloc(sizeof(struct pouch_buf) + capacity)), thebounds-check-free
buf_claim/buf_write, and a verbatimwrite_stream_headeron a block of the shipped
MAX_PLAINTEXT_BLOCK_SIZEcapacity — and opens astream with a 1024-byte path (> the 509-byte bound):
ASan reports the out-of-bounds write on the very
buf_write/write_stream_headerpath (
WRITE of size 1024into a blockallocated by … buf_alloc):With the guard applied (
strlen(path) > UINT8_MAX→ reject before the write),the same 1024-byte path is refused and no out-of-bounds access occurs — ASan
runs clean.
Testing
passes clean with the guard — suitable as a regression under a sanitizer CI job.
MAX_PLAINTEXT_BLOCK_SIZEand assertpouch_uplink_stream_openreturnsNULL(previously: buffer overflow / truncated prefix).