fix(bmp): reject zero height for ICO-embedded BMP (panic on crafted input)#3041
fix(bmp): reject zero height for ICO-embedded BMP (panic on crafted input)#3041Ace-0-A wants to merge 2 commits into
Conversation
A BMP-backed ICO entry whose embedded BITMAPINFOHEADER height is odd (e.g. 1) is halved to 0 by the ICO double-height convention, after the decoder's overflow/zero validation has already run. Decoding such input must return an error, not panic. This test currently fails (panics). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`read_metadata_in_ico_format` halves the BMP height to undo the ICO double-height convention. This halving runs *after* `read_metadata`'s dimension checks, so an entry with an odd doubled-height (e.g. 1) leaves `self.height == 0`. The full-byte and 16-bit pixel readers then evaluate `(self.height - 1).try_into::<u32>().unwrap()`, which panics with `TryFromIntError` on a zero height (reachable via the default lenient `image::load_from_memory` path with a ~120-byte crafted ICO). Reject a zero height after halving, mirroring the existing `width < 0` and `height == i32::MIN` validation in the same parser. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The fix needs to target the implementation according to specification, not avoid a fault. A height of Imagemagick identifies the image as |
|
I played around with your test image and created a few variations to see how Windows Explorer interprets it. I found that Windows behaves as follows: If So I think the fix for ICO would be to implement this fallback in lenient mode and return an error in strict mode. Strict mode should also validate that Also, That said, BMP decoding is still broken. (Also, the BMP decoder has 33 |
|
Windows behavior sounds sensible to me, too. Thanks for testing it out on that primary target. Agreed with verifying Regarding the actual panic, that's an artifact of outlining. We calculate that even if the inner loop (over rows) is supposed to be empty. It's only used to test which row is last which does not happen anyways when there are no rows to iterate on. It could just choose any value in this case instead, i.e. |
|
Regarding the panic, while reading through the BMP decoder for #3042 I figured out what the cause is. Basically, the BMP decoder already verifies that width and height are in the interval I'm planning to make this undocumented assumption explicit by using And yes, the BMP decoder rejects all empty images. It's just super non-obvious from the code and the error it returns is halariously "Image dimensions (0x0 w/4 channels) are too large." |
A BMP-backed ICO whose embedded
BITMAPINFOHEADERheight is odd (e.g.1) panicsthe decoder instead of returning an error. Reachable on the default (lenient)
image::load_from_memorypath with a ~120-byte input.Cause
read_metadata_in_ico_formathalves the BMP height to undo the ICO double-heightconvention (
src/codecs/bmp/decoder.rs:1731). This halving runs afterread_metadata's dimension validation, so an entry with an odd doubled-heightleaves
self.height == 0. The full-byte (24/32-bit) and 16-bit pixel readers thenevaluate
(self.height - 1).try_into::<u32>().unwrap(), which panics withTryFromIntErroron a zero height.A standalone BMP with
height == 0is already rejected bycheck_for_overflow;only the ICO halving can produce a post-validation zero.
Fix
Reject a zero height immediately after halving, reusing the existing
DecoderError::InvalidHeightand mirroring thewidth < 0/height == i32::MINchecks already in the same parser. No public API change.
A regression test is added first (it panics without the fix); the fix commit makes
it pass. The existing BMP/ICO unit suites and the
tests/bad/corpus stay green.Minimal reproducer