fix(ec): reject oversized bitrot payload before narrowing to u32 (#10172)

save_bitrot_sidecar writes payload.len() into the header as a u32; guard against
a payload > 1 GiB (which would silently truncate the length field), mirroring
Go's SaveBitrotSidecar maxBitrotPayloadSize check. The check uses encoded_len()
before serializing, so an oversized manifest never allocates a large buffer.
Never triggers for a real sidecar (a few KB).

Claude-Session: https://claude.ai/code/session_015EE9Sc9EvNp8BCVva4RKdo
This commit is contained in:
Chris Lu
2026-06-30 20:43:11 -07:00
committed by GitHub
parent b4a99b996d
commit ce3ba31bcd
@@ -248,6 +248,19 @@ impl ShardChecksumBuilder {
/// Atomically writes `prot` to `path`, wrapped in the on-disk header with a
/// CRC32C over the serialized payload (temp file + rename).
pub fn save_bitrot_sidecar(path: &str, prot: &EcBitrotProtection) -> io::Result<()> {
// The header records payload_len as a uint32 and the buffer allocation below
// adds it to a constant. Bound the payload well under any overflow (a real
// manifest is a few KB) so neither the length field nor the buffer can wrap.
// Check the encoded length BEFORE serializing so an oversized manifest never
// allocates a huge buffer. Mirrors Go's SaveBitrotSidecar maxBitrotPayloadSize.
const MAX_BITROT_PAYLOAD_SIZE: usize = 1 << 30; // 1 GiB, vastly above any real sidecar
let payload_len = prot.encoded_len();
if payload_len > MAX_BITROT_PAYLOAD_SIZE {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("bitrot sidecar payload too large: {} bytes", payload_len),
));
}
let payload = prot.encode_to_vec();
let mut buf = Vec::with_capacity(BITROT_HEADER_SIZE + payload.len());
buf.extend_from_slice(&BITROT_MAGIC.to_be_bytes());