Skip to content

Fix: replace fixed comps with comps calculation - #25293

Open
yilin0518 wants to merge 1 commit into
bevyengine:mainfrom
yilin0518:fix_save
Open

Fix: replace fixed comps with comps calculation#25293
yilin0518 wants to merge 1 commit into
bevyengine:mainfrom
yilin0518:fix_save

Conversation

@yilin0518

Copy link
Copy Markdown
Contributor

Objective

  • Fix a safe-API soundness issue in the bevy_image::universal.rs: universal image saver CompressedImageSaverUniversal.
  • CompressedImageSaverUniversal::save called source_image.init(data, size.x, size.y, 4) with the channel count hardcoded to 4 for every input image, regardless of its TextureFormat. basis-universal's image::init reads exactly width * height * comps bytes 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×1 R8Unorm image (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×4096 R8Unorm image 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

  • Calculate comps (the channel count source_image reads per pixel) according to the image's TextureFormat instead of hardcoding 4. source_image.init is byte-oriented (each comps is one u8 channel), so only 8-bit-per-channel uncompressed formats can be fed correctly:
    • R8{Unorm,Snorm,Uint,Sint}comps = 1
    • Rg8{Unorm,Snorm,Uint,Sint}comps = 2
    • Rgba8{Unorm,UnormSrgb,Snorm,Uint,Sint}comps = 4
  • All other formats are rejected with the existing CompressedImageSaverError::UnsupportedFormat.
  • The buggy source_image.init(data, size.x, size.y, 4) becomes source_image.init(data, size.x, size.y, comps).

Additional possible soundness issue

  • Because Image::data is a public field and Image::new's length check is only a debug_assert_eq! (skipped in release), a safe-constructed Image can still have data shorter than its declared size×format, which would let init overread even with the correct comps. 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.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-Rendering Drawing game state to the screen P-Unsound A bug that results in undefined compiler behavior labels Aug 5, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Aug 5, 2026
@alice-i-cecile
alice-i-cecile requested a review from beicause August 5, 2026 01:47
@alice-i-cecile alice-i-cecile added this to the 0.20 milestone Aug 5, 2026
.data
.as_ref()
.ok_or(CompressedImageSaverError::UninitializedImage)?;
let comps: u8 = match format {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More descriptive variable name please :)

.data
.as_ref()
.ok_or(CompressedImageSaverError::UninitializedImage)?;
let comps: u8 = match format {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@alice-i-cecile alice-i-cecile added the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior P-Unsound A bug that results in undefined compiler behavior S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

2 participants