CVE-2026-2314
Overview
Background
- CrabbyAvif
- the Rust AVIF decoder Chrome uses for AVIF images, which stores decoded pixels in
Imageplane buffers. - `Pixels`
- an enum (
src/utils/pixels.rs:21) holding plane data as either a borrowedPointer/Pointer16or an ownedBuffer/Buffer16. - `allocate_planes_with_default_values`
src/image.rs:378routine that, per plane in a category, sizes and fills the plane’s owned buffer before pixels are written into it.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Buffer size matches plane_size when skipped, so overflow must come from stale row_bytes/stride. That is unshown. The libyuv writer and how the commit-message alpha-plane case arises are not in the material.
a103c0de433f. An earlier analysis was withdrawn because it had been written against a dependency roll (4e749ba7969e).allocate_planes_with_default_values iterates the planes of a Category, computes plane_size = width * height for each (src/image.rs:388), and is supposed to (re)allocate an owned buffer sized to the current dimensions. Pre-fix, src/image.rs:389-395 short-circuited this with if self.planes[plane_index].is_some() && self.planes[plane_index].unwrap_ref().size() == plane_size && (pixel_bit_size()==0 || pixel_bit_size()==pixel_size*8) { continue; }, skipping the allocation whenever a plane already held a buffer whose element count matched. When that continue fired, the body at src/image.rs:401-404 — pixels.resize(plane_size, ...) (src/utils/pixels.rs:82), the self.row_bytes[plane_index] update, and self.image_owns_planes[plane_index] = true — never ran. Because the skip condition at src/image.rs:390 requires size() == plane_size, the retained buffer’s element count already equals a fresh allocation; what is left stale is the plane’s row_bytes stride (:403), its default-value fill (:402), and its image_owns_planes flag (:404), none of which are refreshed for the current geometry. As the commit message states, this “may inadvertently not allocate the memory in some cases (e.g. alpha plane when the color planes have already been allocated).” A subsequent writer that fills/scales the plane using the stale row_bytes stride then writes past the buffer, matching the advisory’s heap-buffer-overflow WRITE in Codecs (inferred — the libyuv scaler that performs this write is not in the shown material).
The fix deletes the entire conditional skip (diff removes src/image.rs:389-395) so the buffer is always freshly allocated via resize and row_bytes is always reset, and removes the now-unused pixel_bit_size helper (src/utils/pixels.rs:64-71).
plane_size, but left the plane’s row_bytes stride, default fill, and ownership flag stale for the current geometry; removing the skip forces every needed plane to be reallocated and its row_bytes reset.Attack Path
- Supply a crafted AVIF
The renderer decodes an attacker-controlled AVIF whose plane allocation reuses an
Imageacross categories/calls. (inferred — reuse pattern) - Hit the skip branch
A plane whose existing buffer size matches
plane_sizetriggerscontinueatsrc/image.rs:394, so itsrow_bytes, default fill, and ownership flag are never refreshed for the current geometry. - Write past the buffer
Later pixel production writes rows of that plane using the stale
row_bytesstride, which no longer matches the current geometry, overflowing the buffer (advisory: heap-buffer-overflow WRITE). (inferred — the writer is not in the shown material)
Impact Assessment
Files Changed
src/image.rssrc/utils/pixels.rs
Audit Directions
- Conditional (re)allocationFlag “skip allocation if a buffer already exists” optimizations that compare only element count, since they can leave size, stride, or depth metadata stale.
- Buffer/stride couplingVerify that any path which skips a buffer allocation also keeps the companion
row_bytes/stride and ownership flags consistent with the current dimensions.
Patch
diff --git a/src/image.rs b/src/image.rs
index 88e5216..a611822 100644
--- a/src/image.rs
+++ b/src/image.rs
@@ -386,13 +386,6 @@
let plane_index = plane.as_usize();
let width = round2_usize(self.width(plane));
let plane_size = checked_mul!(width, round2_usize(self.height(plane)))?;
- if self.planes[plane_index].is_some()
- && self.planes[plane_index].unwrap_ref().size() == plane_size
- && (self.planes[plane_index].unwrap_ref().pixel_bit_size() == 0
- || self.planes[plane_index].unwrap_ref().pixel_bit_size() == pixel_size * 8)
- {
- continue;
- }
self.planes[plane_index] = Some(if self.depth == 8 {
Pixels::Buffer(Vec::new())
} else {
diff --git a/src/utils/pixels.rs b/src/utils/pixels.rs
index e5002bc..5f78e16 100644
--- a/src/utils/pixels.rs
+++ b/src/utils/pixels.rs
@@ -61,15 +61,6 @@
}
}
- pub(crate) fn pixel_bit_size(&self) -> usize {
- match self {
- Pixels::Pointer(_) => 0,
- Pixels::Pointer16(_) => 0,
- Pixels::Buffer(_) => 8,
- Pixels::Buffer16(_) => 16,
- }
- }
-
pub(crate) fn has_data(&self) -> bool {
match self {
Pixels::Pointer(ptr) => !ptr.is_empty(),
Original Bug Report
blink_avif_decoder_fuzzer: Heap-buffer-overflow in InterpolateRow_Any_AVX2
Detailed Report: https://clusterfuzz.com/testcase?key=5869883250114560
Fuzzing Engine: centipede Fuzz Target: blink_avif_decoder_fuzzer Job Type: centipede_chrome_asan Platform Id: linux
Crash Type: Heap-buffer-overflow WRITE 1 Crash Address: 0x782a69fe8f80 Crash State: InterpolateRow_Any_AVX2 libyuv::ScalePlaneBilinearUp ScalePlane
Sanitizer: address (ASAN)
Recommended Security Severity: High
Regressed: https://clusterfuzz.com/revisions?job=centipede_chrome_asan&range=1567088:1567158
Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=5869883250114560
Issue filed automatically.
See https://chromium.googlesource.com/chromium/src/+/master/testing/libfuzzer/reproducing.md for instructions on reproducing this bug locally.