diff --git a/components/openscreen_platform/BUILD.gn b/components/openscreen_platform/BUILD.gn index 8671a2d34bc9a..a34081409f16e 100644 --- a/components/openscreen_platform/BUILD.gn +++ b/components/openscreen_platform/BUILD.gn @@ -148,17 +148,20 @@ source_set("unittests") { sources = [ "tls_client_connection_unittest.cc", "tls_connection_factory_unittest.cc", + "udp_socket_unittest.cc", ] deps = [ ":openscreen_platform", "//base", "//base/test:test_support", + "//mojo/public/cpp/bindings", "//net", "//services/network:test_support", "//services/network/public/mojom", "//testing/gmock", "//testing/gtest", + "//third_party/openscreen/src/platform:api", ] if (!is_win && !is_ios) { diff --git a/components/openscreen_platform/udp_socket_unittest.cc b/components/openscreen_platform/udp_socket_unittest.cc new file mode 100644 index 0000000000000..96ecfbb2df0c4 --- /dev/null +++ b/components/openscreen_platform/udp_socket_unittest.cc @@ -0,0 +1,105 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "components/openscreen_platform/udp_socket.h" + +#include +#include +#include + +#include "base/test/task_environment.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "net/base/net_errors.h" +#include "services/network/public/mojom/udp_socket.mojom.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/openscreen/src/platform/api/udp_socket.h" +#include "third_party/openscreen/src/platform/base/error.h" +#include "third_party/openscreen/src/platform/base/ip_address.h" +#include "third_party/openscreen/src/platform/base/udp_packet.h" + +namespace openscreen_platform { + +namespace { + +// A UdpSocket::Client that owns the socket and destroys it synchronously +// from within OnRead(). This models the Cast-receiver browser-process +// teardown chain where a recv error propagates through +// Environment::OnRead -> Environment::OnError -> +// ReceiverSession::OnSocketInvalid -> ... -> +// StreamingRuntimeApplication::StopApplication() -> +// receiver_session_client_.reset() +// which synchronously runs ~openscreen_platform::UdpSocket while +// UdpSocket::OnReceived is still on the stack. +class SelfDestructingClient : public openscreen::UdpSocket::Client { + public: + SelfDestructingClient() = default; + ~SelfDestructingClient() override = default; + + void TakeOwnership(std::unique_ptr socket) { + socket_ = std::move(socket); + } + + // openscreen::UdpSocket::Client: + void OnError(openscreen::UdpSocket* socket, + const openscreen::Error& error) override {} + void OnSendError(openscreen::UdpSocket* socket, + const openscreen::Error& error) override {} + void OnRead(openscreen::UdpSocket* socket, + openscreen::ErrorOr packet) override { + // Synchronously free the socket while UdpSocket::OnReceived() is still on + // the stack. After this returns, OnReceived() unconditionally executes + // udp_socket_->ReceiveMore(1); + // which dereferences a destructed mojo::Remote<> stored inside the freed + // |this| allocation. + socket_.reset(); + } + + private: + std::unique_ptr socket_; +}; + +} // namespace + +// POC: heap-use-after-free in openscreen_platform::UdpSocket::OnReceived. +// +// UdpSocket::OnReceived (the network::mojom::UDPSocketListener handler) calls +// client_->OnRead(...) and then unconditionally touches |this->udp_socket_|. +// The Mojo listener dispatch is not weak-guarded (unlike Bind/JoinGroup/Send +// callbacks which use weak_ptr_factory_), so a Client that synchronously +// destroys the UdpSocket from within OnRead() leaves OnReceived() holding a +// dangling |this|. In production (Cast receiver products) the destruction is +// reached via StreamingRuntimeApplication::StopApplication; here we model that +// directly. +TEST(OpenscreenPlatformUdpSocketTest, OnReceivedAfterClientDeletesSocket) { + base::test::TaskEnvironment task_environment; + + // Bound Remote so that, post-free, ReceiveMore(1) attempts a virtual call + // through the destructed proxy (matching the production state). + mojo::Remote udp_remote; + std::ignore = udp_remote.BindNewPipeAndPassReceiver(); + + mojo::PendingRemote listener_remote; + mojo::PendingReceiver pending_listener = + listener_remote.InitWithNewPipeAndPassReceiver(); + + SelfDestructingClient client; + auto socket = std::make_unique( + &client, + openscreen::IPEndpoint{openscreen::IPAddress{127, 0, 0, 1}, 0}, + std::move(udp_remote), std::move(pending_listener)); + UdpSocket* raw_socket = socket.get(); + client.TakeOwnership(std::move(socket)); + + // Network process delivers OnReceived(result < 0, nullopt, nullopt) over the + // UDPSocketListener pipe (services/network/udp_socket.cc forwards recv + // errors unchanged). The listener_ receiver is not weak-guarded, so this is + // a direct call into the implementation method: + raw_socket->OnReceived(net::ERR_FAILED, std::nullopt, std::nullopt); + // ^ heap-use-after-free under ASAN at udp_socket.cc:166: + // udp_socket_->ReceiveMore(1); +} + +} // namespace openscreen_platform