diff --git a/p2p/BUILD.gn b/p2p/BUILD.gn index 8ff23691b9..ed80ee0f53 100644 --- a/p2p/BUILD.gn +++ b/p2p/BUILD.gn @@ -916,14 +916,80 @@ rtc_library("wrapping_active_ice_controller") { ] } +rtc_library("fake_ice_transport") { + testonly = true + visibility = [ "*" ] + sources = [ + "base/fake_ice_transport.h", + "test/fake_ice_transport.h", + ] + deps = [ + ":candidate_pair_interface", + ":connection_info", + ":dtls_stun_piggyback_controller", + ":ice_transport_internal", + ":port", + ":transport_description", + "../api:candidate", + "../api:field_trials", + "../api:ice_transport_interface", + "../api:sequence_checker", + "../api/task_queue", + "../api/task_queue:pending_task_safety_flag", + "../api/transport:enums", + "../api/transport:stun_types", + "../api/units:time_delta", + "../rtc_base:async_packet_socket", + "../rtc_base:byte_buffer", + "../rtc_base:checks", + "../rtc_base:copy_on_write_buffer", + "../rtc_base:logging", + "../rtc_base:macromagic", + "../rtc_base:network_route", + "../rtc_base:socket", + "../rtc_base:task_queue_for_test", + "../rtc_base:timeutils", + "../rtc_base/network:received_packet", + "../rtc_base/network:sent_packet", + "../test:create_test_field_trials", + "//third_party/abseil-cpp/absl/algorithm:container", + "//third_party/abseil-cpp/absl/functional:any_invocable", + "//third_party/abseil-cpp/absl/strings:string_view", + ] +} + +rtc_library("fake_dtls_transport") { + testonly = true + visibility = [ "*" ] + sources = [ "dtls/fake_dtls_transport.h" ] + deps = [ + ":dtls_transport_internal", + ":fake_ice_transport", + ":ice_transport_internal", + ":packet_transport_internal", + "../api:dtls_transport_interface", + "../api:ice_transport_interface", + "../api:rtc_error", + "../api:scoped_refptr", + "../rtc_base:async_packet_socket", + "../rtc_base:buffer", + "../rtc_base:checks", + "../rtc_base:logging", + "../rtc_base:network_route", + "../rtc_base:rtc_base_tests_utils", + "../rtc_base:socket", + "../rtc_base:ssl", + "../rtc_base:threading", + "../rtc_base/network:received_packet", + "//third_party/abseil-cpp/absl/strings:string_view", + ] +} + if (rtc_include_tests) { - rtc_library("fake_ice_transport") { + rtc_library("fake_ice_transport_gated_unused") { testonly = true visibility = [ "*" ] - sources = [ - "base/fake_ice_transport.h", - "test/fake_ice_transport.h", - ] + sources = [] deps = [ ":candidate_pair_interface", ":connection_info", diff --git a/pc/BUILD.gn b/pc/BUILD.gn index 5db70e761f..5578a62020 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -2192,6 +2192,42 @@ rtc_library("datagram_connection_internal") { ] } +# Minimal standalone test target to demonstrate the latent UAF in +# JsepTransportCollection::RollbackTransports() under ASAN. Kept separate +# from rtc_pc_unittests so it can be built inside a Chromium checkout +# (rtc_pc_unittests is gated on !build_with_chromium). +rtc_test("jsep_transport_collection_rollback_uaf_poc") { + testonly = true + is_xctest = true # suppress auto-dep on //test:test_main (we ship main()). + sources = [ "jsep_transport_collection_rollback_uaf_test.cc" ] + deps = [ + ":dtls_srtp_transport", + ":dtls_transport", + ":jsep_transport", + ":jsep_transport_collection", + ":rtp_transport", + ":rtp_transport_internal", + "../api:field_trials", + "../api:ice_transport_interface", + "../api:make_ref_counted", + "../api:scoped_refptr", + "../p2p:dtls_transport_internal", + "../p2p:fake_dtls_transport", + "../p2p:fake_ice_transport", + "../p2p:ice_transport_internal", + "../p2p:p2p_constants", + "../rtc_base:checks", + "../rtc_base:logging", + "../rtc_base:ssl", + "../rtc_base:threading", + "../test:create_test_field_trials", + "../test:test_support", + "//testing/gtest", + "//third_party/webrtc_overrides:metrics", + "//third_party/webrtc_overrides:system_time", + ] +} + if (rtc_include_tests && !build_with_chromium) { rtc_source_set("fake_codec_lookup_helper") { testonly = true diff --git a/pc/jsep_transport_collection_rollback_uaf_test.cc b/pc/jsep_transport_collection_rollback_uaf_test.cc new file mode 100644 index 0000000000..f2fe4bcca9 --- /dev/null +++ b/pc/jsep_transport_collection_rollback_uaf_test.cc @@ -0,0 +1,173 @@ +/* + * POC: Latent heap-use-after-free in + * webrtc::JsepTransportCollection::RollbackTransports(). + * + * RollbackTransports() chains map_change_callback_ invocations with + * short-circuiting `&&`, but UNCONDITIONALLY rewrites mid_to_transport_ + * and calls DestroyUnusedTransports() afterwards. If any callback in the + * "restore old mappings" loop returns false, every later mid is silently + * skipped — yet the offer-created JsepTransport (and its owned + * RtpTransportInternal) those mids' channels still point at is freed. + * + * In production the callback eventually reaches + * BaseChannel::SetRtpTransport(), which stores a *raw* + * RtpTransportInternal* (pc/channel.h:326, no MiraclePtr in + * third_party/webrtc). A skipped channel keeps that raw pointer aimed at + * freed memory; the next SendPacket / DisconnectFromRtpTransport_n / + * transport_name() dereferences it. + * + * This test simulates BaseChannel's pointer storage and demonstrates the + * resulting heap-use-after-free under ASAN. It exercises the public + * JsepTransportCollection contract directly because, under current code, + * the only production source of a `false` return + * (RtpDemuxer::CriteriaWouldConflict) cannot be induced on the stable + * transport during rollback without first putting the PeerConnection into + * a session-error state that blocks rollback. + */ + +#include +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "api/field_trials.h" +#include "api/ice_transport_interface.h" +#include "api/make_ref_counted.h" +#include "api/scoped_refptr.h" +#include "p2p/base/p2p_constants.h" +#include "p2p/dtls/dtls_transport_internal.h" +#include "p2p/dtls/fake_dtls_transport.h" +#include "p2p/test/fake_ice_transport.h" +#include "pc/dtls_srtp_transport.h" +#include "pc/dtls_transport.h" +#include "pc/jsep_transport.h" +#include "pc/jsep_transport_collection.h" +#include "pc/rtp_transport.h" +#include "pc/rtp_transport_internal.h" +#include "rtc_base/logging.h" +#include "rtc_base/thread.h" +#include "test/create_test_field_trials.h" +#include "test/gtest.h" + +namespace webrtc { +namespace { + +FieldTrials g_field_trials = CreateTestFieldTrials(); + +std::unique_ptr MakeJsepTransport(const std::string& name) { + auto ice = make_ref_counted( + std::make_unique(name, + ICE_CANDIDATE_COMPONENT_RTP)); + auto dtls = std::make_unique(ice); + DtlsTransportInternal* dtls_ptr = dtls.get(); + + auto rtp = std::make_unique(/*rtcp_mux_enabled=*/true, + g_field_trials); + rtp->SetDtlsTransportsOwned(std::move(dtls), nullptr); + + auto dtls_wrapper = make_ref_counted(dtls_ptr); + + return std::make_unique( + /*local_certificate=*/nullptr, std::move(rtp), std::move(dtls_wrapper), + /*sctp_transport=*/nullptr, + /*rtcp_mux_active_callback=*/[]() {}); +} + +TEST(JsepTransportCollectionRollbackUaf, + ShortCircuitLeavesDanglingRtpTransport) { + AutoThread main_thread; + + // Simulated per-mid BaseChannel::rtp_transport_ raw pointers + // (pc/channel.h:326 — plain `RtpTransportInternal*`). + std::map channel_rtp_transport; + std::set notified_during_rollback; + + bool rollback_phase = false; + std::string fail_on_mid; + + auto map_change_callback = [&](absl::string_view mid, + JsepTransport* t) -> bool { + std::string m(mid); + if (rollback_phase) { + notified_during_rollback.insert(m); + if (!fail_on_mid.empty() && m == fail_on_mid && t != nullptr) { + // Simulate BaseChannel::SetRtpTransport when ConnectToRtpTransport_n + // (RegisterRtpDemuxerSink) fails: rtp_transport_ ends up nullptr, + // and `false` is propagated up the OnTransportChanged chain. + channel_rtp_transport[m] = nullptr; + return false; + } + } + // Simulate BaseChannel storing the RtpTransportInternal* it was handed. + channel_rtp_transport[m] = t ? t->rtp_transport() : nullptr; + return true; + }; + auto state_change_callback = []() {}; + + JsepTransportCollection collection(map_change_callback, + state_change_callback); + + // --- Reach stable state: bundle {A, B} on transport TA (named "A"). --- + auto ta_owned = MakeJsepTransport("A"); + JsepTransport* ta = ta_owned.get(); + collection.RegisterTransport(std::move(ta_owned)); // A -> TA + EXPECT_TRUE(collection.SetTransportForMid("B", ta)); // B -> TA + collection.CommitTransports(); // stable_mid_to_transport_ = {A->TA, B->TA} + + // --- Apply remote offer: bundle group reordered to {C, A, B}. --- + // A new mid "C" is BUNDLE-tagged; ApplyDescription_n would create a fresh + // JsepTransport TC and move A/B onto it via SetTransportForMid. + auto tc_owned = MakeJsepTransport("C"); + JsepTransport* tc = tc_owned.get(); + RtpTransportInternal* tc_rtp = tc->rtp_transport(); + collection.RegisterTransport(std::move(tc_owned)); // C -> TC + EXPECT_TRUE(collection.SetTransportForMid("A", tc)); // A -> TC + EXPECT_TRUE(collection.SetTransportForMid("B", tc)); // B -> TC + + ASSERT_EQ(channel_rtp_transport["A"], tc_rtp); + ASSERT_EQ(channel_rtp_transport["B"], tc_rtp); + + // --- Rollback. --- + // Arm the callback so that restoring mid "A" to its stable transport fails + // (simulating RegisterRtpDemuxerSink -> CriteriaWouldConflict -> false). + // Because of `ret = ret && callback(...)`, mid "B" is NEVER notified — yet + // TC is destroyed unconditionally by DestroyUnusedTransports(). + rollback_phase = true; + fail_on_mid = "A"; + + bool ok = collection.RollbackTransports(); + EXPECT_FALSE(ok); + + // Prove the short-circuit happened. + EXPECT_TRUE(notified_during_rollback.count("A")); + EXPECT_TRUE(notified_during_rollback.count("C")); // loop 1 (nullptr) + EXPECT_FALSE(notified_during_rollback.count("B")) // SKIPPED! + << "mid B was notified — short-circuit did not occur"; + + // The collection no longer references TC. + EXPECT_EQ(collection.GetTransportForMid("C"), nullptr); + EXPECT_EQ(collection.GetTransportByName("C"), nullptr); + + // Channel B's raw pointer was never updated; it still points at TC's + // RtpTransportInternal, which was just freed. + ASSERT_EQ(channel_rtp_transport["B"], tc_rtp); + + // *** Heap-use-after-free *** — this is exactly what + // BaseChannel::DisconnectFromRtpTransport_n / SendPacket / transport_name() + // would do on the next access. ASAN reports heap-use-after-free here. + RTC_LOG(LS_ERROR) << "Dereferencing dangling rtp_transport_ for mid B: " + << channel_rtp_transport["B"]->transport_name(); +} + +} // namespace +} // namespace webrtc + +// Minimal main() so this target builds inside a Chromium checkout without +// depending on //third_party/webrtc/test:test_main (which is gated on +// rtc_include_tests && !build_with_chromium). +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}