fix: prevent integer overflow and OOB reads in WASM safety checks - #8
Merged
Conversation
Owner
|
Thanks for catching these, the four fixes are all real and still applicable on current main. Three small asks:
If it'd be easier, toggle "Allow edits from maintainers" on the PR and I'll push the rebase for you. Otherwise happy to re-review as soon as CI flips green. |
Four bugs fixed: 1. Integer overflow in tq_dot_batch: num_vectors * bytes_per_vector could wrap u32, creating an undersized buffer and causing out-of-bounds reads in the dotBatch loop. Use std.math.mul to detect overflow and return early. 2. Integer overflow in format.slicePayload: polar_bytes + qjl_bytes could wrap u32, making payload_end smaller than the actual payload and bypassing the bounds check. Use std.math.add to detect overflow. 3. Payload size validation in decode/dot: the engine now validates that polar_bytes and qjl_bytes in the header are consistent with the declared dimension before passing them to polar/qjl functions. Prevents out-of-bounds reads from crafted compressed data with correct dim but undersized payload fields. 4. Dead code removal in dotBatch: the comptime conditional `if (is_aarch64) 4 else 4` always evaluated to 4. Replaced with std.simd.suggestVectorLength for consistency with other modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Contributor
Author
|
Hi @teamchong, thanks for the quick review! I've rebased on main (after #7), resolved the one-line conflict in
Ready for re-review whenever you have a moment. |
teamchong
added a commit
that referenced
this pull request
Apr 19, 2026
…Needed Follow-up to #6756491 (PR #8). The original validatePayloadSizes duplicated the polar/qjl encoded-size formulas inline: const polar_bits = num_pairs * 7; // BITS_PER_PAIR const expected_polar = (polar_bits + 7) / 8 + 1; const expected_qjl = (dim + 7) / 8; Both modules already export the same math as pub helpers (polar.polarBytesNeeded, qjl.qjlBytesNeeded). Calling them directly keeps the validator in sync automatically if either format ever changes its internal widths (e.g. R_BITS/THETA_BITS retuning, QJL widening from 1-bit). Removes the silent drift risk. Also adds symmetric test coverage for the qjl_bytes validation path — the original tests only corrupted polar_bytes (offset 6..10), leaving the qjl_bytes check at offset 10..14 untested. Two new tests (decode + dot) corrupt qjl_bytes and assert the same InvalidPayload / 0.0 behaviour. 63 tests total, all pass.
teamchong
added a commit
that referenced
this pull request
Apr 19, 2026
- Rewrite design doc: removes 'not implemented' status, fixes 2-mode claim (now 9 branches), corrects u64→u32 offsets in the format spec, updates sizes to match the actual 88.3 MB cache, moves done items into the shipped checklist, removes the 'Names' padding section. - Bump to 0.4.1 for PR #8 (security fixes: u32 overflow guards in tq_dot_batch + format.slicePayload, validatePayloadSizes for decode/dot) + follow-up refactor that delegates to polarBytesNeeded/qjlBytesNeeded.
6 tasks
teamchong
added a commit
that referenced
this pull request
Apr 19, 2026
Dynamic context window for the Gemma 4 E2B draw demo. Engine boots with a small "router" system prompt; when the model emits setType("..."), a side-channel observer detects it and engine.mountKV swaps the active KV to that type's pre-baked branch — no runtime prefill on the swap.
9 per-type branches under demo/src/draw/prompts/ (router + architecture / sequence / flowchart / state / orgchart / er / class / swimlane), packed into a new TQKC multi-branch container format at public/system-cache.bin (88.3 MB). Design: demo/docs/dynamic-context-window.md.
Core pieces:
- ModeTracker in grammar.ts with 9 SDK_MODE constants + onEnter hooks
- engine.mountKV(name) + registerBranch
- worker mountBranchAndPrefill: swap KV + re-prefill user turn atomically
- DiagramType union expanded to all 8 values in types.ts + sdk-types.ts
- Router skips the thinking phase (its only job is to pick setType); branch runs full think+code under specialised KV
- Retry restores to router snapshot (activeBranch bookkeeping included)
Rolls in demo improvements accumulated on this branch: subgroup-butterfly reductions, window-bound softmax, shared-KV skip in batched prefill, uniform-buffer dedup, mobile-aware WebGPU error card, markdown-stripped thinking cloud, cancel-and-restart, singleton lock, orphan auto-fix before retry, retry-LCP rollback.
Follow-up to #8: validatePayloadSizes now delegates to polarBytesNeeded / qjlBytesNeeded instead of re-deriving the formulas, plus symmetric qjl_bytes-corruption tests. Package bumped to 0.4.1.
22 new TS unit tests + 2 new Zig tests (63 total), all pass. TS clean, zig fmt clean, CI green across Linux/macOS/Windows.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four safety bugs found and fixed in the WASM compression pipeline:
Integer overflow in
tq_dot_batch(wasm_exports.zig):num_vectors * bytes_per_vectorcould wrapu32, creating an undersized slice and causing out-of-bounds reads in thedotBatchloop. Fixed withstd.math.muloverflow check.Integer overflow in
format.slicePayload(format.zig):polar_bytes + qjl_bytescould wrapu32, makingpayload_endsmaller than actual payload, bypassing the bounds check. Fixed withstd.math.addoverflow check.Missing payload size validation (
turboquant.zig):decode()anddot()validated header dimension but not thatpolar_bytes/qjl_byteswere consistent with that dimension. A crafted compressed buffer with correctdimbut undersized payload fields would cause out-of-bounds reads in polar/qjl functions. AddedvalidatePayloadSizes()to check expected sizes before use.Dead code in
dotBatch(turboquant.zig):comptime if (is_aarch64) 4 else 4always evaluates to 4 — both branches are identical. Replaced withstd.simd.suggestVectorLengthfor consistency with other modules, and removed the unusedbuiltinimport.Test plan
zig build test)decode rejects payload with inconsistent field sizes— verifies decode rejects corrupted polar_bytesdot returns zero on payload with inconsistent field sizes— verifies dot returns 0 on corrupted payloadtq_dot_batchwith large num_vectors * bytes_per_vector that overflows u32 should return silently🤖 Generated with Claude Code