The fast path in fill_buffer (line 159) uses a bare << self.nbits shift:
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap()) << self.nbits;
When self.nbits >= 64, this panics with "attempt to shift left with overflow" — which occurs in all build modes, not just debug.
The slow path (line 167) already handles this correctly with checked_shl:
self.buffer |= u64::from_le_bytes(input_data)
.checked_shl(self.nbits as u32)
.unwrap_or(0);
How nbits reaches 64: consume_bits guards against underflow with only a debug_assert! (compiled out in release builds). Malformed deflate data can cause the decompressor to consume more bits than are in the buffer, wrapping nbits (a u8) to a large value via unsigned subtraction. On the next fill_buffer call, the shift overflows and panics.
Impact: When used via the png crate, any malformed PNG with a corrupt deflate stream can trigger this panic. In environments using panic=abort (common in embedded/mobile), this is an instant process kill with no opportunity for error recovery.
Proposed fix (3 lines):
- fill_buffer fast path — use checked_shl, matching the slow path:
// before
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap()) << self.nbits;
*input = &input[(63 - self.nbits as usize) / 8..];
// after
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap())
.checked_shl(self.nbits as u32)
.unwrap_or(0);
*input = &input[(63 - self.nbits.min(63) as usize) / 8..];
- consume_bits — use saturating_sub to prevent wrapping on malformed input:
// before
self.nbits -= nbits;
// after
self.nbits = self.nbits.saturating_sub(nbits);
The fast path in fill_buffer (line 159) uses a bare << self.nbits shift:
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap()) << self.nbits;
When self.nbits >= 64, this panics with "attempt to shift left with overflow" — which occurs in all build modes, not just debug.
The slow path (line 167) already handles this correctly with checked_shl:
self.buffer |= u64::from_le_bytes(input_data)
.checked_shl(self.nbits as u32)
.unwrap_or(0);
How nbits reaches 64: consume_bits guards against underflow with only a debug_assert! (compiled out in release builds). Malformed deflate data can cause the decompressor to consume more bits than are in the buffer, wrapping nbits (a u8) to a large value via unsigned subtraction. On the next fill_buffer call, the shift overflows and panics.
Impact: When used via the png crate, any malformed PNG with a corrupt deflate stream can trigger this panic. In environments using panic=abort (common in embedded/mobile), this is an instant process kill with no opportunity for error recovery.
Proposed fix (3 lines):
// before
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap()) << self.nbits;
*input = &input[(63 - self.nbits as usize) / 8..];
// after
self.buffer |= u64::from_le_bytes(input[..8].try_into().unwrap())
.checked_shl(self.nbits as u32)
.unwrap_or(0);
*input = &input[(63 - self.nbits.min(63) as usize) / 8..];
// before
self.nbits -= nbits;
// after
self.nbits = self.nbits.saturating_sub(nbits);