High watch CVSS 8.3 chrome UAF 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromecast
DescriptionUse after free in Chromecast
ComponentChromecast
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.
Tracker534997484
Fix commite2f094b952dc (chromium/src) +251/-17
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-15 (per vendor advisory)
Fix landed2026-09-17 (public git)
Disclosed2026-09-22
Patch gap5 days — fix public in source before the advisory

Background

`BackendDecryptor`
A chromecast media pipeline component that wraps a MediaPipelineBackend::AudioDecryptor to decrypt encrypted audio buffers and return the decrypted results through a callback.
`DecryptCB` (`decrypt_cb_`)
The client-supplied callback that BackendDecryptor runs to deliver decrypted ready_buffers_ back to the pipeline.
Member destruction order
In C++, a class’s non-static data members are destroyed in the reverse of their declaration order, so a member declared later is torn down before one declared earlier.
Synchronous delegate notification
When decryptor_ is destroyed it may abort in-flight decrypt operations and, before returning, synchronously call back into its delegate (BackendDecryptor), which touches decrypt_cb_ and task_runner_.

Root Cause Analysis

In backend_decryptor.h, decrypt_cb_ was declared after both task_runner_ and decryptor_, which meant that during BackendDecryptor destruction decrypt_cb_ was torn down first while decryptor_ was destroyed afterward. When decryptor_’s destructor aborted pending decrypt operations, it could synchronously notify its delegate, driving BackendDecryptor::OnPushBufferForDecryptComplete (or OnDecryptComplete) to access the already-destroyed decrypt_cb_ (and task_runner_), producing a use-after-free.

The fix reorders the members so decrypt_cb_ and task_runner_ are declared before decryptor_, guaranteeing the callback and task runner outlive the decryptor and remain valid when it fires its delegate callbacks during teardown. The change also hardens the callback sites by replacing DCHECK(decrypt_cb_) with runtime if (decrypt_cb_) guards and by turning DCHECK(!pending_buffers_.empty()) in OnDecryptComplete into an early return, so a late or unexpected notification no longer dereferences invalid state.

Key insight
The single core mistake is a member declaration order that let decrypt_cb_ (and task_runner_) be destroyed before the decryptor_ that calls back into them during its own destruction; reordering the members so the dependencies are declared first ensures they outlive decryptor_ and are still alive when it synchronously notifies its delegate.

Attack Path

  1. Feed encrypted audio to the pipeline Attacker-influenced encrypted media is routed through the chromecast CMA pipeline so that BackendDecryptor::Decrypt pushes buffers into decryptor_ and leaves operations pending (kBufferPending).
  2. Trigger destruction with work outstanding Tearing down or resetting the pipeline while decrypt operations are still pending causes ~BackendDecryptor to run with a live decryptor_ and unflushed pending_buffers_.
  3. Provoke synchronous abort callback As decryptor_ is destroyed it aborts the pending operation and synchronously notifies its delegate, invoking OnPushBufferForDecryptComplete/OnDecryptComplete on the partially-destroyed BackendDecryptor.
  4. Use-after-free on `decrypt_cb_` Because decrypt_cb_ was already destroyed (declared after decryptor_), running or dereferencing it accesses freed callback/binding state, the corrupted condition the patch removes.
  5. Escalation Controlling the freed callback’s backing allocation could be leveraged toward a stronger primitive (inferred).

Impact Assessment

An attacker who can drive encrypted media through the Chromecast media pipeline can cause a use-after-free of the decrypt callback during BackendDecryptor teardown, corrupting or dereferencing freed heap state in the media/pipeline process. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any progression from the freed callback to a read/write primitive or code execution is (inferred) rather than demonstrated by the patch. Preconditions are the ability to reach the decrypt path and to trigger destruction while decrypt operations remain pending.

Changed Functions

FunctionChangeNotes
task_runner_
chromecast/media/cma/pipeline/backend_decryptor.cc
modified
if
chromecast/media/cma/pipeline/backend_decryptor.cc
modified
TestAudioDecryptor
chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
modified
TestAudioDecryptor
chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
modified
if
chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
modified
BackendDecryptorTest
chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
modified
TEST_F
chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
modified

Files Changed

  • chromecast/media/cma/pipeline/backend_decryptor.cc
  • chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
  • chromecast/media/cma/BUILD.gn
  • chromecast/media/cma/pipeline/backend_decryptor.h

Audit Directions

  • Member ordering vs. callback lifetime
    Audit classes that own a backend/decryptor object plus the callbacks and task runners it invokes, checking that dependency members are declared before the object so they outlive its destruction-time notifications.
  • Synchronous callbacks during destruction
    Look for destructors or abort/flush paths that synchronously notify a delegate, and verify no member the callback touches has already been torn down.
  • DCHECK-guarded invariants on shared state
    Flag DCHECK(callback) or DCHECK(!container.empty()) guards on state that can legitimately change during teardown, since these become no-ops in release builds and mask real use-after-free or empty-access paths.
From e2f094b952dcc91519eb8c7122c0b3aed203613d Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Thu, 17 Sep 2026 09:09:26 -0700
Subject: [PATCH] Ensure callback outlives decryptor in BackendDecryptor

Reorder class members in BackendDecryptor so that decrypt_cb_ and
task_runner_ are declared before decryptor_. Because member destruction
proceeds in reverse declaration order, this ensures that callback and
task runner dependencies remain valid if the decryptor aborts pending
operations and synchronously notifies its delegate during destruction.

Additionally, add checks before executing decrypt_cb_, validate that
pending_buffers_ is not empty in OnDecryptComplete, remove a redundant
TaskRunnerImpl allocation in the constructor, and add unit tests.

Fixed: 534997484
Change-Id: Ie4a45392bb55c7b48e108136c87b35427f1f4d2d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8349791
Commit-Queue: Andrew Paseltiner <[email protected]>
Reviewed-by: Shawn Quereshi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1699864}
---
diff --git a/chromecast/media/cma/pipeline/backend_decryptor.cc b/chromecast/media/cma/pipeline/backend_decryptor.cc
index 59e4843..19f32fc 100644
--- a/chromecast/media/cma/pipeline/backend_decryptor.cc
+++ b/chromecast/media/cma/pipeline/backend_decryptor.cc
@@ -17,10 +17,9 @@
 BackendDecryptor::BackendDecryptor(EncryptionScheme scheme)
     : decrypt_success_(true),
       wait_eos_(false),
-      task_runner_(new TaskRunnerImpl) {
+      task_runner_(std::make_unique<TaskRunnerImpl>()) {
   DCHECK(MediaPipelineBackend::CreateAudioDecryptor);
 
-  task_runner_ = std::make_unique<TaskRunnerImpl>();
   decryptor_ = base::WrapUnique(
       MediaPipelineBackend::CreateAudioDecryptor(scheme, task_runner_.get()));
 
@@ -43,32 +42,38 @@
   // also needed so that the last buffer can be flushed.
   pending_buffers_.push(buffer);
 
-  if (buffer->end_of_stream())
+  if (buffer->end_of_stream()) {
     wait_eos_ = true;
+  }
 
   MediaPipelineBackend::BufferStatus status = decryptor_->PushBufferForDecrypt(
       buffer.get(),
       buffer->end_of_stream() ? nullptr : buffer->writable_data());
 
-  if (status != MediaPipelineBackend::kBufferPending)
+  if (status != MediaPipelineBackend::kBufferPending) {
     OnPushBufferForDecryptComplete(status);
+  }
 }
 
 void BackendDecryptor::OnPushBufferForDecryptComplete(
     MediaPipelineBackend::BufferStatus status) {
   // If the pushed buffer is EOS, the callback should be called when all the
   // buffers are decrypted.
-  if (wait_eos_)
+  if (wait_eos_) {
     return;
+  }
 
-  DCHECK(decrypt_cb_);
-  decrypt_cb_.Run(
-      decrypt_success_ && status == MediaPipelineBackend::kBufferSuccess,
-      std::move(ready_buffers_));
+  if (decrypt_cb_) {
+    decrypt_cb_.Run(
+        decrypt_success_ && status == MediaPipelineBackend::kBufferSuccess,
+        std::move(ready_buffers_));
+  }
 }
 
 void BackendDecryptor::OnDecryptComplete(bool success) {
-  DCHECK(!pending_buffers_.empty());
+  if (pending_buffers_.empty()) {
+    return;
+  }
 
   // Cache the success value and return it in OnPushBufferForDecryptComplete.
   decrypt_success_ &= success;
@@ -83,10 +88,11 @@
   if (wait_eos_ && buffer->end_of_stream()) {
     // Last frame, all the buffers should be decrypted.
     DCHECK(pending_buffers_.empty());
-    DCHECK(decrypt_cb_);
-    LOG(INFO) << "Return all the ready buffers, size = "
-              << ready_buffers_.size();
-    decrypt_cb_.Run(decrypt_success_, std::move(ready_buffers_));
+    if (decrypt_cb_) {
+      LOG(INFO) << "Return all the ready buffers, size = "
+                << ready_buffers_.size();
+      decrypt_cb_.Run(decrypt_success_, std::move(ready_buffers_));
+    }
   }
 }
 
diff --git a/chromecast/media/cma/pipeline/backend_decryptor.h b/chromecast/media/cma/pipeline/backend_decryptor.h
index 222ead29..65a9977 100644
--- a/chromecast/media/cma/pipeline/backend_decryptor.h
+++ b/chromecast/media/cma/pipeline/backend_decryptor.h
@@ -45,11 +45,11 @@
   bool decrypt_success_;
   bool wait_eos_;
 
-  // |task_runner_| should have a longer life than |decryptor_|.
+  // |decrypt_cb_| and |task_runner_| should have a longer life than
+  // |decryptor_|.
+  DecryptCB decrypt_cb_;
   std::unique_ptr<TaskRunnerImpl> task_runner_;
   std::unique_ptr<MediaPipelineBackend::AudioDecryptor> decryptor_;
-
-  DecryptCB decrypt_cb_;
 };
 
 }  // namespace media
diff --git a/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc b/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
new file mode 100644
index 0000000..7890c28
--- /dev/null
+++ b/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
@@ -0,0 +1,227 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/media/cma/pipeline/backend_decryptor.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/test/task_environment.h"
+#include "chromecast/media/base/cast_decoder_buffer_impl.h"
+#include "chromecast/public/media/media_pipeline_backend.h"
+#include "chromecast/public/task_runner.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace chromecast {
+namespace media {
+
+namespace {
+
+enum class DecryptorMode {
+  kPendingAndAbortOnDestruction,
+  kImmediateSuccess,
+};
+
+DecryptorMode g_decryptor_mode = DecryptorMode::kPendingAndAbortOnDestruction;
+
+class TestAudioDecryptor;
+TestAudioDecryptor* g_last_decryptor = nullptr;
+
+// AudioDecryptor test implementation whose behavior is configurable for tests.
+class TestAudioDecryptor : public MediaPipelineBackend::AudioDecryptor {
+ public:
+  TestAudioDecryptor() { g_last_decryptor = this; }
+
+  ~TestAudioDecryptor() override {
+    if (g_last_decryptor == this) {
+      g_last_decryptor = nullptr;
+    }
+    if (g_decryptor_mode == DecryptorMode::kPendingAndAbortOnDestruction &&
+        has_pending_ && delegate_) {
+      delegate_->OnPushBufferForDecryptComplete(
+          MediaPipelineBackend::kBufferFailed);
+    }
+  }
+
+  void SetDelegate(Delegate* delegate) override { delegate_ = delegate; }
+
+  BufferStatus PushBufferForDecrypt(CastDecoderBuffer* buffer,
+                                    uint8_t* output) override {
+    if (g_decryptor_mode == DecryptorMode::kPendingAndAbortOnDestruction) {
+      has_pending_ = true;
+      return MediaPipelineBackend::kBufferPending;
+    }
+
+    if (g_decryptor_mode == DecryptorMode::kImmediateSuccess) {
+      if (delegate_) {
+        delegate_->OnDecryptComplete(true);
+      }
+      return MediaPipelineBackend::kBufferSuccess;
+    }
+
+    return MediaPipelineBackend::kBufferSuccess;
+  }
+
+  Delegate* delegate() const { return delegate_; }
+
+ private:
+  Delegate* delegate_ = nullptr;
+  bool has_pending_ = false;
+};
+
+void OnDecrypted(bool* called,
+                 bool* out_success,
+                 size_t* out_buffer_count,
+                 bool success,
+                 StreamDecryptor::BufferQueue buffers) {
+  if (called) {
+    *called = true;
+  }
+  if (out_success) {
+    *out_success = success;
+  }
+  if (out_buffer_count) {
+    *out_buffer_count = buffers.size();
+  }
+}
+
+}  // namespace
+
+// Provide the factory function so BackendDecryptor creates TestAudioDecryptor.
+MediaPipelineBackend::AudioDecryptor*
+MediaPipelineBackend::CreateAudioDecryptor(EncryptionScheme scheme,
+                                           TaskRunner* task_runner) {
+  return new TestAudioDecryptor();
+}
+
+class BackendDecryptorTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    g_decryptor_mode = DecryptorMode::kPendingAndAbortOnDestruction;
+    g_last_decryptor = nullptr;
+  }
+
+  void TearDown() override { g_last_decryptor = nullptr; }
+
+  base::test::TaskEnvironment task_environment_;
+};
+
+TEST_F(BackendDecryptorTest, DelegateNotificationOnDestruction) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = true;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, nullptr));
+
+  // Push a buffer; decryptor returns kBufferPending so completion is deferred.
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  // Reset decryptor. During destruction, AudioDecryptor notifies delegate.
+  decryptor.reset();
+
+  EXPECT_TRUE(called);
+  EXPECT_FALSE(success);
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithoutInit) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  // Push buffer without calling Init().
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  // Reset decryptor; should complete cleanly without initialized callback.
+  decryptor.reset();
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithoutPendingOperation) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Reset decryptor without pushing any buffers.
+  decryptor.reset();
+
+  EXPECT_FALSE(called);
+}
+
+TEST_F(BackendDecryptorTest, ImmediateSuccessfulDecryption) {
+  g_decryptor_mode = DecryptorMode::kImmediateSuccess;
+
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = false;
+  size_t buffer_count = 0;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, &buffer_count));
+
+  // Push a buffer; decryptor completes immediately.
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  EXPECT_TRUE(called);
+  EXPECT_TRUE(success);
+  EXPECT_EQ(buffer_count, 1u);
+}
+
+TEST_F(BackendDecryptorTest, EmptyPendingBuffersOnDecryptComplete) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Invoke OnDecryptComplete directly when no buffer is pending.
+  EXPECT_TRUE(g_last_decryptor);
+  if (g_last_decryptor && g_last_decryptor->delegate()) {
+    g_last_decryptor->delegate()->OnDecryptComplete(false);
+  }
+
+  EXPECT_FALSE(called);
+}
+
+TEST_F(BackendDecryptorTest, EndOfStreamDecryption) {
+  g_decryptor_mode = DecryptorMode::kImmediateSuccess;
+
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = false;
+  size_t buffer_count = 0;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, &buffer_count));
+
+  // Push an EOS buffer; decryptor completes and flushes all ready buffers.
+  decryptor->Decrypt(CastDecoderBufferImpl::CreateEOSBuffer());
+
+  EXPECT_TRUE(called);
+  EXPECT_TRUE(success);
+  EXPECT_EQ(buffer_count, 1u);
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithEosPending) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Push an EOS buffer with pending completion.
+  decryptor->Decrypt(CastDecoderBufferImpl::CreateEOSBuffer());
+
+  // Reset decryptor while waiting for EOS.
+  decryptor.reset();
+
+  EXPECT_FALSE(called);
+}
+
+}  // namespace media
+}  // namespace chromecast
diff --git a/chromecast/media/cma/BUILD.gn b/chromecast/media/cma/BUILD.gn
index fd90cc3..4df96eb 100644
--- a/chromecast/media/cma/BUILD.gn
+++ b/chromecast/media/cma/BUILD.gn
@@ -54,6 +54,7 @@
     "base/demuxer_stream_for_test.h",
     "base/multi_demuxer_stream_adapter_unittest.cc",
     "pipeline/audio_video_pipeline_impl_unittest.cc",
+    "pipeline/backend_decryptor_unittest.cc",
     "pipeline/media_pipeline_impl_unittest.cc",
   ]
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc b/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
new file mode 100644
index 0000000..7890c28
--- /dev/null
+++ b/chromecast/media/cma/pipeline/backend_decryptor_unittest.cc
@@ -0,0 +1,227 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/media/cma/pipeline/backend_decryptor.h"
+
+#include <memory>
+#include <utility>
+
+#include "base/functional/bind.h"
+#include "base/test/task_environment.h"
+#include "chromecast/media/base/cast_decoder_buffer_impl.h"
+#include "chromecast/public/media/media_pipeline_backend.h"
+#include "chromecast/public/task_runner.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace chromecast {
+namespace media {
+
+namespace {
+
+enum class DecryptorMode {
+  kPendingAndAbortOnDestruction,
+  kImmediateSuccess,
+};
+
+DecryptorMode g_decryptor_mode = DecryptorMode::kPendingAndAbortOnDestruction;
+
+class TestAudioDecryptor;
+TestAudioDecryptor* g_last_decryptor = nullptr;
+
+// AudioDecryptor test implementation whose behavior is configurable for tests.
+class TestAudioDecryptor : public MediaPipelineBackend::AudioDecryptor {
+ public:
+  TestAudioDecryptor() { g_last_decryptor = this; }
+
+  ~TestAudioDecryptor() override {
+    if (g_last_decryptor == this) {
+      g_last_decryptor = nullptr;
+    }
+    if (g_decryptor_mode == DecryptorMode::kPendingAndAbortOnDestruction &&
+        has_pending_ && delegate_) {
+      delegate_->OnPushBufferForDecryptComplete(
+          MediaPipelineBackend::kBufferFailed);
+    }
+  }
+
+  void SetDelegate(Delegate* delegate) override { delegate_ = delegate; }
+
+  BufferStatus PushBufferForDecrypt(CastDecoderBuffer* buffer,
+                                    uint8_t* output) override {
+    if (g_decryptor_mode == DecryptorMode::kPendingAndAbortOnDestruction) {
+      has_pending_ = true;
+      return MediaPipelineBackend::kBufferPending;
+    }
+
+    if (g_decryptor_mode == DecryptorMode::kImmediateSuccess) {
+      if (delegate_) {
+        delegate_->OnDecryptComplete(true);
+      }
+      return MediaPipelineBackend::kBufferSuccess;
+    }
+
+    return MediaPipelineBackend::kBufferSuccess;
+  }
+
+  Delegate* delegate() const { return delegate_; }
+
+ private:
+  Delegate* delegate_ = nullptr;
+  bool has_pending_ = false;
+};
+
+void OnDecrypted(bool* called,
+                 bool* out_success,
+                 size_t* out_buffer_count,
+                 bool success,
+                 StreamDecryptor::BufferQueue buffers) {
+  if (called) {
+    *called = true;
+  }
+  if (out_success) {
+    *out_success = success;
+  }
+  if (out_buffer_count) {
+    *out_buffer_count = buffers.size();
+  }
+}
+
+}  // namespace
+
+// Provide the factory function so BackendDecryptor creates TestAudioDecryptor.
+MediaPipelineBackend::AudioDecryptor*
+MediaPipelineBackend::CreateAudioDecryptor(EncryptionScheme scheme,
+                                           TaskRunner* task_runner) {
+  return new TestAudioDecryptor();
+}
+
+class BackendDecryptorTest : public testing::Test {
+ protected:
+  void SetUp() override {
+    g_decryptor_mode = DecryptorMode::kPendingAndAbortOnDestruction;
+    g_last_decryptor = nullptr;
+  }
+
+  void TearDown() override { g_last_decryptor = nullptr; }
+
+  base::test::TaskEnvironment task_environment_;
+};
+
+TEST_F(BackendDecryptorTest, DelegateNotificationOnDestruction) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = true;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, nullptr));
+
+  // Push a buffer; decryptor returns kBufferPending so completion is deferred.
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  // Reset decryptor. During destruction, AudioDecryptor notifies delegate.
+  decryptor.reset();
+
+  EXPECT_TRUE(called);
+  EXPECT_FALSE(success);
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithoutInit) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  // Push buffer without calling Init().
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  // Reset decryptor; should complete cleanly without initialized callback.
+  decryptor.reset();
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithoutPendingOperation) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Reset decryptor without pushing any buffers.
+  decryptor.reset();
+
+  EXPECT_FALSE(called);
+}
+
+TEST_F(BackendDecryptorTest, ImmediateSuccessfulDecryption) {
+  g_decryptor_mode = DecryptorMode::kImmediateSuccess;
+
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = false;
+  size_t buffer_count = 0;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, &buffer_count));
+
+  // Push a buffer; decryptor completes immediately.
+  decryptor->Decrypt(base::MakeRefCounted<CastDecoderBufferImpl>(16));
+
+  EXPECT_TRUE(called);
+  EXPECT_TRUE(success);
+  EXPECT_EQ(buffer_count, 1u);
+}
+
+TEST_F(BackendDecryptorTest, EmptyPendingBuffersOnDecryptComplete) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Invoke OnDecryptComplete directly when no buffer is pending.
+  EXPECT_TRUE(g_last_decryptor);
+  if (g_last_decryptor && g_last_decryptor->delegate()) {
+    g_last_decryptor->delegate()->OnDecryptComplete(false);
+  }
+
+  EXPECT_FALSE(called);
+}
+
+TEST_F(BackendDecryptorTest, EndOfStreamDecryption) {
+  g_decryptor_mode = DecryptorMode::kImmediateSuccess;
+
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  bool success = false;
+  size_t buffer_count = 0;
+  decryptor->Init(
+      base::BindRepeating(&OnDecrypted, &called, &success, &buffer_count));
+
+  // Push an EOS buffer; decryptor completes and flushes all ready buffers.
+  decryptor->Decrypt(CastDecoderBufferImpl::CreateEOSBuffer());
+
+  EXPECT_TRUE(called);
+  EXPECT_TRUE(success);
+  EXPECT_EQ(buffer_count, 1u);
+}
+
+TEST_F(BackendDecryptorTest, DestructionWithEosPending) {
+  auto decryptor =
+      std::make_unique<BackendDecryptor>(EncryptionScheme::kAesCbc);
+
+  bool called = false;
+  decryptor->Init(base::BindRepeating(&OnDecrypted, &called, nullptr, nullptr));
+
+  // Push an EOS buffer with pending completion.
+  decryptor->Decrypt(CastDecoderBufferImpl::CreateEOSBuffer());
+
+  // Reset decryptor while waiting for EOS.
+  decryptor.reset();
+
+  EXPECT_FALSE(called);
+}
+
+}  // namespace media
+}  // namespace chromecast
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.