- test/MemoryLayout.t.sol L7 cites README section "Memory layout"; no such heading exists (the prose is under "### The pattern"); every other README citation in the tests names a real heading. LOW.
- README L432 and test/HashPattern.t.sol L206:
// Write C and D to scratch to produce the final hash E annotates keccak256(0, 0x40), which writes nothing; C and D were stored at L425/L430. Use "Hash C and D, already in scratch, to produce the final hash E".
- README L234 "All non-struct types end up in one of 3 buckets" while L259-260 puts the struct's region in bucket one. Use "Every type ends up in one of 3 buckets".
- test/MemoryLayout.t.sol L231-233 "an
Outermost holding an Outer holding a Foo is 2 words at each level" — Foo is 4 words (asserted at L167); only the two outer sizes are asserted. Use "2 words at each of the two outer levels".
- README L498-501 concludes "Equal folds therefore have equal lengths" from the base case alone; the length conclusion needs the peeling step. Replacement bullet (doc-only): "the fold over 0 items is the hash of 0 bytes and the fold over n >= 1 items is
hash(fold(n - 1) + hash(item)), a hash of 64 bytes. If the folds over n and m items are equal, with n <= m, peeling one layer at a time gives fold(n - 1) = fold(m - 1) and equal last-item hashes, down to fold(0) = fold(m - n); the hash of 0 bytes cannot equal a hash of 64 bytes, so n = m, and each peeled pair of equal item hashes is, by induction over the item type, a pair of equal items."
Findings from the audit run
F49 [LOW] dim 3 cat — test/MemoryLayout.t.sol:7-8
Struct doc cites a README section "Memory layout" that does not exist
No README heading is named 'Memory layout'; the layout prose the tests pin lives under '### The pattern'. Every other section reference in the tests quotes a real heading.
EVIDENCE: test/MemoryLayout.t.sol L7-8 /// The struct README.md "Memory layout" describes: a 4-word region, one word ; grep -n '^#' README.md has no such heading; claim at README L258-262 under ### The pattern.
FIX: L7: /// The struct README.md "The pattern" describes: a 4-word region, one word
F46 [INFO] dim 3 cat — README.md:432-433 (test/HashPattern.t.sol:205-206)
Yul comment 'Write C and D to scratch' describes a step the code has already done
C is written to scratch at L425 and D at L430; the final statement only hashes them. The verbatim test copy carries the same comment.
EVIDENCE: README L425 mstore(0, keccak256(0, 0x40)), L430 mstore(0x20, keccak256(add(deref_, 0x20), mload(deref_))), L432-433 // Write C and D to scratch to produce the final hash E / let E := keccak256(0, 0x40); test/HashPattern.t.sol L205-206.
FIX: README L432 and test L205: // Hash C and D, already in scratch, to produce the final hash E.
Fix: comment-only edit on both lines; no bytecode, no assertion, no ruling touched, compiles trivially (no forge run needed for a // line). Proposed wording "Hash C and D, already in scratch, to produce the final hash E" is accurate and complete. No test is proposed and none is appropriate — the existing testHandlingPointers already asserts e == keccak(C||D), so behaviour is pinned; the gap is purely documentation. Fix is complete: both occurrences (README + test) named.
Severity: INFO is right. Skill scheme L118-120: "suggestion for improvement with no direct risk". Dimension 3 (documentation), not Pass-6, so the L373 hazard scale does not apply; even read as a hazard, the comment cannot mislead anyone into a wrong hash because the surrounding comments on L424/L429 already state C and D are stored. Production impact for the internal-function hashing library (sole source importer rainlang.interface signed-context digest): zero — README example code and a test comment, no library source. Keep INFO.
F47 [INFO] dim 3 cat — README.md:234-238, 259-262
'All non-struct types end up in one of 3 buckets' but the next example puts a struct in bucket one
Structs and static arrays are exactly bucket one; the qualifier contradicts the Foo example that follows.
EVIDENCE: README L234 All non-struct types end up in one of 3 buckets:; L259-260 'The thing it points to falls into the first bucket, a 4-word region of memory defined by its type.'
FIX: README L234: Every type ends up in one of 3 buckets:
F51 [INFO] dim 3 cat — test/MemoryLayout.t.sol:231-233
Doc says the nested chain is '2 words at each level' but the innermost Foo is 4 words
Only Outermost and Outer are 2 words and only those two sizes are asserted; Foo is 0x80 (testFooIsFourWords).
EVIDENCE: L231-233 'Outer holding a Foo is 2 words at each level'; body L262, L264 assert the two outer sizes; L167 asserts Foo is 0x80.
FIX: L232: 'Outer holding a Foo is 2 words at each of the two outer levels, and'
F66 [INFO] dim 5 cat — README.md:498-501
Fold induction attributes the equal-length conclusion to the base case alone
The base case separates only length 0 from >= 1; equal lengths for n, m >= 1 follow by unwinding H(fold(n-1)‖H(x)) = H(fold(m-1)‖H(y)) until one side reaches 0. The 'therefore' skips the step the injectivity-across-lengths argument rests on.
EVIDENCE: README L498-501 'the fold over 0 items is the hash of 0 bytes, which cannot equal the hash of the 64 bytes any longer fold hashes. Equal folds therefore have equal lengths and, by induction, equal items.'
FIX: Replace L498-501 with the unwinding argument (text in d5:correctness:readme#4).
Whole-repo audit of rainlanguage/rain.lib.hash at 1e6e59f3d1fc4fd30c86a5836aa156eed382e729 (main, 2026-09-06; audit skill 0.35.0 at claude-audit-skills 9fefd09902d777aabc967ef6625d038e6c2101b7). Pass-2 gate: ledgered (adversarial-mutation-test record 2026-09-03 at 5055221444d547aba034b88fbd2525973b17d9a9; library code unchanged since). Fixes marked verified were applied to a scratch copy and run with forge in the rainix sol-shell (dbcd9d3c) during the audit; everything else is proposed only. Full run record: /home/gildlab/artifacts/rain.lib.hash-audit-2026-09-06/.
// Write C and D to scratch to produce the final hash Eannotateskeccak256(0, 0x40), which writes nothing; C and D were stored at L425/L430. Use "Hash C and D, already in scratch, to produce the final hash E".Outermostholding anOuterholding aFoois 2 words at each level" —Foois 4 words (asserted at L167); only the two outer sizes are asserted. Use "2 words at each of the two outer levels".hash(fold(n - 1) + hash(item)), a hash of 64 bytes. If the folds over n and m items are equal, with n <= m, peeling one layer at a time givesfold(n - 1)=fold(m - 1)and equal last-item hashes, down tofold(0)=fold(m - n); the hash of 0 bytes cannot equal a hash of 64 bytes, so n = m, and each peeled pair of equal item hashes is, by induction over the item type, a pair of equal items."Findings from the audit run
F49 [LOW] dim 3 cat — test/MemoryLayout.t.sol:7-8
Struct doc cites a README section "Memory layout" that does not exist
No README heading is named 'Memory layout'; the layout prose the tests pin lives under '### The pattern'. Every other section reference in the tests quotes a real heading.
EVIDENCE: test/MemoryLayout.t.sol L7-8
/// The struct README.md "Memory layout" describes: a 4-word region, one word;grep -n '^#' README.mdhas no such heading; claim at README L258-262 under### The pattern.FIX: L7:
/// The struct README.md "The pattern" describes: a 4-word region, one wordF46 [INFO] dim 3 cat — README.md:432-433 (test/HashPattern.t.sol:205-206)
Yul comment 'Write C and D to scratch' describes a step the code has already done
C is written to scratch at L425 and D at L430; the final statement only hashes them. The verbatim test copy carries the same comment.
EVIDENCE: README L425
mstore(0, keccak256(0, 0x40)), L430mstore(0x20, keccak256(add(deref_, 0x20), mload(deref_))), L432-433// Write C and D to scratch to produce the final hash E/let E := keccak256(0, 0x40); test/HashPattern.t.sol L205-206.FIX: README L432 and test L205:
// Hash C and D, already in scratch, to produce the final hash E.Fix: comment-only edit on both lines; no bytecode, no assertion, no ruling touched, compiles trivially (no forge run needed for a
//line). Proposed wording "Hash C and D, already in scratch, to produce the final hash E" is accurate and complete. No test is proposed and none is appropriate — the existingtestHandlingPointersalready assertse == keccak(C||D), so behaviour is pinned; the gap is purely documentation. Fix is complete: both occurrences (README + test) named.Severity: INFO is right. Skill scheme L118-120: "suggestion for improvement with no direct risk". Dimension 3 (documentation), not Pass-6, so the L373 hazard scale does not apply; even read as a hazard, the comment cannot mislead anyone into a wrong hash because the surrounding comments on L424/L429 already state C and D are stored. Production impact for the internal-function hashing library (sole source importer rainlang.interface signed-context digest): zero — README example code and a test comment, no library source. Keep INFO.
F47 [INFO] dim 3 cat — README.md:234-238, 259-262
'All non-struct types end up in one of 3 buckets' but the next example puts a struct in bucket one
Structs and static arrays are exactly bucket one; the qualifier contradicts the Foo example that follows.
EVIDENCE: README L234
All non-struct types end up in one of 3 buckets:; L259-260 'The thing it points to falls into the first bucket, a 4-word region of memory defined by its type.'FIX: README L234:
Every type ends up in one of 3 buckets:F51 [INFO] dim 3 cat — test/MemoryLayout.t.sol:231-233
Doc says the nested chain is '2 words at each level' but the innermost Foo is 4 words
Only Outermost and Outer are 2 words and only those two sizes are asserted; Foo is 0x80 (testFooIsFourWords).
EVIDENCE: L231-233 '
Outerholding aFoois 2 words at each level'; body L262, L264 assert the two outer sizes; L167 asserts Foo is 0x80.FIX: L232: '
Outerholding aFoois 2 words at each of the two outer levels, and'F66 [INFO] dim 5 cat — README.md:498-501
Fold induction attributes the equal-length conclusion to the base case alone
The base case separates only length 0 from >= 1; equal lengths for n, m >= 1 follow by unwinding H(fold(n-1)‖H(x)) = H(fold(m-1)‖H(y)) until one side reaches 0. The 'therefore' skips the step the injectivity-across-lengths argument rests on.
EVIDENCE: README L498-501 'the fold over 0 items is the hash of 0 bytes, which cannot equal the hash of the 64 bytes any longer fold hashes. Equal folds therefore have equal lengths and, by induction, equal items.'
FIX: Replace L498-501 with the unwinding argument (text in d5:correctness:readme#4).
Whole-repo audit of rainlanguage/rain.lib.hash at
1e6e59f3d1fc4fd30c86a5836aa156eed382e729(main, 2026-09-06; audit skill 0.35.0 at claude-audit-skills9fefd09902d777aabc967ef6625d038e6c2101b7). Pass-2 gate:ledgered(adversarial-mutation-test record 2026-09-03 at5055221444d547aba034b88fbd2525973b17d9a9; library code unchanged since). Fixes marked verified were applied to a scratch copy and run withforgein the rainix sol-shell (dbcd9d3c) during the audit; everything else is proposed only. Full run record:/home/gildlab/artifacts/rain.lib.hash-audit-2026-09-06/.