Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Codecs
DescriptionUse after free in Codecs
ComponentCodecs
Bug ClassUAF
Tracker506387278
Fix commit75293062587d (chromium/src) +1/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Files Changed

  • third_party/crabbyavif/BUILD.gn
From 75293062587d0704a2614f9ce59346d0cc83a1af Mon Sep 17 00:00:00 2001
From: Vignesh Venkat <vigneshv@google.com>
Date: Mon, 27 Apr 2026 16:52:16 -0700
Subject: [PATCH] crabbyavif, BUILD.gn: Allowlist dav1d_data_create

This will be needed when
https://github.com/webmproject/CrabbyAvif/pull/812 is merged and
autorolled into Chromium.

Bug: 506387278
Change-Id: I763001d2770220338fc5f2e634d08e45e1f00081
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7797801
Reviewed-by: James Zern <jzern@google.com>
Commit-Queue: James Zern <jzern@google.com>
Auto-Submit: Vignesh Venkat <vigneshv@google.com>
Cr-Commit-Position: refs/heads/main@{#1621426}
---

diff --git a/third_party/crabbyavif/BUILD.gn b/third_party/crabbyavif/BUILD.gn
index 718cf757..b7a012f 100644
--- a/third_party/crabbyavif/BUILD.gn
+++ b/third_party/crabbyavif/BUILD.gn
@@ -15,6 +15,7 @@
     "generate=functions,types,vars,methods,constructors,destructors",
     "allowlist-item=DAV1D_MAX_THREADS",
     "allowlist-item=dav1d_close",
+    "allowlist-item=dav1d_data_create",
     "allowlist-item=dav1d_data_unref",
     "allowlist-item=dav1d_data_wrap",
     "allowlist-item=dav1d_default_settings",
Loading diff…

Original Bug Report

reported by vm...@google.com

Renderer Heap UAF Read in AVIF Image Decoder via dav1d

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential heap Use-After-Free (UAF) read vulnerability exists in the renderer process when decoding AVIF images. The issue is caused by a buffer lifetime mismatch between Chromium’s AVIFImageDecoder and the underlying dav1d decoder, where dav1d retains raw pointers to a non-persistent buffer that is later reallocated. An attacker could exploit this by serving a crafted AVIF image to leak renderer heap memory via decoded pixels.

Affected files:

  • third_party/crabbyavif/src/src/codecs/dav1d.rs
  • third_party/blink/renderer/platform/image-decoders/avif/avif_image_decoder.cc
  • third_party/crabbyavif/src/src/decoder/mod.rs
  • third_party/crabbyavif/src/src/decoder/tile.rs

Estimated timestamp from git blame: 2025-03-21

Summary

A potential heap Use-After-Free (UAF) read vulnerability exists in the renderer process when decoding crafted AVIF images. The issue occurs due to a lifetime mismatch between a non-persistent I/O buffer managed by Chromium’s AVIFImageDecoder and the internal state maintained by the dav1d C library, mediated by the crabbyavif wrapper.

Technical Details

In third_party/blink/renderer/platform/image-decoders/avif/avif_image_decoder.cc, the avifIO interface is configured with avif_io_.persistent = false (line 780). When reading fragmented samples, AVIFImageDecoder::ReadFromSegmentReader clears and populates a temporary std::vector<uint8_t> (io_data->buffer).

The crabbyavif library takes this buffer and wraps it for the dav1d decoder via dav1d_data_wrap in third_party/crabbyavif/src/src/codecs/dav1d.rs. Crucially, it provides a no-op free callback (avif_dav1d_free_callback), assuming the caller manages the buffer’s lifecycle correctly for the duration of the decoding.

When dav1d_send_data parses a sample containing a complete frame followed by trailing Open Bitstream Units (OBUs) for an incomplete subsequent frame, it stores raw pointers to the trailing OBUs within its context (e.g., c->tile[n].data in third_party/dav1d/libdav1d/src/obu.c). The complete frame is returned to Chromium, but dav1d internally retains these pointers.

When Chromium requests the next frame, it fetches the next sample. If this sample is larger than the previous one, io_data->buffer.reserve(size) is called, triggering a reallocation of the std::vector backing store. The original memory containing the trailing OBUs is freed. However, because the free callback provided to dav1d was a no-op, dav1d’s internal refcounting system is unaware that the memory has been freed.

When dav1d resumes decoding to complete the pending frame, it dereferences the stale pointers in its context (e.g., in dav1d_decode_frame_init_cdf), resulting in a Use-After-Free read.

Impact

An attacker can trigger this heap UAF read in the sandboxed renderer process. By interpreting freed heap memory as AV1 entropy-coded tile data, the decoded pixels can be read back by the attacker via JavaScript (e.g., drawing to an HTML <canvas> and calling getImageData()). This leaks renderer heap contents and provides a powerful primitive for bypassing ASLR.

Potential Reproduction Steps

Note: These are potential steps based on static code analysis.

  1. Create an AVIF image with at least two samples (e.g., an animated AVIF).
  2. Format the data to force Chromium’s SegmentReader to use the non-contiguous slow path (e.g., by serving the image over HTTP in small chunks).
  3. Structure the first sample to contain a complete AV1 frame followed by a trailing, incomplete second frame (e.g., a frame header OBU).
  4. Structure the second sample to contain the rest of the second frame, ensuring it has a larger byte size than the first sample to trigger std::vector::reserve reallocation.
  5. Load the image in Chrome. The decoder will trigger a UAF read when processing the second sample as dav1d reads from the freed buffer of the first sample.

Suggested Fix

There are a few ways to resolve this buffer lifetime mismatch:

  1. Implement Proper Refcounting: Instead of using a no-op free callback, crabbyavif’s IO abstraction should be refactored to allow dav1d to take ownership or increment a reference count on the underlying backing store (e.g., by wrapping the C++ std::vector in a refcounted C wrapper passed through FFI).
  2. Copy on Non-Persistent IO: In third_party/crabbyavif/src/src/codecs/dav1d.rs, if avifIO is marked as non-persistent, Dav1d::get_next_image should allocate its own buffer and copy the av1_payload before wrapping it for dav1d, rather than performing a zero-copy wrap.
  3. Force Dav1d Flush: Ensure that Dav1d::flush actually calls the C API dav1d_flush(), which clears all internal pointers and incomplete frames, though this may break decoding of frames spread across multiple dav1d_send_data calls.

Evaluated with Chrome root at commit: a1e33f5848218e21d4a16ae2c1bc94e815c30c7f


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker