Skip to content

pkt_num_len can return 1 byte, leading to AEAD decryption failures under packet reorder #2448

@dave

Description

@dave

Summary

quiche/src/packet.rs::pkt_num_len will choose a 1-byte truncated packet number when fewer than 128 packets are unacked. RFC 9000 §17.1 permits this, but it is unsafe in practice: if the receiver observes more than 128 packets reordered, the entire valid range of a 1-byte truncation collapses, and the decoded full packet number lands on the wrong candidate. The AEAD nonce is derived from the full packet number, so the receiver fails to decrypt an otherwise-good packet.

Source

quiche/src/packet.rs::pkt_num_len:

pub fn pkt_num_len(pn: u64, largest_acked: u64) -> usize {
    let num_unacked: u64 = pn.saturating_sub(largest_acked) + 1;
    let min_bits = u64::BITS - num_unacked.leading_zeros() + 1;
    min_bits.div_ceil(8) as usize     // returns 1 when num_unacked < 128
}

The reference Go implementation, quic-go, refuses 1-byte truncation for exactly this reason. From internal/protocol/packet_number.go#L41-L42:

// it never chooses a PacketNumberLen of 1 byte, since this is too short under certain circumstances

Field observation

Hit in production at https://viatrail.app — Cloudflare R2-served HTTP/3 tile fetches stalling on mobile. qlog shows clusters of packet_dropped with trigger=payload_decrypt_error on 1-RTT packets, with decoded PN offset from actual PN by exactly 256·k — the precise signature of 1-byte truncation ambiguity.

Reproducer

https://github.com/dave/quiche-bug — Docker-based server (stock or patched via --build-arg APPLY_PATCH=0|1) plus Go HTTP/3 client using quic-go.

Server AEAD failures (90s, 8 workers)
Stock 6
With pkt_num_len.max(2) 0

Proposed fix

Floor pkt_num_len at 2 bytes (1-line change). PR to follow.

Cost: 1 byte per outgoing 1-RTT packet during the brief window where num_unacked < 128. quic-go has shipped this trade-off since 2017.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions