Skip to content

reliable 1.4.5: a reassembled packet never carries stale heap bytes, and a caller's unterminated endpoint name cannot be over-read

Latest

Choose a tag to compare

@rowan-claude rowan-claude released this 14 Sep 00:12
e4e7092

reliable 1.4.5: a reassembled packet never carries stale heap bytes, and a caller's unterminated endpoint name cannot be over-read

A packet reassembled from fragments is now built in zeroed memory, and an endpoint name the caller never terminated can no longer be read past. Both are from security#26 — items 2 and 3 — and landed together in reliable#66.

The reassembly buffer is allocated through the endpoint's allocator: allocate_function, the caller's own if one was supplied and reliable_default_allocate_function, a bare malloc, if not. That memory arrives uninitialized, holding whatever the heap last put there. The fix does not bypass the caller's allocator and does not add an allocation — reliable_endpoint_receive_packet still asks the same allocator for the same buffer, and now runs one memset over it before any fragment is stored. The length is the one already computed for that allocation: RELIABLE_MAX_PACKET_HEADER_BYTES, plus the fragment count times the endpoint's own config.fragment_size, plus eight. It is bounded by the endpoint's own configuration rather than by anything a peer sends, and it is one memset per reassembly allocation, not one per fragment. As the code stands the delivered bytes could not have contained stale heap: completeness is enforced by the fragment bitmap and every fragment's offset is validated before the copy. This change is about what happens when that stops being true — a future logic error that leaves a hole in the buffer now leaks zeros into the delivered packet instead of the contents of freed memory. test_fragment_reassembly_buffer_zeroed covers it.

reliable_config_t.name is a fixed array the caller fills, and nothing in the API requires the caller to NUL terminate it. reliable_endpoint_create copies the config into the endpoint and now forces a terminator onto the last byte of its own copy, so every log that prints the endpoint's name stops inside the array — including the allocation-failure log at create, which now prints that terminated copy rather than the caller's struct. The config checks run before the copy exists and so cannot rely on the terminator: each of the twelve create-time rejection logs now prints the caller's name with a precision, %.*s bounded by the size of the field, so printf stops at the end of the array instead of reading past it. Before this, a caller who filled the whole name with no terminator had the library read off the end of their struct — adjacent memory disclosed into the log, or a fault. test_endpoint_name_terminated and test_config_name_bounded_in_rejection_log cover it.

Nothing on the wire changes, no symbol changes, and reassembled bytes are unchanged. Found by an outside security review.