Fix: replace fixed comps with comps calculation - #25293
Conversation
| .data | ||
| .as_ref() | ||
| .ok_or(CompressedImageSaverError::UninitializedImage)?; | ||
| let comps: u8 = match format { |
There was a problem hiding this comment.
More descriptive variable name please :)
| .data | ||
| .as_ref() | ||
| .ok_or(CompressedImageSaverError::UninitializedImage)?; | ||
| let comps: u8 = match format { |
There was a problem hiding this comment.
Can we add a regression test for this? This is subtle and pretty high severity, so I would like both comments and automated testing to help protect us.
API redesign would be even better but I've been fought on making Image more type-safe before :p
There was a problem hiding this comment.
I've redesigned the implementation and added two tests(One test used to test those supported format and the other test unsupported format). However, the tests have a limitation: I can't verify whether out-of-bounds reads have actually been eliminated, since the real pixel-reading logic happens inside C++. My Rust-side tests can only check whether save() succeeds or the format is correctly rejected — and I can't use Miri to detect out-of-bounds reading.
I'm also not fully confident that my classification of "formats that can't be handled correctly and should be rejected" is accurate. I'd appreciate your suggestion about this PR.
Objective
CompressedImageSaverUniversal::savecalledsource_image.init(data, size.x, size.y, 4)with the channel count hardcoded to4for every input image, regardless of itsTextureFormat. basis-universal'simage::initreads exactlywidth * height * compsbytes from the source buffer, so for any format whose pixel size is not 4 bytes this is an out-of-bounds read. For example, a 1×1R8Unormimage (1 byte of data) causes the encoder to read 4 bytes — a 3-byte overread per pixel, reachable entirely through safe code (Image::new+CompressedImageSaver::save). Larger images amplify the overread proportionally (e.g. a 4096×4096R8Unormimage overreads by ~48 MiB). Affected formats include every non-4-byte-per-pixel uncompressed format:R8*(1 B/px),Rg8*(2 B/px),R16*(2 B/px), etc.Solution
comps(the channel countsource_imagereads per pixel) according to the image'sTextureFormatinstead of hardcoding4.source_image.initis byte-oriented (eachcompsis oneu8channel), so only 8-bit-per-channel uncompressed formats can be fed correctly:R8{Unorm,Snorm,Uint,Sint}→comps = 1Rg8{Unorm,Snorm,Uint,Sint}→comps = 2Rgba8{Unorm,UnormSrgb,Snorm,Uint,Sint}→comps = 4CompressedImageSaverError::UnsupportedFormat.source_image.init(data, size.x, size.y, 4)becomessource_image.init(data, size.x, size.y, comps).Additional possible soundness issue
Image::datais a public field andImage::new's length check is only adebug_assert_eq!(skipped in release), a safe-constructedImagecan still havedatashorter than its declaredsize×format, which would letinitoverread even with the correctcomps. I don't know whether this is a intentional design to reduce check in release mode, but if not I can provide a new PR to fix it.Testing
I'm trying to build a PoC to prove that it did read out-of-bounds memory, and I use miri to detect this UB. But Miri don't support FFI, so I can't prove this is a true Bug.