noun: fixes u3r_bytes read past a direct atom - #1091
Merged
Merged
Conversation
In the direct-atom branch, the shift count [c3_min(a_w, 4) << 3] is exactly 32 whenever [a_w >= 4] -- the width of c3_w, so the shift is undefined behaviour. x86-64 and arm64 both mask the shift count, so it evaluates to [d >> 0] and a caller asking for bytes past the end of the atom gets the atom's own bytes instead of the zeros it should read. Rewrites the function so there is no shift at all. A direct atom is its own byte buffer, so both cases share a single copy-and-zero-fill path, which makes the bug structurally impossible rather than patching the shift. Adds _test_bytes, _test_words and _test_chubs, covering, for a direct and an indirect atom: an exact-length read, an overlong read, a read straddling the end, a read entirely past the end, and a zero-length read. Past-the-end reads return zeros only if the memory beyond the atom happens to be zero, so the tests dirty the relevant allocator size classes with freed 0xff-filled atoms first.
Now that u3r_bytes handles a direct atom as a plain byte buffer, both word- and chub-granular reads are just u3r_bytes with the offset and length scaled, leaving one copy-and-zero-fill implementation instead of three. Note that this narrows the accepted range: [b_w << 3] overflows c3_w at [b_w >= 2^29], where u3r_words previously accepted a word count up to 2^32. That is a 4GB destination buffer and no caller comes close.
The allocation is [len_w] bytes but the read asked for [a_w + len_w], overrunning it by [a_w] whenever the offset is nonzero. Every caller in the tree passes a zero offset, so this is latent rather than live.
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.
Closes #1090.
Found while reviewing #970, but this is not vere64-specific — it reproduces on
developas-is.The bug
pkg/noun/retrieve.c,u3r_bytes(), direct-atom branch:When
a_w >= 4the clamp makes the shift count exactly 32, the width ofc3_w— undefined behaviour. x86-64 and arm64 both mask the shift count, so it evaluates tod >> 0and the caller gets the atom's own bytes where it asked for bytes past the end, which should read as zero.Confirmed with a throwaway probe before fixing:
want
0000000000000000, observed4847464500000000._leer_cut()inpkg/noun/jets/e/leer.cis a live caller of the affected shape (u3r_bytes(pos_w, len_w, ...)with a nonzero offset), so this is reachable, not just theoretical.The fix
Rewritten so there is no shift at all. A direct atom is its own byte buffer, so both cases share one copy-and-zero-fill path — the bug becomes structurally impossible rather than patched. The
hav_wternary is load-bearing:len_w - a_wunderflows whena_w >= len_w.The collapse
u3r_wordsandu3r_chubsthen become casts, leaving one implementation instead of three.This narrows the accepted range, and I'd rather flag it than have a reviewer find it:
b_w << 3overflowsc3_watb_w >= 2^29, whereu3r_wordspreviously accepted a word count up to2^32. That is a 4GB destination buffer and no caller comes anywhere near it. If that is objectionable, the principled fix is makingu3r_bytes's length parametersc3_z, which is a signature change with a long call-site tail and belongs in its own PR.u3r_bytes_allocThird commit, separable if a reviewer disagrees. It allocated
len_wbytes but asked fora_w + len_w, overrunning the destination bya_w. Every caller in the tree passesa_w == 0(verified by grep), so it is latent rather than live.Tests
_test_bytes,_test_wordsand_test_chubsinpkg/noun/retrieve_tests.c, following the existing_test_mugidiom. For both a direct and an indirect atom: offset 0 at exactly the available length; offset 0 past the available length (tail must zero-fill); a nonzero offset straddling the end; an offset entirely past the end (the regression case above); and zero length (must not write anything). Theu3r_chubscases use an atom of three words, so the final chub is half-populated and must zero-extend.Reads past the end return zeros only if the memory beyond the atom happens to be zero, which it is on a freshly mapped page — so a broken implementation can pass by luck. The tests dirty the relevant allocator size classes first (64 atoms per class filled with
0xff, then freed).Verified the tests have teeth by mutating the fix:
hav_wternary →fail (i) dog past (2)fail (d) cat past (1), showinghave: 48 47 46 45 ...againstwant: 00 00 00 00 ...Verification
u3r_bytesis widely used —u3r_bytes_all, the hashing jets (shax,blake,keccak,sha1,ripe,chacha,argon2,ed_*),aes_sivandurwasmall route through it — so I ran the whole C suite, not justretrieve-test. All pass:Also grepped for callers that might have depended on the old past-the-end behaviour. The nonzero-offset callers are
leer.c,urwasm.candames.c; all three want the documented zero-fill, which is what the fix restores.Notes
u3a_word_bytes/u3a_word_bytes_shift— those are vere64 additions and do not exist ondevelop. Uses4and<< 2, matching the surrounding code.XX: assumes little-endiancomments are kept where the casts happen.