fix(base64): keep the vector fast paths free of wrapping bounds - #4123
Conversation
The block loops tested `index + 16 <= end`, which wraps to a true comparison for a view sitting at the very end of a large backing buffer, and `encode_v128` sized its UTF-16 buffer as `char_count * 2` without checking that the product fits in an `Int`. Neither is reachable through any realistic caller -- both need an allocation close to two gigabytes -- but the vector loads and stores they guard are not bounds checked, so a wrapped index reads and writes outside the buffer rather than failing. Both loops now bound themselves by subtraction, and the encoder hands an input whose buffer size would wrap to the scalar encoder, which builds the string without ever materializing that buffer. Found by Codex CLI while reviewing the equivalent code in #4122, which fixes the same two defects in `encoding/hex`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex CLI review (
|
There was a problem hiding this comment.
Pull request overview
Hardens the encoding/base64 SIMD (v128) fast paths against integer-wrapping loop bounds and potential output-buffer size wrapping, preserving existing behavior while preventing memory-unsafe out-of-bounds reads/writes on extreme inputs.
Changes:
- Adds an input-length guard in
encode_v128to avoid wrapped output-buffer sizing before unchecked vector stores. - Rewrites SIMD loop bounds in
encode_v128to use subtraction (end - index >= ...) instead of potentially-wrapping addition (index + ... <= end). - Rewrites the SIMD loop bound in
decode_v128to use wrap-free subtraction for both input and output bounds.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| encoding/base64/encode_v128.mbt | Adds a size guard and rewrites vector-loop bounds to prevent wrapping-based OOB in the encoder fast path. |
| encoding/base64/decode_v128.mbt | Rewrites vector-loop bounds to avoid wrapping checks and keep unchecked stores within the output buffer. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Coverage Report for CI Build 6263Coverage decreased (-0.004%) to 90.791%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Hardening only — no behaviour change for any input a real caller can construct, and no API change.
While reviewing the equivalent code in #4122, Codex CLI flagged two defects in
encoding/hex's new vector paths and noted thatencoding/base64shares one of them: "the base64 precedent shares this defect; that does not make it safe." It shares the other as well. This fixes both there.The two defects
Wrapping block bounds. The loops tested
index + 16 <= end. For aBytesViewsitting at the very end of a large backing buffer,index + 16wraps negative and the comparison becomes true, so a short view enters the vector loop and reads past its end. All three sites now bound by subtraction:Each subtraction is wrap-free from the view invariants:
end = start_offset + length <= src.length() <= Int::MAXwithindexin[start_offset, end], and in the decoderbody/indexare view-relative withwritten <= out.length().Unchecked output size.
encode_v128sized its UTF-16 buffer aschar_count * 2, wherechar_countis itselffull_groups * 4— two wrapping multiplies feeding an allocation whose vector stores are not bounds checked. A wrapped size means writing outside the buffer rather than failing. The encoder now declines such an input:The scalar encoder builds the string through
StringBuilderwithout ever materializing that buffer, and its growth is bounds checked, so it fails safely at sizes the fast path refuses.Neither defect is reachable through a realistic caller — both need an allocation approaching two gigabytes — but the code they guard is unchecked, so the failure mode is memory corruption rather than a panic. The fix is three bounds and one guard.
Cost
None. Native, this branch vs
origin/main, interleaved on the same machine:Every figure is within run-to-run noise; the loop-bound rewrite is free.
moon test encoding/base64passes 21/21 on native and wasm, fullmoon test7512/7512, andpkg.generated.mbtiis unchanged.Review
Reviewed by Codex CLI at
ultrareasoning effort — approved on the first pass, verdict posted below.One deliberate gap: the size guard needs a 512 MiB input to reach, so no test exercises it. I asked whether it should be restructured to be testable and the reviewer's answer was no — "exercising it requires impractical CI memory, while existing differential tests cover ordinary block/tail behavior and the boundary itself has a straightforward arithmetic proof."