Skip to content

Commit 9dad660

Browse files
icbakercopybara-github
authored andcommitted
Fix a ParsableByteArray limit bug in FlacFrameReader
Also clean-up the implementation a bit to avoid allocating two arrays and copying between them. Instead we just allocate a single (large enough) array, which also makes the code more concise. Writing a test for this required `ParsableByteArray` to enforce its limit, so I added that logic too behind a test-only (for now) gate. We can use this gate in other tests as part of flushing out more limit bugs as we work towards b/147657250. PiperOrigin-RevId: 785501853
1 parent 82fbbf2 commit 9dad660

File tree

4 files changed

+131
-11
lines changed

4 files changed

+131
-11
lines changed

libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static java.nio.ByteOrder.LITTLE_ENDIAN;
2020

2121
import androidx.annotation.Nullable;
22+
import androidx.annotation.VisibleForTesting;
2223
import com.google.common.collect.ImmutableSet;
2324
import com.google.common.primitives.Chars;
2425
import com.google.common.primitives.Ints;
@@ -30,6 +31,7 @@
3031
import java.nio.charset.Charset;
3132
import java.nio.charset.StandardCharsets;
3233
import java.util.Arrays;
34+
import java.util.concurrent.atomic.AtomicBoolean;
3335

3436
/**
3537
* Wraps a byte array, providing a set of methods for parsing data from it. Numerical values are
@@ -52,6 +54,9 @@ public final class ParsableByteArray {
5254
StandardCharsets.UTF_16BE,
5355
StandardCharsets.UTF_16LE);
5456

57+
// TODO: b/147657250 - Flip this to true
58+
private static final AtomicBoolean shouldEnforceLimitOnLegacyMethods = new AtomicBoolean();
59+
5560
private byte[] data;
5661
private int position;
5762
// TODO(internal b/147657250): Enforce this limit on all read methods.
@@ -227,6 +232,7 @@ public void readBytes(ParsableBitArray bitArray, int length) {
227232
* @param length The number of bytes to read.
228233
*/
229234
public void readBytes(byte[] buffer, int offset, int length) {
235+
maybeAssertAtLeastBytesLeftForLegacyMethod(length);
230236
System.arraycopy(data, position, buffer, offset, length);
231237
position += length;
232238
}
@@ -239,12 +245,14 @@ public void readBytes(byte[] buffer, int offset, int length) {
239245
* @param length The number of bytes to read.
240246
*/
241247
public void readBytes(ByteBuffer buffer, int length) {
248+
maybeAssertAtLeastBytesLeftForLegacyMethod(length);
242249
buffer.put(data, position, length);
243250
position += length;
244251
}
245252

246253
/** Peeks at the next byte as an unsigned value. */
247254
public int peekUnsignedByte() {
255+
maybeAssertAtLeastBytesLeftForLegacyMethod(1);
248256
return (data[position] & 0xFF);
249257
}
250258

@@ -280,6 +288,7 @@ public char peekChar(Charset charset) {
280288

281289
/** Peek the UTF-16 char at {@link #position}{@code + offset}. */
282290
private char peekChar(ByteOrder byteOrder, int offset) {
291+
maybeAssertAtLeastBytesLeftForLegacyMethod(2);
283292
return byteOrder == BIG_ENDIAN
284293
? Chars.fromBytes(data[position + offset], data[position + offset + 1])
285294
: Chars.fromBytes(data[position + offset + 1], data[position + offset]);
@@ -320,59 +329,69 @@ public int peekCodePoint(Charset charset) {
320329

321330
/** Reads the next byte as an unsigned value. */
322331
public int readUnsignedByte() {
332+
maybeAssertAtLeastBytesLeftForLegacyMethod(1);
323333
return (data[position++] & 0xFF);
324334
}
325335

326336
/** Reads the next two bytes as an unsigned value. */
327337
public int readUnsignedShort() {
338+
maybeAssertAtLeastBytesLeftForLegacyMethod(2);
328339
return (data[position++] & 0xFF) << 8 | (data[position++] & 0xFF);
329340
}
330341

331342
/** Reads the next two bytes as an unsigned value. */
332343
public int readLittleEndianUnsignedShort() {
344+
maybeAssertAtLeastBytesLeftForLegacyMethod(2);
333345
return (data[position++] & 0xFF) | (data[position++] & 0xFF) << 8;
334346
}
335347

336348
/** Reads the next two bytes as a signed value. */
337349
public short readShort() {
350+
maybeAssertAtLeastBytesLeftForLegacyMethod(2);
338351
return (short) ((data[position++] & 0xFF) << 8 | (data[position++] & 0xFF));
339352
}
340353

341354
/** Reads the next two bytes as a signed value. */
342355
public short readLittleEndianShort() {
356+
maybeAssertAtLeastBytesLeftForLegacyMethod(2);
343357
return (short) ((data[position++] & 0xFF) | (data[position++] & 0xFF) << 8);
344358
}
345359

346360
/** Reads the next three bytes as an unsigned value. */
347361
public int readUnsignedInt24() {
362+
maybeAssertAtLeastBytesLeftForLegacyMethod(3);
348363
return (data[position++] & 0xFF) << 16
349364
| (data[position++] & 0xFF) << 8
350365
| (data[position++] & 0xFF);
351366
}
352367

353368
/** Reads the next three bytes as a signed value. */
354369
public int readInt24() {
370+
maybeAssertAtLeastBytesLeftForLegacyMethod(3);
355371
return ((data[position++] & 0xFF) << 24) >> 8
356372
| (data[position++] & 0xFF) << 8
357373
| (data[position++] & 0xFF);
358374
}
359375

360376
/** Reads the next three bytes as a signed value in little endian order. */
361377
public int readLittleEndianInt24() {
378+
maybeAssertAtLeastBytesLeftForLegacyMethod(3);
362379
return (data[position++] & 0xFF)
363380
| (data[position++] & 0xFF) << 8
364381
| (data[position++] & 0xFF) << 16;
365382
}
366383

367384
/** Reads the next three bytes as an unsigned value in little endian order. */
368385
public int readLittleEndianUnsignedInt24() {
386+
maybeAssertAtLeastBytesLeftForLegacyMethod(3);
369387
return (data[position++] & 0xFF)
370388
| (data[position++] & 0xFF) << 8
371389
| (data[position++] & 0xFF) << 16;
372390
}
373391

374392
/** Reads the next four bytes as an unsigned value. */
375393
public long readUnsignedInt() {
394+
maybeAssertAtLeastBytesLeftForLegacyMethod(4);
376395
return (data[position++] & 0xFFL) << 24
377396
| (data[position++] & 0xFFL) << 16
378397
| (data[position++] & 0xFFL) << 8
@@ -381,6 +400,7 @@ public long readUnsignedInt() {
381400

382401
/** Reads the next four bytes as an unsigned value in little endian order. */
383402
public long readLittleEndianUnsignedInt() {
403+
maybeAssertAtLeastBytesLeftForLegacyMethod(4);
384404
return (data[position++] & 0xFFL)
385405
| (data[position++] & 0xFFL) << 8
386406
| (data[position++] & 0xFFL) << 16
@@ -389,6 +409,7 @@ public long readLittleEndianUnsignedInt() {
389409

390410
/** Reads the next four bytes as a signed value */
391411
public int readInt() {
412+
maybeAssertAtLeastBytesLeftForLegacyMethod(4);
392413
return (data[position++] & 0xFF) << 24
393414
| (data[position++] & 0xFF) << 16
394415
| (data[position++] & 0xFF) << 8
@@ -397,6 +418,7 @@ public int readInt() {
397418

398419
/** Reads the next four bytes as a signed value in little endian order. */
399420
public int readLittleEndianInt() {
421+
maybeAssertAtLeastBytesLeftForLegacyMethod(4);
400422
return (data[position++] & 0xFF)
401423
| (data[position++] & 0xFF) << 8
402424
| (data[position++] & 0xFF) << 16
@@ -405,6 +427,7 @@ public int readLittleEndianInt() {
405427

406428
/** Reads the next eight bytes as a signed value. */
407429
public long readLong() {
430+
maybeAssertAtLeastBytesLeftForLegacyMethod(8);
408431
return (data[position++] & 0xFFL) << 56
409432
| (data[position++] & 0xFFL) << 48
410433
| (data[position++] & 0xFFL) << 40
@@ -417,6 +440,7 @@ public long readLong() {
417440

418441
/** Reads the next eight bytes as a signed value in little endian order. */
419442
public long readLittleEndianLong() {
443+
maybeAssertAtLeastBytesLeftForLegacyMethod(8);
420444
return (data[position++] & 0xFFL)
421445
| (data[position++] & 0xFFL) << 8
422446
| (data[position++] & 0xFFL) << 16
@@ -429,6 +453,7 @@ public long readLittleEndianLong() {
429453

430454
/** Reads the next four bytes, returning the integer portion of the fixed point 16.16 integer. */
431455
public int readUnsignedFixedPoint1616() {
456+
maybeAssertAtLeastBytesLeftForLegacyMethod(4);
432457
int result = (data[position++] & 0xFF) << 8 | (data[position++] & 0xFF);
433458
position += 2; // Skip the non-integer portion.
434459
return result;
@@ -518,6 +543,7 @@ public String readString(int length) {
518543
* @return The string encoded by the bytes in the specified character set.
519544
*/
520545
public String readString(int length, Charset charset) {
546+
maybeAssertAtLeastBytesLeftForLegacyMethod(length);
521547
String result = new String(data, position, length, charset);
522548
position += length;
523549
return result;
@@ -531,6 +557,7 @@ public String readString(int length, Charset charset) {
531557
* @return The string, not including any terminating NUL byte.
532558
*/
533559
public String readNullTerminatedString(int length) {
560+
maybeAssertAtLeastBytesLeftForLegacyMethod(length);
534561
if (length == 0) {
535562
return "";
536563
}
@@ -630,6 +657,7 @@ public String readLine(Charset charset) {
630657
* @return Decoded long value
631658
*/
632659
public long readUtf8EncodedLong() {
660+
maybeAssertAtLeastBytesLeftForLegacyMethod(1);
633661
int length = 0;
634662
long value = data[position];
635663
// find the high most 0 bit
@@ -647,6 +675,7 @@ public long readUtf8EncodedLong() {
647675
if (length == 0) {
648676
throw new NumberFormatException("Invalid UTF-8 sequence first byte: " + value);
649677
}
678+
maybeAssertAtLeastBytesLeftForLegacyMethod(length);
650679
for (int i = 1; i < length; i++) {
651680
int x = data[position + i];
652681
if ((x & 0xC0) != 0x80) { // if the high most 0 bit not 7th
@@ -724,6 +753,23 @@ public Charset readUtfCharsetFromBom() {
724753
return null;
725754
}
726755

756+
/**
757+
* Sets whether all read/peek methods should enforce that {@link #getPosition()} never exceeds
758+
* {@link #limit()}.
759+
*
760+
* <p>Setting this to {@code true} in tests can help catch cases of accidentally reading beyond
761+
* {@link #limit()} but still within the bounds of the underlying {@link #getData()}.
762+
*
763+
* <p>Some (newer) methods will always enforce the invariant, even when this is set to {@code
764+
* false}.
765+
*
766+
* <p>Defaults to false (this may change in a later release).
767+
*/
768+
@VisibleForTesting
769+
public static void setShouldEnforceLimitOnLegacyMethods(boolean enforceLimit) {
770+
ParsableByteArray.shouldEnforceLimitOnLegacyMethods.set(enforceLimit);
771+
}
772+
727773
/**
728774
* Returns the index of the next occurrence of '\n' or '\r', or {@link #limit} if none is found.
729775
*/
@@ -898,6 +944,22 @@ && isUtf8ContinuationByte(data[position + 3])) {
898944
}
899945
}
900946

947+
/**
948+
* Enforces that {@link #bytesLeft()} is at least {@code bytesNeeded} if {@link
949+
* #shouldEnforceLimitOnLegacyMethods} is set to {@code true}.
950+
*
951+
* <p>This should only be called from methods that previously didn't enforce the limit. All new
952+
* methods added to this class should unconditionally enforce the limit.
953+
*/
954+
private void maybeAssertAtLeastBytesLeftForLegacyMethod(int bytesNeeded) {
955+
if (shouldEnforceLimitOnLegacyMethods.get()) {
956+
if (bytesLeft() < bytesNeeded) {
957+
throw new IndexOutOfBoundsException(
958+
"bytesNeeded= " + bytesNeeded + ", bytesLeft=" + bytesLeft());
959+
}
960+
}
961+
}
962+
901963
private static boolean isUtf8ContinuationByte(byte b) {
902964
return (b & 0xC0) == 0x80;
903965
}

libraries/extractor/src/main/java/androidx/media3/extractor/FlacFrameReader.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -106,22 +106,21 @@ public static boolean checkFrameHeaderFromPeek(
106106
throws IOException {
107107
long originalPeekPosition = input.getPeekPosition();
108108

109-
byte[] frameStartBytes = new byte[2];
110-
input.peekFully(frameStartBytes, 0, 2);
111-
int frameStart = (frameStartBytes[0] & 0xFF) << 8 | (frameStartBytes[1] & 0xFF);
109+
// We will try and read the first subframe header following the frame header as well.
110+
int dataToCheck = FlacConstants.MAX_FRAME_HEADER_SIZE + 1;
111+
ParsableByteArray scratch = new ParsableByteArray(dataToCheck);
112+
113+
// Check the start marker first, before peeking the rest of the frame header.
114+
input.peekFully(scratch.getData(), 0, 2);
115+
int frameStart = scratch.peekChar();
112116
if (frameStart != frameStartMarker) {
113117
input.resetPeekPosition();
114118
input.advancePeekPosition((int) (originalPeekPosition - input.getPosition()));
115119
return false;
116120
}
117121

118-
// Try and read the first subframe header following the frame header as well.
119-
int dataToCheck = FlacConstants.MAX_FRAME_HEADER_SIZE + 1;
120-
ParsableByteArray scratch = new ParsableByteArray(dataToCheck);
121-
System.arraycopy(
122-
frameStartBytes, /* srcPos= */ 0, scratch.getData(), /* destPos= */ 0, /* length= */ 2);
123-
124-
int totalBytesPeeked = ExtractorUtil.peekToLength(input, scratch.getData(), 2, dataToCheck - 2);
122+
int totalBytesPeeked = 2;
123+
totalBytesPeeked += ExtractorUtil.peekToLength(input, scratch.getData(), 2, dataToCheck - 2);
125124
scratch.setLimit(totalBytesPeeked);
126125

127126
input.resetPeekPosition();

libraries/extractor/src/main/java/androidx/media3/extractor/FlacStreamMetadata.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import static androidx.media3.extractor.VorbisUtil.parseVorbisComments;
1919

2020
import androidx.annotation.Nullable;
21+
import androidx.annotation.VisibleForTesting;
2122
import androidx.media3.common.C;
2223
import androidx.media3.common.Format;
2324
import androidx.media3.common.Metadata;
@@ -165,7 +166,8 @@ public FlacStreamMetadata(
165166
concatenateVorbisMetadata(vorbisComments, pictureFrames));
166167
}
167168

168-
private FlacStreamMetadata(
169+
@VisibleForTesting
170+
/* package */ FlacStreamMetadata(
169171
int minBlockSizeSamples,
170172
int maxBlockSizeSamples,
171173
int minFrameSize,

libraries/extractor/src/test/java/androidx/media3/extractor/FlacFrameReaderTest.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,25 @@
1515
*/
1616
package androidx.media3.extractor;
1717

18+
import static androidx.media3.test.utils.TestUtil.createByteArray;
1819
import static com.google.common.truth.Truth.assertThat;
1920

21+
import androidx.media3.common.Metadata;
2022
import androidx.media3.common.util.ParsableByteArray;
2123
import androidx.media3.common.util.Util;
2224
import androidx.media3.extractor.FlacFrameReader.SampleNumberHolder;
2325
import androidx.media3.extractor.FlacMetadataReader.FlacStreamMetadataHolder;
26+
import androidx.media3.extractor.FlacStreamMetadata.SeekTable;
2427
import androidx.media3.extractor.flac.FlacConstants;
2528
import androidx.media3.test.utils.FakeExtractorInput;
2629
import androidx.media3.test.utils.TestUtil;
2730
import androidx.test.core.app.ApplicationProvider;
2831
import androidx.test.ext.junit.runners.AndroidJUnit4;
32+
import com.google.common.primitives.Bytes;
2933
import com.google.common.primitives.UnsignedBytes;
3034
import java.io.IOException;
35+
import org.junit.AfterClass;
36+
import org.junit.BeforeClass;
3137
import org.junit.Test;
3238
import org.junit.runner.RunWith;
3339

@@ -40,6 +46,16 @@
4046
@RunWith(AndroidJUnit4.class)
4147
public class FlacFrameReaderTest {
4248

49+
@BeforeClass
50+
public static void enableParsableByteArrayLimitEnforcement() {
51+
ParsableByteArray.setShouldEnforceLimitOnLegacyMethods(true);
52+
}
53+
54+
@AfterClass
55+
public static void disableParsableByteArrayLimitEnforcement() {
56+
ParsableByteArray.setShouldEnforceLimitOnLegacyMethods(false);
57+
}
58+
4359
@Test
4460
public void checkAndReadFrameHeader_validData_updatesPosition() throws Exception {
4561
FlacStreamMetadataHolder streamMetadataHolder =
@@ -294,6 +310,47 @@ public void checkFrameHeaderFromPeek_validData_isTrue() throws Exception {
294310
assertThat(result).isTrue();
295311
}
296312

313+
@Test
314+
public void checkFrameHeaderFromPeek_validData_maxHeaderSize() throws Exception {
315+
byte[] frameHeader =
316+
Bytes.concat(
317+
// The sync code and blocking strategy bit (variable block size)
318+
createByteArray(0xFF, 0xF9),
319+
// Indicates 16-bit uncommon block size and sample rate, and mono channel count.
320+
createByteArray(0b0111_1101, 0x00),
321+
// The smallest 7 byte coded number: 0x8000_0000
322+
createByteArray(0xFE, 0x82, 0x80, 0x80, 0x80, 0x80, 0x80),
323+
// The 16-bit uncommon block size (257 - 1) & sample rate (256kHz), as indicated above.
324+
createByteArray(0x01, 0x00, 0x01, 0x00),
325+
// The CRC placeholder (updated below).
326+
createByteArray(0x00));
327+
frameHeader[15] =
328+
UnsignedBytes.checkedCast(
329+
Util.crc8(frameHeader, /* start= */ 0, /* end= */ 15, /* initialValue= */ 0));
330+
331+
ExtractorInput input = new FakeExtractorInput.Builder().setData(frameHeader).build();
332+
FlacStreamMetadata flacStreamMetadata =
333+
new FlacStreamMetadata(
334+
/* minBlockSizeSamples= */ 16,
335+
/* maxBlockSizeSamples= */ 257,
336+
/* minFrameSize= */ 0, /* unknown */
337+
/* maxFrameSize= */ 0 /* unknown */,
338+
/* sampleRate= */ 256,
339+
/* channels= */ 1,
340+
/* bitsPerSample= */ 16,
341+
/* totalSamples= */ 0 /* unknown */,
342+
(SeekTable) null,
343+
(Metadata) null);
344+
345+
int frameStartMarker = FlacMetadataReader.getFrameStartMarker(input);
346+
347+
boolean result =
348+
FlacFrameReader.checkFrameHeaderFromPeek(
349+
input, flacStreamMetadata, frameStartMarker, new SampleNumberHolder());
350+
351+
assertThat(result).isTrue();
352+
}
353+
297354
@Test
298355
public void checkFrameHeaderFromPeek_validData_writesSampleNumber() throws Exception {
299356
FlacStreamMetadataHolder streamMetadataHolder =

0 commit comments

Comments
 (0)