Skip to content

fix: reject 5-byte LEB128 encodings that overflow uint32 in decodeUInt32 - #1690

Open
eeshsaxena wants to merge 1 commit into
workos:mainfrom
eeshsaxena:fix/leb128-decode-uint32-range
Open

fix: reject 5-byte LEB128 encodings that overflow uint32 in decodeUInt32#1690
eeshsaxena wants to merge 1 commit into
workos:mainfrom
eeshsaxena:fix/leb128-decode-uint32-range

Conversation

@eeshsaxena

Copy link
Copy Markdown

Problem

decodeUInt32 in src/common/utils/leb128.ts validates the byte count of a LEB128 sequence but not the range of its last byte.

A uint32 needs at most 5 bytes. The 5th byte is read at shift = 28, and only 32 - 28 = 4 of its 7 data bits fit in a uint32. When the 5th byte carries data above 0x0F, the value is greater than MAX_UINT32, but the JavaScript 32-bit << silently drops the overflowing bits instead of erroring:

result |= (byte & DATA_BITS_MASK) << shift; // (0x10 << 28) truncates to 0

So over-range 5-byte encodings decode to a wrong value rather than being rejected:

input decoded value correct behavior
[0x80,0x80,0x80,0x80,0x10] (2³²) 0 reject
[0xFF,0xFF,0xFF,0xFF,0x1F] 4294967295 reject
[0xFF,0xFF,0xFF,0xFF,0x7F] 4294967295 reject

encodeUInt32 already validates the uint32 range, and the suite's existing throws for encoding that exceeds uint32 range test only covers the too-many-bytes case ([0x80,0x80,0x80,0x80,0x80,0x01]), so this class of over-range input slipped through — a decoder that's meant to fail on out-of-range input silently returns garbage instead.

Fix

Reject an over-range final byte before the shift, so the decoder fails loudly to match the encoder:

if (shift === FINAL_BYTE_SHIFT && (byte & DATA_BITS_MASK) > FINAL_BYTE_MAX_DATA) {
  throw new Error('LEB128 sequence exceeds uint32 range');
}

FINAL_BYTE_SHIFT is (MAX_BYTES_FOR_UINT32 - 1) * DATA_BITS_PER_BYTE = 28 and FINAL_BYTE_MAX_DATA = 0x0F. Every valid encoding (final byte ≤ 0x0F, including MAX_UINT32[0xFF,0xFF,0xFF,0xFF,0x0F]) is unaffected; only genuinely out-of-range input now throws.

Tests

Added three cases to the invalid inputs block: two over-range 5-byte sequences that must throw LEB128 sequence exceeds uint32 range, and one asserting MAX_UINT32 (final byte exactly 0x0F) still decodes to 4294967295. Full leb128.spec.ts passes (42/42); reverting only the source change fails the two new "throws" cases, confirming they catch the bug.

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.
@eeshsaxena
eeshsaxena requested review from a team as code owners September 1, 2026 17:50
@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR hardens unsigned LEB128 decoding by rejecting fifth-byte payloads that exceed the uint32 range before JavaScript’s bitwise shift can truncate them.

  • Adds explicit bounds for the four usable data bits in the fifth byte.
  • Adds regression coverage for overflowing encodings and the valid MAX_UINT32 boundary.

Confidence Score: 5/5

The PR appears safe to merge.

The new check rejects only fifth-byte payloads above the four bits available in a uint32, while preserving the valid maximum boundary and existing successful decoding behavior.

Important Files Changed

Filename Overview
src/common/utils/leb128.ts Adds a mathematically correct fifth-byte range check without rejecting valid uint32 encodings.
src/common/utils/leb128.spec.ts Covers two overflowing five-byte encodings and confirms that the maximum valid uint32 encoding remains accepted.

Reviews (1): Last reviewed commit: "Reject 5-byte LEB128 encodings that over..." | Re-trigger Greptile

@eeshsaxena eeshsaxena changed the title Reject 5-byte LEB128 encodings that overflow uint32 in decodeUInt32 fix: reject 5-byte LEB128 encodings that overflow uint32 in decodeUInt32 Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant