Skip to content

v0.2.1 — i32 stride OOB class fix

Choose a tag to compare

@lilith lilith released this 02 May 04:37
· 64 commits to main since this release

⚠️ YANKED — this version has been pulled from crates.io. It contains a use-after-free in StreamingDecoder::update (fixed in 0.3.4) plus other soundness issues fixed across the 0.2.x / 0.3.x series. Use 0.3.4 or later — or, recommended for new projects, switch to zenwebp, which is #![forbid(unsafe_code)] and has no FFI exposure.


Patch release closing a class of OOB issues found in a second-pass audit.

Want pure-Rust WebP? For new projects, zenwebp is the recommended choice — a pure-Rust port of libwebp built with #![forbid(unsafe_code)], with full feature parity, comparable runtime performance and compression, and no FFI surface area. webpx is maintained for existing users.

Soundness fixes

Encoder & streaming stride values cast to i32 without an upper-bound check. A caller-supplied u32 / usize stride >= 2^31 wraps to a negative i32; libwebp's row pointer arithmetic then walks backwards through process memory. Reachable from safe Rust without any unsafe block at the call site by constructing a &[u8] of >= 2 GiB on 64-bit and passing a stride above i32::MAX. Fixed at every site:

  • Encoder::new_rgba_stride / new_bgra_stride / new_rgb_stride / new_bgr_stride (via validate_buffer_size_stride)
  • Encoder::new_argb / new_argb_stride
  • Encoder::new_yuv (y_stride, uv_stride, a_stride — via validate_yuv_planes)
  • The same paths in import_pixels for encode_owned
  • StreamingDecoder::with_buffer

Encoder::from_img and Encoder::from_pixels_stride truncated (stride * bpp) as u32 when the source usize stride exceeded u32::MAX / bpp. Truncation produced a stride smaller than the actual byte stride; validate_buffer_size_stride couldn't catch it and libwebp encoded with the wrong row layout — silent wrong-output bug, not OOB on the input slice. Fixed with a saturating cast so oversized strides hit the new i32::MAX upper-bound check.

Latent UB

All MaybeUninit::<libwebp_sys::*>::uninit() sites switched to MaybeUninit::<libwebp_sys::*>::zeroed(). libwebp's *Init* functions (WebPMemoryWriterInit, WebPAnimDecoderOptionsInit, WebPAnimEncoderOptionsInit, etc.) only set their documented fields; the pad: [u32; 1] / padding: [u32; 7] / padding: [u32; 4] arrays they leave alone are now initialized to zero rather than uninitialized. Per Rust's UB rules, reading uninit bytes during assume_init is technically UB even when those bytes are never observed.

Deprecations

  • compat::webp and compat::webp_animation modules and every public type carry #[deprecated(since = "0.2.1")]. Both shims swallow error variants the main API surfaces and don't expose Limits for untrusted-input decoding. They're retained for at least one minor release.

Documentation

  • Encoder::new_argb rustdoc now spells out the little-endian-only assumption: the 0xAARRGGBB numeric layout matches libwebp's expected [B, G, R, A] byte order on every CI target. Big-endian callers should treat this path as unsupported.

Regression tests

tests/soundness.rs gains a stride_overflow module with four regression tests covering ARGB, RGBA, YUV, and streaming with_buffer stride bounds. Total soundness regression count: 8 → 12. Full suite: 264 tests.

See CHANGELOG.md for the full diff summary.