diff --git a/src/common/utils/leb128.spec.ts b/src/common/utils/leb128.spec.ts index 869e59e9c..ed936f917 100644 --- a/src/common/utils/leb128.spec.ts +++ b/src/common/utils/leb128.spec.ts @@ -242,6 +242,28 @@ describe('leb128', () => { 'LEB128 sequence exceeds maximum length for uint32', ); }); + + test('throws for a 5-byte encoding whose final byte overflows uint32', () => { + // 5 bytes, but the final byte carries data bits above bit 31 + // (0x10 -> 2^32). Without a range check the 32-bit shift silently drops + // the overflow and returns 0 instead of rejecting the value. + const data = new Uint8Array([0x80, 0x80, 0x80, 0x80, 0x10]); + expect(() => decodeUInt32(data)).toThrow( + 'LEB128 sequence exceeds uint32 range', + ); + }); + + test('throws when the final byte uses all 7 data bits', () => { + const data = new Uint8Array([0xff, 0xff, 0xff, 0xff, 0x7f]); + expect(() => decodeUInt32(data)).toThrow( + 'LEB128 sequence exceeds uint32 range', + ); + }); + + test('accepts MAX_UINT32 whose final byte is exactly 0x0f', () => { + const data = new Uint8Array([0xff, 0xff, 0xff, 0xff, 0x0f]); + expect(decodeUInt32(data).value).toBe(4294967295); + }); }); }); diff --git a/src/common/utils/leb128.ts b/src/common/utils/leb128.ts index 8f6978fa8..463556844 100644 --- a/src/common/utils/leb128.ts +++ b/src/common/utils/leb128.ts @@ -9,6 +9,11 @@ const CONTINUATION_BIT = 0x80; const DATA_BITS_MASK = 0x7f; const DATA_BITS_PER_BYTE = 7; const MAX_BYTES_FOR_UINT32 = 5; +// The 5th byte is read at this shift; a uint32 only has 32 - 28 = 4 data bits +// left for it, so its 7-bit payload must not exceed 0x0F. A larger payload +// encodes a value above MAX_UINT32. +const FINAL_BYTE_SHIFT = (MAX_BYTES_FOR_UINT32 - 1) * DATA_BITS_PER_BYTE; +const FINAL_BYTE_MAX_DATA = 0x0f; /** * Encodes an unsigned 32-bit integer into LEB128 format. @@ -65,6 +70,16 @@ export function decodeUInt32( throw new Error('LEB128 sequence exceeds maximum length for uint32'); } + // On the final (5th) byte only 4 of its 7 data bits fit in a uint32. Reject a + // larger payload instead of letting the 32-bit `<<` below silently drop the + // overflowing bits and return a wrong value for an out-of-range encoding. + if ( + shift === FINAL_BYTE_SHIFT && + (byte & DATA_BITS_MASK) > FINAL_BYTE_MAX_DATA + ) { + throw new Error('LEB128 sequence exceeds uint32 range'); + } + result |= (byte & DATA_BITS_MASK) << shift; if (!hasContinuationBit(byte)) {