Skip to content

Reject malformed element counts in message decoders - #451

Open
gaoflow wants to merge 3 commits into
sunng87:masterfrom
gaoflow:fix-signed-count-capacity-overflow
Open

Reject malformed element counts in message decoders#451
gaoflow wants to merge 3 commits into
sunng87:masterfrom
gaoflow:fix-signed-count-capacity-overflow

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 29, 2026

Copy link
Copy Markdown

Several message decoders read a repeated-element count as a signed i16/i32 and hand it to Vec::with_capacity before reading any element. A negative count (0xffff as i16 = -1) sign-extends to usize::MAX and panics with capacity overflow, so a ~7-byte crafted message crashes whoever decodes it — the shipped client/proxy on a backend message, or the server on Bind.

Repro (debug and release):
PgWireBackendMessage::decode(&mut BytesMut::from(&b"T\0\0\0\x06\xff\xff"[..]), &ctx)capacity overflow.

#192 fixed this for Parse, Bind and ParameterDescription by widening the count to u16 but stopped short of the siblings that share the shape. This completes it for the six missed: RowDescription, CopyInResponse/CopyOutResponse/CopyBothResponse, NegotiateProtocolVersion (re-introduced in #281), and Bind's result_column_format_codes (#192 widened the other two counts in that same function, not this third).

The counts are read unsigned and routed through one shared codec::read_count that rejects a count larger than the remaining bytes, turning a malformed message into a decode Err. A single helper also keeps the pattern from silently reappearing as it did in #281; and reading unsigned alone isn't enough for the i32 option count, where u32::MAX still forces a huge allocation, so the bound is applied uniformly. The four counts #192 already widened are left untouched.

Tests exercise all six sites through the public decode API, plus zero-count and well-formed positive-count controls.

Several decoders read a repeated-element count as a signed i16/i32 and
pass it to Vec::with_capacity before reading any element. A negative count
(0xffff read as i16 = -1) sign-extends to usize::MAX and panics with
"capacity overflow", so a short crafted message crashes the peer decoding
it (the shipped client/proxy on backend messages, the server on Bind).

sunng87#192 fixed this for Parse, Bind and ParameterDescription by widening the
count to u16 but left RowDescription, the three Copy*Response messages,
NegotiateProtocolVersion, and Bind's result_column_format_codes. Read
those counts as unsigned and route them through a shared codec::read_count
that rejects a count exceeding the bytes remaining, turning a malformed
message into a decode error instead of a panic.
Comment thread src/messages/codec.rs Outdated
/// collection. Counts are unsigned; a value larger than the bytes left in the
/// buffer cannot describe a real message (each element takes at least one byte),
/// so it is rejected instead of driving an oversized `Vec::with_capacity`.
pub(crate) fn read_count(count: usize, buf: &BytesMut) -> PgWireResult<usize> {

@sunng87 sunng87 Jul 29, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this count is element count, but here we are comparing it with byte count, which need to be corrected. For reading bytes this is ok, but can be wrong for numbers.

let's rename this function to ensure_count and return PgWireResult<()> so we won't need to get count as u16, then convert back to i16 again.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 074ca57 — renamed to ensure_count, returns PgWireResult<()>, and callers keep the wire count as-is (single conversion). 150 tests pass.

Comment thread src/messages/codec.rs Outdated
/// so it is rejected instead of driving an oversized `Vec::with_capacity`.
pub(crate) fn ensure_count(count: usize, buf: &BytesMut) -> PgWireResult<()> {
let remaining = buf.remaining();
if count > remaining {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I said, we cannot simply compare count with remaining. this count is about how many elements, for example, in Bind, it's the number of Int16s, so to compare with remaining we will need to multiply 2

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6dac70 — ensure_count now takes the per-element size and compares count × elem_size against the remaining bytes (2 for Int16 format codes, 1 for startup option names, 19 for row-description fields). Added a Bind case with 5 codes over 6 bytes to the rejection test. 150 tests pass.

Counts on the wire are element counts, not byte counts: Bind's result
format codes are Int16s, so a count that fits the remaining bytes can
still overrun the buffer when read as 2-byte elements. ensure_count now
takes the per-element size and rejects count * size > remaining.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants