CVE-2026-113318
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmojo/public/cpp/system/data_pipe_drainer.cc |
modified | |
Clientmojo/public/cpp/system/data_pipe_drainer.h |
modified | |
DataPipeDrainerTestmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
TestClientmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
DataPipeDrainerTestmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
whilemojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
ifmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
TEST_Fmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified | |
BindLambdaForTestingmojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc |
modified |
Files Changed
mojo/public/cpp/system/tests/data_pipe_drainer_unittest.ccmojo/public/cpp/system/data_pipe_drainer.ccmojo/public/cpp/system/data_pipe_drainer.h
Patch
From 781e3c8ee33e2566fe561948c4128b2105ad690d Mon Sep 17 00:00:00 2001 From: Daniel Cheng <[email protected]> Date: Fri, 30 Oct 2026 08:34:11 -0700 Subject: [PATCH] Handle reentrant destruction from clients in `mojo::DataPipeDrainer` `DataPipeDrainer::ReadData()` called `source_->EndReadData()` unconditionally after `client_->OnDataAvailable()`; however, a client that reentrantly destroyed the `mojo::DataPipeDrainer` would trigger a use-after-free. Avoiding this use-after-free requires additional handling (https://crrev.com/c/2672379, https://crrev.com/c/7781209); forgetting to handle this edge case results in a security bug instead (crbug.com/550181232, crbug.com/566136673). Since `mojo::DataPipeDrainer` is intended for general consumption, it should handle this more gracefully instead of requiring all of its callers to be careful. Add a WeakPtr guard and document the lifetime and validity expectations. Bug: 566347710 Change-Id: I7f43003e044757c91c1fee967eae4e465eee1892 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8683777 Reviewed-by: Tom Sepez <[email protected]> Commit-Queue: Daniel Cheng <[email protected]> Cr-Commit-Position: refs/heads/main@{#1731506} --- diff --git a/mojo/public/cpp/system/data_pipe_drainer.cc b/mojo/public/cpp/system/data_pipe_drainer.cc index 0d2262b..60d53ee 100644 --- a/mojo/public/cpp/system/data_pipe_drainer.cc +++ b/mojo/public/cpp/system/data_pipe_drainer.cc @@ -32,7 +32,11 @@ base::span<const uint8_t> buffer; MojoResult rv = source_->BeginReadData(MOJO_READ_DATA_FLAG_NONE, buffer); if (rv == MOJO_RESULT_OK) { + base::WeakPtr<DataPipeDrainer> weak_self = weak_factory_.GetWeakPtr(); client_->OnDataAvailable(buffer); + if (!weak_self) { + return; + } source_->EndReadData(buffer.size()); } else if (rv == MOJO_RESULT_FAILED_PRECONDITION) { client_->OnDataComplete(); diff --git a/mojo/public/cpp/system/data_pipe_drainer.h b/mojo/public/cpp/system/data_pipe_drainer.h index 741d786..4f1730ee 100644 --- a/mojo/public/cpp/system/data_pipe_drainer.h +++ b/mojo/public/cpp/system/data_pipe_drainer.h @@ -5,7 +5,7 @@ #ifndef MOJO_PUBLIC_CPP_SYSTEM_DATA_PIPE_DRAINER_H_ #define MOJO_PUBLIC_CPP_SYSTEM_DATA_PIPE_DRAINER_H_ -#include <stddef.h> +#include <stdint.h> #include "base/containers/span.h" #include "base/memory/raw_ptr.h" @@ -20,6 +20,9 @@ public: class Client { public: + // The memory backing `data` is transitively owned by the `DataPipeDrainer` + // and is guaranteed to be valid until `OnDataAvailable()` returns, or until + // the `DataPipeDrainer` is destroyed. virtual void OnDataAvailable(base::span<const uint8_t> data) = 0; virtual void OnDataComplete() = 0; @@ -27,7 +30,8 @@ virtual ~Client() {} }; - DataPipeDrainer(Client*, mojo::ScopedDataPipeConsumerHandle source); + // `client` must outlive this `DataPipeDrainer`. + DataPipeDrainer(Client* client, mojo::ScopedDataPipeConsumerHandle source); DataPipeDrainer(const DataPipeDrainer&) = delete; DataPipeDrainer& operator=(const DataPipeDrainer&) = delete; diff --git a/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc b/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc index 8683385..71ebadad 100644 --- a/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc +++ b/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc @@ -4,72 +4,191 @@ #include "mojo/public/cpp/system/data_pipe_drainer.h" +#include <stdint.h> + +#include <memory> +#include <optional> +#include <string> #include <string_view> +#include <utility> #include "base/containers/span.h" -#include "base/functional/bind.h" #include "base/functional/callback.h" #include "base/run_loop.h" #include "base/strings/string_view_util.h" +#include "base/test/bind.h" #include "base/test/task_environment.h" -#include "base/values.h" +#include "mojo/public/c/system/types.h" +#include "mojo/public/cpp/system/data_pipe.h" #include "testing/gtest/include/gtest/gtest.h" namespace mojo { -namespace test { +namespace { -template <typename Functor> -base::RepeatingClosure BindLambda(Functor callable) { - return base::BindRepeating([](Functor callable) { callable(); }, callable); -} - -class DataPipeDrainerTest : public testing::Test, - public DataPipeDrainer::Client { +class TestClient : public DataPipeDrainer::Client { public: - DataPipeDrainerTest(const DataPipeDrainerTest&) = delete; - DataPipeDrainerTest& operator=(const DataPipeDrainerTest&) = delete; - - protected: - DataPipeDrainerTest() { - ScopedDataPipeProducerHandle producer_handle; - ScopedDataPipeConsumerHandle consumer_handle; - EXPECT_EQ(CreateDataPipe(nullptr, producer_handle, consumer_handle), - MOJO_RESULT_OK); - drainer_ = - std::make_unique<DataPipeDrainer>(this, std::move(consumer_handle)); - producer_handle_ = std::move(producer_handle); + explicit TestClient(ScopedDataPipeConsumerHandle consumer) { + drainer_.emplace(this, std::move(consumer)); } - ScopedDataPipeProducerHandle producer_handle_; - base::RepeatingClosure completion_callback_; + TestClient(const TestClient&) = delete; + TestClient& operator=(const TestClient&) = delete; + + bool WaitForData(std::string_view expected_data) { + while (data_ != expected_data && !is_complete_) { + data_run_loop_.emplace(); + data_run_loop_->Run(); + } + return data_ == expected_data; + } + + void WaitForComplete() { completion_run_loop_.Run(); } void OnDataAvailable(base::span<const uint8_t> data) override { data_.append(base::as_string_view(data)); + if (data_run_loop_) { + data_run_loop_->Quit(); + } + if (data_available_callback_) { + std::move(data_available_callback_).Run(); + } } - void OnDataComplete() override { completion_callback_.Run(); } + void OnDataComplete() override { + EXPECT_FALSE(is_complete_); + is_complete_ = true; + if (data_run_loop_) { + data_run_loop_->Quit(); + } + completion_run_loop_.Quit(); + if (completion_callback_) { + std::move(completion_callback_).Run(); + } + } - base::test::SingleThreadTaskEnvironment task_environment_; + void ResetDrainer() { drainer_.reset(); } + bool has_drainer() const { return drainer_.has_value(); } + const std::string& data() const { return data_; } + + void set_data_available_callback(base::RepeatingClosure callback) { + data_available_callback_ = std::move(callback); + } + void set_completion_callback(base::RepeatingClosure callback) { + completion_callback_ = std::move(callback); + } + + private: std::string data_; - std::unique_ptr<DataPipeDrainer> drainer_; + bool is_complete_ = false; + std::optional<base::RunLoop> data_run_loop_; + base::RunLoop completion_run_loop_; + base::RepeatingClosure data_available_callback_; + base::RepeatingClosure completion_callback_; + std::optional<DataPipeDrainer> drainer_; +}; + +class DataPipeDrainerTest : public testing::Test { + public: + DataPipeDrainerTest() { + ScopedDataPipeConsumerHandle consumer_handle; + EXPECT_EQ(CreateDataPipe(nullptr, producer_handle_, consumer_handle), + MOJO_RESULT_OK); + client_ = std::make_unique<TestClient>(std::move(consumer_handle)); + } + + protected: + base::test::SingleThreadTaskEnvironment task_environment_; + ScopedDataPipeProducerHandle producer_handle_; + std::unique_ptr<TestClient> client_; }; TEST_F(DataPipeDrainerTest, TestCompleteIsCalledOnce) { - bool had_data_complete = false; + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + EXPECT_TRUE(client_->WaitForData("hello")); - completion_callback_ = BindLambda([&had_data_complete]() { - EXPECT_FALSE(had_data_complete); - had_data_complete = true; - }); - size_t bytes_written = 0; - EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteData( - base::byte_span_from_cstring("hello"), - MOJO_WRITE_DATA_FLAG_NONE, bytes_written)); - EXPECT_EQ(bytes_written, 5u); - base::RunLoop().RunUntilIdle(); producer_handle_.reset(); - base::RunLoop().RunUntilIdle(); + client_->WaitForComplete(); } -} // namespace test +TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataAvailable) { + client_->set_data_available_callback( + base::BindLambdaForTesting([&]() { client_->ResetDrainer(); })); + client_->set_completion_callback(base::BindLambdaForTesting( + []() { ADD_FAILURE() << "OnDataComplete should not be called."; })); + + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + + EXPECT_TRUE(client_->WaitForData("hello")); + EXPECT_FALSE(client_->has_drainer()); + EXPECT_EQ( + MOJO_RESULT_FAILED_PRECONDITION, + producer_handle_->WriteAllData(base::byte_span_from_cstring("world"))); +} + +TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataAvailableWithQueuedEOF) { + client_->set_data_available_callback( + base::BindLambdaForTesting([&]() { client_->ResetDrainer(); })); + client_->set_completion_callback(base::BindLambdaForTesting( + []() { ADD_FAILURE() << "OnDataComplete should not be called."; })); + + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + producer_handle_.reset(); + + EXPECT_TRUE(client_->WaitForData("hello")); + EXPECT_FALSE(client_->has_drainer()); +} + +TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataComplete) { + client_->set_completion_callback( + base::BindLambdaForTesting([&]() { client_->ResetDrainer(); })); + + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + producer_handle_.reset(); + client_->WaitForComplete(); + + EXPECT_EQ(client_->data(), "hello"); + EXPECT_FALSE(client_->has_drainer()); +} + +TEST_F(DataPipeDrainerTest, DestroyClientOnDataAvailable) { + base::RunLoop run_loop; + std::string captured_data; + client_->set_data_available_callback(base::BindLambdaForTesting([&]() { + captured_data = client_->data(); + client_.reset(); + run_loop.Quit(); + })); + client_->set_completion_callback(base::BindLambdaForTesting( + []() { ADD_FAILURE() << "OnDataComplete should not be called."; })); + + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + producer_handle_.reset(); + run_loop.Run(); + + EXPECT_EQ(captured_data, "hello"); + EXPECT_EQ(client_, nullptr); +} + +TEST_F(DataPipeDrainerTest, DestroyClientOnDataComplete) { + base::RunLoop run_loop; + client_->set_completion_callback(base::BindLambdaForTesting([&]() { + EXPECT_EQ(client_->data(), "hello"); + client_.reset(); + run_loop.Quit(); + })); + + EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData( + base::byte_span_from_cstring("hello"))); + producer_handle_.reset(); + run_loop.Run(); + + EXPECT_EQ(client_, nullptr); +} + +} // namespace } // namespace mojo
Regression Test / PoC
diff --git a/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc b/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc
index 8683385..71ebadad 100644
--- a/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc
+++ b/mojo/public/cpp/system/tests/data_pipe_drainer_unittest.cc
@@ -4,72 +4,191 @@
#include "mojo/public/cpp/system/data_pipe_drainer.h"
+#include <stdint.h>
+
+#include <memory>
+#include <optional>
+#include <string>
#include <string_view>
+#include <utility>
#include "base/containers/span.h"
-#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/run_loop.h"
#include "base/strings/string_view_util.h"
+#include "base/test/bind.h"
#include "base/test/task_environment.h"
-#include "base/values.h"
+#include "mojo/public/c/system/types.h"
+#include "mojo/public/cpp/system/data_pipe.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace mojo {
-namespace test {
+namespace {
-template <typename Functor>
-base::RepeatingClosure BindLambda(Functor callable) {
- return base::BindRepeating([](Functor callable) { callable(); }, callable);
-}
-
-class DataPipeDrainerTest : public testing::Test,
- public DataPipeDrainer::Client {
+class TestClient : public DataPipeDrainer::Client {
public:
- DataPipeDrainerTest(const DataPipeDrainerTest&) = delete;
- DataPipeDrainerTest& operator=(const DataPipeDrainerTest&) = delete;
-
- protected:
- DataPipeDrainerTest() {
- ScopedDataPipeProducerHandle producer_handle;
- ScopedDataPipeConsumerHandle consumer_handle;
- EXPECT_EQ(CreateDataPipe(nullptr, producer_handle, consumer_handle),
- MOJO_RESULT_OK);
- drainer_ =
- std::make_unique<DataPipeDrainer>(this, std::move(consumer_handle));
- producer_handle_ = std::move(producer_handle);
+ explicit TestClient(ScopedDataPipeConsumerHandle consumer) {
+ drainer_.emplace(this, std::move(consumer));
}
- ScopedDataPipeProducerHandle producer_handle_;
- base::RepeatingClosure completion_callback_;
+ TestClient(const TestClient&) = delete;
+ TestClient& operator=(const TestClient&) = delete;
+
+ bool WaitForData(std::string_view expected_data) {
+ while (data_ != expected_data && !is_complete_) {
+ data_run_loop_.emplace();
+ data_run_loop_->Run();
+ }
+ return data_ == expected_data;
+ }
+
+ void WaitForComplete() { completion_run_loop_.Run(); }
void OnDataAvailable(base::span<const uint8_t> data) override {
data_.append(base::as_string_view(data));
+ if (data_run_loop_) {
+ data_run_loop_->Quit();
+ }
+ if (data_available_callback_) {
+ std::move(data_available_callback_).Run();
+ }
}
- void OnDataComplete() override { completion_callback_.Run(); }
+ void OnDataComplete() override {
+ EXPECT_FALSE(is_complete_);
+ is_complete_ = true;
+ if (data_run_loop_) {
+ data_run_loop_->Quit();
+ }
+ completion_run_loop_.Quit();
+ if (completion_callback_) {
+ std::move(completion_callback_).Run();
+ }
+ }
- base::test::SingleThreadTaskEnvironment task_environment_;
+ void ResetDrainer() { drainer_.reset(); }
+ bool has_drainer() const { return drainer_.has_value(); }
+ const std::string& data() const { return data_; }
+
+ void set_data_available_callback(base::RepeatingClosure callback) {
+ data_available_callback_ = std::move(callback);
+ }
+ void set_completion_callback(base::RepeatingClosure callback) {
+ completion_callback_ = std::move(callback);
+ }
+
+ private:
std::string data_;
- std::unique_ptr<DataPipeDrainer> drainer_;
+ bool is_complete_ = false;
+ std::optional<base::RunLoop> data_run_loop_;
+ base::RunLoop completion_run_loop_;
+ base::RepeatingClosure data_available_callback_;
+ base::RepeatingClosure completion_callback_;
+ std::optional<DataPipeDrainer> drainer_;
+};
+
+class DataPipeDrainerTest : public testing::Test {
+ public:
+ DataPipeDrainerTest() {
+ ScopedDataPipeConsumerHandle consumer_handle;
+ EXPECT_EQ(CreateDataPipe(nullptr, producer_handle_, consumer_handle),
+ MOJO_RESULT_OK);
+ client_ = std::make_unique<TestClient>(std::move(consumer_handle));
+ }
+
+ protected:
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ ScopedDataPipeProducerHandle producer_handle_;
+ std::unique_ptr<TestClient> client_;
};
TEST_F(DataPipeDrainerTest, TestCompleteIsCalledOnce) {
- bool had_data_complete = false;
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+ EXPECT_TRUE(client_->WaitForData("hello"));
- completion_callback_ = BindLambda([&had_data_complete]() {
- EXPECT_FALSE(had_data_complete);
- had_data_complete = true;
- });
- size_t bytes_written = 0;
- EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteData(
- base::byte_span_from_cstring("hello"),
- MOJO_WRITE_DATA_FLAG_NONE, bytes_written));
- EXPECT_EQ(bytes_written, 5u);
- base::RunLoop().RunUntilIdle();
producer_handle_.reset();
- base::RunLoop().RunUntilIdle();
+ client_->WaitForComplete();
}
-} // namespace test
+TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataAvailable) {
+ client_->set_data_available_callback(
+ base::BindLambdaForTesting([&]() { client_->ResetDrainer(); }));
+ client_->set_completion_callback(base::BindLambdaForTesting(
+ []() { ADD_FAILURE() << "OnDataComplete should not be called."; }));
+
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+
+ EXPECT_TRUE(client_->WaitForData("hello"));
+ EXPECT_FALSE(client_->has_drainer());
+ EXPECT_EQ(
+ MOJO_RESULT_FAILED_PRECONDITION,
+ producer_handle_->WriteAllData(base::byte_span_from_cstring("world")));
+}
+
+TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataAvailableWithQueuedEOF) {
+ client_->set_data_available_callback(
+ base::BindLambdaForTesting([&]() { client_->ResetDrainer(); }));
+ client_->set_completion_callback(base::BindLambdaForTesting(
+ []() { ADD_FAILURE() << "OnDataComplete should not be called."; }));
+
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+ producer_handle_.reset();
+
+ EXPECT_TRUE(client_->WaitForData("hello"));
+ EXPECT_FALSE(client_->has_drainer());
+}
+
+TEST_F(DataPipeDrainerTest, DestroyDrainerOnDataComplete) {
+ client_->set_completion_callback(
+ base::BindLambdaForTesting([&]() { client_->ResetDrainer(); }));
+
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+ producer_handle_.reset();
+ client_->WaitForComplete();
+
+ EXPECT_EQ(client_->data(), "hello");
+ EXPECT_FALSE(client_->has_drainer());
+}
+
+TEST_F(DataPipeDrainerTest, DestroyClientOnDataAvailable) {
+ base::RunLoop run_loop;
+ std::string captured_data;
+ client_->set_data_available_callback(base::BindLambdaForTesting([&]() {
+ captured_data = client_->data();
+ client_.reset();
+ run_loop.Quit();
+ }));
+ client_->set_completion_callback(base::BindLambdaForTesting(
+ []() { ADD_FAILURE() << "OnDataComplete should not be called."; }));
+
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+ producer_handle_.reset();
+ run_loop.Run();
+
+ EXPECT_EQ(captured_data, "hello");
+ EXPECT_EQ(client_, nullptr);
+}
+
+TEST_F(DataPipeDrainerTest, DestroyClientOnDataComplete) {
+ base::RunLoop run_loop;
+ client_->set_completion_callback(base::BindLambdaForTesting([&]() {
+ EXPECT_EQ(client_->data(), "hello");
+ client_.reset();
+ run_loop.Quit();
+ }));
+
+ EXPECT_EQ(MOJO_RESULT_OK, producer_handle_->WriteAllData(
+ base::byte_span_from_cstring("hello")));
+ producer_handle_.reset();
+ run_loop.Run();
+
+ EXPECT_EQ(client_, nullptr);
+}
+
+} // namespace
} // namespace mojo
Original Bug Report
Heap-use-after-free in mojo::DataPipeDrainer::ReadData
Summary
A page whose service worker uses a race-network-and-fetch-handler static-router rule loads an iframe and detaches it while the raced network response is still streaming. RaceNetworkRequestSimpleBufferManager::MaybeWriteData() hits a write error and, since bcea409251239 (2026-09-14), calls Finish(); that runs the clone-completed callback, and ServiceWorkerMainResourceLoader::CheckLifecycle() deletes the loader, destroying the mojo::DataPipeDrainer whose ReadData() is still on the stack. ReadData() then uses the freed drainer. Browser process, zero interaction, default flags.
Environment
- Chrome for Testing 155.0.8059.12 (Beta), 156.0.8072.0 (Canary); code present at main b7c07f35773a. Stable 154 is not affected (predates the change).
- Linux x86-64
- Evidence build: ASan, Chromium main 156.0.8074.0
Reproduction
python3 poc.py 8000
chrome --user-data-dir=/tmp/p --no-first-run http://127.0.0.1:8000/poc.html
- Start
poc.py(a service worker needs an http page). - Launch Chrome with a fresh profile. Zero interaction; the browser crashes within about 5 s.
Rate: Beta 5/5, Canary 9/9, ASan 8/8.
Evidence
Browser process, UI thread. Full report: asan.txt.
heap-use-after-free READ of size 8, 8 bytes inside 120-byte region
#0 mojo::DataPipeDrainer::ReadData (data_pipe_drainer.cc:36)
freed: ~ServiceWorkerRaceNetworkRequestURLLoaderClient
<- ServiceWorkerMainResourceLoader::CheckLifecycle :1614
<- OnCloneCompletedForFetchHandler <- RaceNetworkRequestSimpleBufferManager::Finish
<- MaybeWriteData :160 <- OnDataAvailable <- DataPipeDrainer::ReadData :35
MiraclePtr Status: NOT PROTECTED
Stock desktop builds read PartitionAlloc’s freed-memory fill (0xCDCD…) and crash on it; free and use are on one stack, and re-occupation is not demonstrated.
Vulnerable code
mojo/public/cpp/system/data_pipe_drainer.cc
35 client_->OnDataAvailable(buffer); [1]
36 source_->EndReadData(buffer.size()); [2]
[1] Can reach Finish() and delete this drainer. [2] Reads source_ from the freed object.
Impact
Any web page, no interaction. Browser-process use-after-free. Reporter’s estimate: High (S1); Critical by class, if re-occupation is not required.
Credit
Xinyang Ge (Anthropic), assisted by Claude
Attachments: poc.py, asan.txt, report.md