Throw on truncated VLQs in strict decode mode#9
Conversation
TokenIterator.nextUnsignedVLQ() kept reading digits as long as the continuation bit was set, without checking whether any input was left. If a scopes string was truncated (or otherwise malformed) so that its last VLQ ended with the continuation bit set, the next read ran past the end of the string: charCodeAt returned NaN, the base64 lookup gave undefined, and `undefined & mask` quietly collapsed to 0. Decoding then stopped and returned a corrupt-but-silent result even in strict mode, which is documented to throw on malformed input. Give TokenIterator a strict flag and bail out when a digit is undefined (EOF or a character outside the base64 alphabet) mid-VLQ: throw in strict mode, keep the existing best-effort behaviour in lax mode. The decoder passes the flag through based on its DecodeMode. Adds unit tests for the truncated and out-of-alphabet cases, plus decode-level tests for a scopes string that ends mid-VLQ in both modes.
|
Thanks, I was debating whether we should split On the other hand, this makes calls to As such, I'd be in favor of landing the CL as-is, thanks! (Just documenting that we thought about this :)) |
|
Thanks for merging, and for thinking it through on the hot-loop side. The monomorphism worry is real: splitting into a SafeTokenIterator could easily cost more than the branch it removes, especially since the strict path is the rare one. If it ever shows up in a profile, the branch could always hoist out to the call site instead. Appreciate you documenting the tradeoff either way. |
Hi @szuend — small correctness fix I ran into while reading through the VLQ decoder.
The bug
TokenIterator.nextUnsignedVLQ()loops on the continuation bit with no bounds check:If a
scopesstring is truncated so that its final VLQ has the continuation bit set, the loop reads one character past the end.charCodeAtreturnsNaN,BASE64_CODES[NaN]isundefined, andundefined & VLQ_BASE_MASKcoerces to0— so the continuation check also sees0and decoding just stops. The result is a silently corrupt decode. SinceDecodeMode.STRICTis documented to throw on malformed input, that case slips through the contract. The same thing happens for a continuation that runs into a character outside the base64 alphabet.The fix
TokenIteratortakes astrictflag. When a digit comes backundefinedmid-VLQ (either EOF on a truncated string, or a char outside the alphabet), we now throw in strict mode and keep the existing best-effort behaviour in lax mode — lax stops and returns what it decoded so far, exactly as before. The decoder passes the flag through from itsDecodeMode.This is intentionally narrow: a
,still terminates a VLQ the way it always has (it maps to0, same asA), so the item-separator semantics and the existing lax tests are untouched.Tests
vlq.test.tsfor the truncated and out-of-alphabet cases in strict mode.decode.test.tsfor a scopes string that ends mid-VLQ, asserting strict throws and lax tolerates it.The VLQ code is adapted from Chrome DevTools, so I kept the change minimal and in the same style as the surrounding strict/lax handling.