High watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Video
DescriptionUse after free in Video
ComponentVideo
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker495477995
Fix commita047955845e5 (aom) +58/-20
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedSyn4pse
Reported2026-03-24 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-04-15
Patch gap16 days — fix public in source before the advisory

Background

`av1_scale_references`
Pre-encode pass that allocates and populates SCALED reference-frame buffers, but ONLY for references that will actually be used during encoding.
`force_mv_inter_layer`
Output flag of skip_inter_mode_nonrd signaling that inter-layer motion is forced in SVC (spatial scalable) real-time encoding.
SVC (spatial scalability)
AV1 mode encoding multiple spatial layers (spatial_layer_id > 0), where a layer may reference a rescaled version of another layer’s frame.
`scaled_ref`
A per-reference scaled buffer fetched via av1_get_scaled_ref_frame; valid only when av1_scale_references actually scaled that reference for the current frame.

Root Cause Analysis

In skip_inter_mode_nonrd, the block that computes *force_mv_inter_layer (the *force_mv_inter_layer = 0; if (use_svc && spatial_layer_id > 0 && skip_mvsearch_*) {...} logic) was positioned AFTER several early return true (skip-mode) paths. When one of those early returns fired, force_mv_inter_layer was left in an uninitialized/stale state. A later stage, combined_motion_search within pickmode, then consulted this flag to decide whether to use a scaled reference, and proceeded to fetch a scaled_ref for a reference that av1_scale_references had never scaled or allocated for this frame. Because the scaled-buffer slot was never populated (or held a stale pointer to a freed buffer from a prior frame), dereferencing it produced a use-after-free.

The bug is a control-flow ordering defect: an output contract (force_mv_inter_layer must always be set) was violated on the early-exit paths.

The fix moves the flag-setup block before the early returns so the value is always defined, and adds asserts binding av1_is_scaled to a non-null, dimension-matched scaled ref.

Key insight
An out-parameter left unset on an early-return path let a downstream consumer act on stale state, causing pickmode to dereference a scaled reference buffer that av1_scale_references never allocated. The invariant “scaled ref is valid iff scaling was requested” was silently broken by ordering, not by a bad computation.

Attack Path

  1. Configure SVC encode Drive AV1 real-time encoding with multiple spatial layers so spatial_layer_id > 0.
  2. Trigger early skip Craft content/parameters so skip_inter_mode_nonrd takes an early return true path before force_mv_inter_layer was originally set.
  3. Leave flag stale force_mv_inter_layer retains an uninitialized/prior value, mismatching what av1_scale_references actually scaled.
  4. Enter combined motion search Pickmode reaches combined_motion_search and selects the scaled-reference path based on the stale flag.
  5. Dereference stale scaled ref The never-allocated/freed scaled_ref buffer is dereferenced, yielding a use-after-free.

Impact Assessment

A use-after-free in the AV1 encoder path reachable through SVC real-time encoding, potentially triggerable in WebRTC/WebCodecs encode scenarios that expose libaom encoding to attacker-influenced configuration and content. Exploitation could lead to memory corruption and, chained with further primitives, code execution in the process hosting the encoder. Severity is tempered by the encode-side (rather than decode-side) reachability and the specific SVC multi-layer configuration required.

Changed Functions

FunctionChangeNotes
if
av1/encoder/nonrd_pickmode.c
modified
if
test/svc_datarate_test.cc
modified
TEST_P
test/svc_datarate_test.cc
modified

Files Changed

  • av1/encoder/nonrd_pickmode.c
  • test/svc_datarate_test.cc

Audit Directions

  • Out-parameters on early-return paths
    Audit functions that set output flags/pointers late; ensure every return path (especially early skips) leaves out-params defined before any consumer reads them.
  • Scaled-reference validity invariants
    Review all av1_get_scaled_ref_frame / av1_is_scaled consumers in nonrd and rd pickmode to confirm they only fetch scaled buffers for references av1_scale_references actually allocated for the current frame.
  • Cross-frame stale buffer reuse
    Look for encoder state (flags, buffer pointers) carried across frames or layers without reinitialization, where a prior frame’s freed/rescaled buffer could be dereferenced.
From a047955845e50e43786d51cdefcfc9e87804ed61 Mon Sep 17 00:00:00 2001
From: Marco Paniconi <[email protected]>
Date: Sun, 29 Mar 2026 20:27:20 -0700
Subject: [PATCH] Set force_mv_inter_layer earlier in skip_inter_mode

For nonrd_pickmode: move the setting of
force_mv_inter_layer earlier in the
skip_inter_mode_nonrd(), to make sure it always
get set (in case of false return in that function).

Thie prevents the usage of a scaled_ref in pickmode
(combined_motion search) when it has actually not been
set/scaled in av1_scale_references (before encoding).

Fixes a crash for use after free (UAF), reported
in the issues below.

Added svc unittest to generate the issue. Also added
assert check for scaled_ref in combined_motion_search.

Bug: 495477995, 495996858
Change-Id: I578d19156d97a50546edc9422bc3581566f1236e
---

diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index 0f2a1c7..942b8ab 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -192,7 +192,7 @@
                                   int *rate_mv, int64_t best_rd_sofar,
                                   int use_base_mv) {
   MACROBLOCKD *xd = &x->e_mbd;
-  const AV1_COMMON *cm = &cpi->common;
+  AV1_COMMON *cm = &cpi->common;
   const SPEED_FEATURES *sf = &cpi->sf;
   MB_MODE_INFO *mi = xd->mi[0];
   int step_param = (sf->rt_sf.fullpel_search_step_param)
@@ -207,6 +207,14 @@
   int cost_list[5];
   int search_subpel = 1;
 
+  if (av1_is_scaled(get_ref_scale_factors(cm, ref))) {
+    const YV12_BUFFER_CONFIG *scaled_ref = av1_get_scaled_ref_frame(cpi, ref);
+    (void)scaled_ref;
+    assert(scaled_ref != NULL);
+    assert(scaled_ref->y_crop_width == cm->width &&
+           scaled_ref->y_crop_height == cm->height);
+  }
+
   start_mv = get_fullmv_from_mv(&ref_mv);
 
   if (!use_base_mv)
@@ -2490,6 +2498,23 @@
       (*this_mode != GLOBALMV || *ref_frame != LAST_FRAME))
     return true;
 
+  *force_mv_inter_layer = 0;
+  if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
+      ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
+       (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
+       (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
+    // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
+    // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
+    // Skip newmv and filter search.
+    *force_mv_inter_layer = 1;
+    if (*this_mode == NEWMV) {
+      search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
+    } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
+               svc_mv.as_int) {
+      return true;
+    }
+  }
+
   // If the segment reference frame feature is enabled then do nothing if the
   // current ref frame is not allowed.
   if (segfeature_active(seg, segment_id, SEG_LVL_REF_FRAME)) {
@@ -2565,23 +2590,6 @@
     return true;
   }
 
-  *force_mv_inter_layer = 0;
-  if (cpi->ppi->use_svc && svc->spatial_layer_id > 0 &&
-      ((*ref_frame == LAST_FRAME && svc->skip_mvsearch_last) ||
-       (*ref_frame == GOLDEN_FRAME && svc->skip_mvsearch_gf) ||
-       (*ref_frame == ALTREF_FRAME && svc->skip_mvsearch_altref))) {
-    // Only test mode if NEARESTMV/NEARMV is (svc_mv.mv.col, svc_mv.mv.row),
-    // otherwise set NEWMV to (svc_mv.mv.col, svc_mv.mv.row).
-    // Skip newmv and filter search.
-    *force_mv_inter_layer = 1;
-    if (*this_mode == NEWMV) {
-      search_state->frame_mv[*this_mode][*ref_frame] = svc_mv;
-    } else if (search_state->frame_mv[*this_mode][*ref_frame].as_int !=
-               svc_mv.as_int) {
-      return true;
-    }
-  }
-
   // For screen content: skip mode testing based on source_sad.
   if (cpi->oxcf.tune_cfg.content == AOM_CONTENT_SCREEN &&
       !x->force_zeromv_skip_for_blk) {
diff --git a/test/svc_datarate_test.cc b/test/svc_datarate_test.cc
index 0df6782..2f68ba7 100644
--- a/test/svc_datarate_test.cc
+++ b/test/svc_datarate_test.cc
@@ -247,6 +247,7 @@
     external_resize_pattern_ = 0;
     dynamic_tl_ = false;
     dynamic_scale_factors_ = false;
+    disable_last_ref_ = false;
   }
 
   void PreEncodeFrameHook(::libaom_test::VideoSource *video,
@@ -302,7 +303,7 @@
         spatial_layer_id, multi_ref_, comp_pred_,
         (video->frame() % cfg_.kf_max_dist) == 0, dynamic_enable_disable_mode_,
         rps_mode_, rps_recovery_frame_, simulcast_mode_, use_last_as_scaled_,
-        use_last_as_scaled_single_ref_);
+        use_last_as_scaled_single_ref_, disable_last_ref_);
     if (intra_only_ == 1 && frame_sync_ > 0) {
       // Set an Intra-only frame on SL0 at frame_sync_.
       // In order to allow decoding to start on SL0 in mid-sequence we need to
@@ -964,7 +965,7 @@
       int multi_ref, int comp_pred, int is_key_frame,
       int dynamic_enable_disable_mode, int rps_mode, int rps_recovery_frame,
       int simulcast_mode, bool use_last_as_scaled,
-      bool use_last_as_scaled_single_ref) {
+      bool use_last_as_scaled_single_ref, bool disable_last_ref) {
     int lag_index = 0;
     int base_count = frame_cnt >> 2;
     layer_id->spatial_layer_id = spatial_layer;
@@ -1164,6 +1165,11 @@
     if (dynamic_enable_disable_mode == 1 &&
         layer_id->spatial_layer_id == number_spatial_layers_ - 1)
       ref_frame_config->reference[0] = 0;
+    // Always disable LAST reference under this flag. use GOLDEN reference.
+    if (disable_last_ref) {
+      ref_frame_config->reference[0] = 0;
+      ref_frame_config->reference[3] = 1;
+    }
     return layer_flags;
   }
 
@@ -1508,6 +1514,23 @@
     CheckDatarate(0.80, 1.60);
   }
 
+  virtual void BasicRateTargetingSVC1TL2SLDisableLASTTest() {
+    SetUpCbr();
+    cfg_.g_error_resilient = 0;
+
+    ::libaom_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352,
+                                         288, 30, 1, 0, 300);
+    const int bitrate_array[2] = { 300, 600 };
+    cfg_.rc_target_bitrate = bitrate_array[GET_PARAM(4)];
+    ResetModel();
+    disable_last_ref_ = true;
+    screen_mode_ = true;
+    ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+#if CONFIG_AV1_DECODER
+    EXPECT_EQ((int)GetMismatchFrames(), 0);
+#endif
+  }
+
   virtual void BasicRateTargetingSVC3TL3SLIntraStartDecodeBaseMidSeq() {
     SetUpCbr();
     cfg_.rc_max_quantizer = 56;
@@ -2380,6 +2403,7 @@
   int external_resize_pattern_;
   bool dynamic_tl_;
   bool dynamic_scale_factors_;
+  bool disable_last_ref_;
 };
 
 // Check basic rate targeting for CBR, for 3 temporal layers, 1 spatial.
@@ -2458,6 +2482,12 @@
   BasicRateTargetingSVC1TL2SLTest();
 }
 
+// Check basic rate targeting for CBR, for 2 spatial layers, 1 temporal.
+// Disable the usage of LAST referenc frame.
+TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SLDisableLAST) {
+  BasicRateTargetingSVC1TL2SLDisableLASTTest();
+}
+
 // Check basic rate targeting for CBR, for 3 spatial layers, 3 temporal,
 // with Intra-only frame inserted in the stream. Verify that we can start
 // decoding the SL0 stream at the intra_only frame in mid-sequence.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/test/svc_datarate_test.cc b/test/svc_datarate_test.cc
index 0df6782..2f68ba7 100644
--- a/test/svc_datarate_test.cc
+++ b/test/svc_datarate_test.cc
@@ -247,6 +247,7 @@
     external_resize_pattern_ = 0;
     dynamic_tl_ = false;
     dynamic_scale_factors_ = false;
+    disable_last_ref_ = false;
   }
 
   void PreEncodeFrameHook(::libaom_test::VideoSource *video,
@@ -302,7 +303,7 @@
         spatial_layer_id, multi_ref_, comp_pred_,
         (video->frame() % cfg_.kf_max_dist) == 0, dynamic_enable_disable_mode_,
         rps_mode_, rps_recovery_frame_, simulcast_mode_, use_last_as_scaled_,
-        use_last_as_scaled_single_ref_);
+        use_last_as_scaled_single_ref_, disable_last_ref_);
     if (intra_only_ == 1 && frame_sync_ > 0) {
       // Set an Intra-only frame on SL0 at frame_sync_.
       // In order to allow decoding to start on SL0 in mid-sequence we need to
@@ -964,7 +965,7 @@
       int multi_ref, int comp_pred, int is_key_frame,
       int dynamic_enable_disable_mode, int rps_mode, int rps_recovery_frame,
       int simulcast_mode, bool use_last_as_scaled,
-      bool use_last_as_scaled_single_ref) {
+      bool use_last_as_scaled_single_ref, bool disable_last_ref) {
     int lag_index = 0;
     int base_count = frame_cnt >> 2;
     layer_id->spatial_layer_id = spatial_layer;
@@ -1164,6 +1165,11 @@
     if (dynamic_enable_disable_mode == 1 &&
         layer_id->spatial_layer_id == number_spatial_layers_ - 1)
       ref_frame_config->reference[0] = 0;
+    // Always disable LAST reference under this flag. use GOLDEN reference.
+    if (disable_last_ref) {
+      ref_frame_config->reference[0] = 0;
+      ref_frame_config->reference[3] = 1;
+    }
     return layer_flags;
   }
 
@@ -1508,6 +1514,23 @@
     CheckDatarate(0.80, 1.60);
   }
 
+  virtual void BasicRateTargetingSVC1TL2SLDisableLASTTest() {
+    SetUpCbr();
+    cfg_.g_error_resilient = 0;
+
+    ::libaom_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352,
+                                         288, 30, 1, 0, 300);
+    const int bitrate_array[2] = { 300, 600 };
+    cfg_.rc_target_bitrate = bitrate_array[GET_PARAM(4)];
+    ResetModel();
+    disable_last_ref_ = true;
+    screen_mode_ = true;
+    ASSERT_NO_FATAL_FAILURE(RunLoop(&video));
+#if CONFIG_AV1_DECODER
+    EXPECT_EQ((int)GetMismatchFrames(), 0);
+#endif
+  }
+
   virtual void BasicRateTargetingSVC3TL3SLIntraStartDecodeBaseMidSeq() {
     SetUpCbr();
     cfg_.rc_max_quantizer = 56;
@@ -2380,6 +2403,7 @@
   int external_resize_pattern_;
   bool dynamic_tl_;
   bool dynamic_scale_factors_;
+  bool disable_last_ref_;
 };
 
 // Check basic rate targeting for CBR, for 3 temporal layers, 1 spatial.
@@ -2458,6 +2482,12 @@
   BasicRateTargetingSVC1TL2SLTest();
 }
 
+// Check basic rate targeting for CBR, for 2 spatial layers, 1 temporal.
+// Disable the usage of LAST referenc frame.
+TEST_P(DatarateTestSVC, BasicRateTargetingSVC1TL2SLDisableLAST) {
+  BasicRateTargetingSVC1TL2SLDisableLASTTest();
+}
+
 // Check basic rate targeting for CBR, for 3 spatial layers, 3 temporal,
 // with Intra-only frame inserted in the stream. Verify that we can start
 // decoding the SL0 stream at the intra_only frame in mid-sequence.
Loading diff…

Original Bug Report

reported by [email protected]

UAF in SVC reconfiguration of AV1 encoder

Summary

When the AV1 encoder undergoes SVC reconfiguration with concurrent resolution changes, row-mt worker threads continue access per-block state from a prior encode configuration that has already been freed, leading to the UAF.

Details

The WebRTC SVC spec exposes scalabilityMode as a mutable property on RTCRtpEncodingParameters. Blink forwards this string directly into the native layer in RTCRtpSender::ToRtpParameters:

if (encoding->hasScalabilityMode()) {
  webrtc_encoding.scalability_mode = encoding->scalabilityMode().Utf8();
}

WebRTC maps the mode string into AV1 SVC configuration via SetAv1SvcConfig, which also clamps spatial layers against the current frame dimensions:

if (ScalabilityMode reduced = LimitNumSpatialLayers(
        *scalability_mode,
        GetLimitedNumSpatialLayers(video_codec.width, video_codec.height));
    *scalability_mode != reduced) {
  scalability_mode = reduced;
}

Any scalabilityMode change forces a full encoder reset via VideoStreamEncoder::RequiresEncoderReset:

if (new_send_codec.GetScalabilityMode() !=
    prev_send_codec.GetScalabilityMode()) {
  return true;
}

This means that mode changes combined with resolution changes (which alter the clamped spatial layer count) can produce a rapid sequence of encoder resets.

The encoder reset tears down and rebuilds internal AV1 state, but does not ensure that in-flight row-mt worker threads have drained before freeing backing buffers. The non-RD encode path follows this call chain on worker threads:

enc_row_mt_worker_hook → av1_encode_sb_row → av1_nonrd_use_partition → pick_sb_modes_nonrd → av1_nonrd_pick_inter_mode_sb → av1_block_yrd

Within av1_block_yrd, the worker reads per-block coefficient and reference buffers. The local buffers are declared by the DECLARE_BLOCK_YRD_BUFFERS() macro in nonrd_opt.c. When an encoder reset frees or recycles the backing encoder state while a worker thread is still executing this path, the worker access freed memory, leading to the UAF.

Reproduction

Run chromium (e.g., https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1603396.zip) with:

chrome --no-sandbox poc.html

You would observe the UAF shown in asan.txt.

View on issue tracker