From 6a7fd488e2754132e4950a1bc6e0ddc458e117f4 Mon Sep 17 00:00:00 2001 From: eeshsaxena <139802361+eeshsaxena@users.noreply.github.com> Date: Tue, 1 Sep 2026 23:19:45 +0530 Subject: [PATCH] Reject 5-byte LEB128 encodings that overflow uint32 in decodeUInt32 decodeUInt32 guards the maximum byte count, but not the range of the final byte. On the 5th byte only 4 of its 7 data bits fit in a uint32, so a byte above 0x0F encodes a value greater than MAX_UINT32. The 32-bit `<<` then silently drops the overflowing bits, so e.g. [0x80,0x80,0x80,0x80,0x10] (2^32) decodes to 0 and [0xFF,0xFF,0xFF,0xFF,0x7F] decodes to 4294967295 instead of being rejected. encodeUInt32 already validates the uint32 range, and the suite's existing 'exceeds uint32 range' test only covered the too-many-bytes case. Reject an over-range final byte before the shift so decode fails loudly on such input, matching the encoder. Valid encodings (final byte <= 0x0F, including MAX_UINT32) are unaffected. --- src/common/utils/leb128.spec.ts | 22 ++++++++++++++++++++++ src/common/utils/leb128.ts | 15 +++++++++++++++ 2 files changed, 37 insertions(+) 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)) {