Skip to content

[RLH-31] [INFO] Tests that assert less than their names or docs claim: tautological assertTrue lines, allocator facts asserted for element 0 only with a wrong 'zero-initialised' doc, a boundary value never pinned, unused fuzz params #71

Description

@thedavidmeister
  • MemoryLayout L129 and L145 follow an assertEq that already fixes w, so the inequality compares two compile-time literals and can never fail: delete them (the NatSpec already states the contrast). HashPattern L177 and HashPatternFold L108 are real checks written as assertTrue(a != b), which prints no operands: use assertNotEq (forge-std 1.16.1 has both overloads). Verified 31/31.
  • HashPattern testFooListIsWordList (L69-105) pins the README word-list claim and additionally new Foo[](n) allocation order and sizes — a codegen fact the README disclaims — but only for element 0 and without reading the elements; its doc says "zero-initialised" while each element's words are 0, 0, 0x60, 0x60 (dynamic members point at the zero slot; verified). Keep only the length and word-equals-pointer assertions here; move the allocator facts, strengthened per element (fooPointer == ptr + 0x20 + n*0x20 + i*0x80, words 0, 0, 0x60, 0x60, foo_.c.length == 0; verified 1024 runs) to MemoryLayout under their own name with "default-initialised (value members 0, dynamic members pointing at the zero slot)".
  • HashPattern testBytesTrueLength asserts only wordOne_ == wordTwo_, not the README's stated word (0x01 then 31 zero bytes) nor the rounded-length collision it contrasts: add assertEq(wordOne_, uint256(bytes32(hex"01"))) and hash both at keccak256(add(x_, 0x20), 0x20) asserting the rounded hashes collide.
  • MemoryLayout testDeeplyNestedStructIsOnePointerWordPerLevel(uint256 x, uint256 y): x and y reach no assertion (the depth-1 sibling asserts w0 == x). Adding two new locals hits stack-too-deep; the verified stack-safe form reuses the dead locals after L266: assembly { w1 := mload(outermost) midW1 := mload(midPtr) } then assertEq(w1, y); assertEq(midW1, x); (or drop the params).

Findings from the audit run

F57 [INFO] dim 5 cat — test/MemoryLayout.t.sol:129, 145 (test/HashPattern.t.sol:177; test/HashPatternFold.t.sol:108)

assertTrue(a != b) sites: two in MemoryLayout are tautologies on literals after the preceding assertEq; two elsewhere print no operands where forge-std assertNotEq exists

MemoryLayout L129/L145 follow an assertEq that already fixes w, so the inequality compares two compile-time literals and can never fail; HashPattern L177 and HashPatternFold L108 are real checks written as assertTrue, which on failure prints only 'assertion failed'.

EVIDENCE: MemoryLayout L128-129 assertEq(w, type(uint256).max); assertTrue(w != uint256(uint8(int8(-1))));, L144-145 assertEq(w, uint256(0x01020304) << 224); assertTrue(w != uint256(uint32(0x01020304)));; HashPattern L177 assertTrue(hashOne_ != hashTwo_);; HashPatternFold L108 assertTrue(folded != hashX);; forge-std 1.16.1 StdAssertions declares assertNotEq(bytes32,bytes32) and (uint256,uint256).

FIX: Delete MemoryLayout L129 and L145 (keep the wording in NatSpec); HashPattern L177 assertNotEq(hashOne_, hashTwo_);; HashPatternFold L108 assertNotEq(folded, hashX);.

Correctness of the claim: confirmed. MemoryLayout L129 follows assertEq(w, type(uint256).max), so w != uint256(uint8(int8(-1))) reduces to max != 255 — unreachable-false; L145 likewise (0x01020304<<224) != 0x01020304. Deleting them weakens nothing; the NatSpec on both tests (L124, L140-141) already states the "not the zero-padded form" contrast the deleted lines were trying to express. The two assertNotEq swaps are strict improvements (operands printed on failure) with identical pass/fail semantics. One nuance the finding does not state: HashPattern L177 is also implied by the two assertEq on L178-179 (hashOne_ == keccak(hex"01") and hashTwo_ == keccak(hex"0100") are distinct absent a keccak collision), so it is redundant rather than a "real check"; the swap is still harmless and the line documents the README's "MUST respect the true length" claim directly, so keeping it as assertNotEq is fine. HashPatternFold L108 is a genuine (fuzzed) check that folding a singleton differs from the item hash.

No new test is proposed and none is needed: nothing fails today, nothing should — this is a test-hygiene change, not a coverage gap.

Severity: INFO is correct. Skill scheme L120: "suggestion for improvement with no direct risk." Test-only edit in an internal-function hashing library; the tautological lines cannot mask a defect because the assertEq immediately above each one pins the exact word value, so no production behaviour (including rainlang.interface's signed-context digest) depends on the change. Not a Pass-6 finding, so L373 does not apply; if it were, it would still be INFO (convention kept by discipline, cheaply enforced).

F61 [INFO] dim 5 cat — test/HashPattern.t.sol:69-105

testFooListIsWordList mixes the README word-list claim with allocator-layout facts: those are asserted only for element 0, their 'zero-initialised' doc is wrong (element words are 0, 0, 0x60, 0x60), and the README says the pattern does not depend on the allocator

L93-101 pin the README claim (word i+1 == pointer to foos_[i]); L89/91/102-104 additionally pin new Foo[](n) allocation order and sizes, a solc codegen fact the README disclaims at L357-359, but only for element 0 and without reading the elements, whose dynamic members point at the zero slot (which is also why the NoAlloc tests guard 0x60 — pinned nowhere). Three agents disagree on direction (drop vs strengthen); the thorough option keeps the word-list claim here and moves the allocator facts, strengthened, to MemoryLayout under their own name.

EVIDENCE: L71-73 doc 'one 0x80 zero-initialised Foo per element'; L89 assertEq(ptr, fmpBefore); L91 assertEq(fmpAfter - ptr, 0x20 + n * 0x20 + n * 0x80); L102-104 if (i == 0) { assertEq(fooPointer, ptr + 0x20 + n * 0x20); }. README L317-318 (the named claim), L357-359 'the pattern never depends on what the allocator does beyond it'. Probe at HEAD: each element mload = 0, 0, 0x60, 0x60; assertEq(fooPointer, ptr + 0x20 + n * 0x20 + i * 0x80) PASS 1024 runs.

FIX: testFooListIsWordList: keep only len and word-equals-pointer assertions (drop L76-79, 89, 91, 102-104 and the doc sentence). Add to test/MemoryLayout.t.sol testNewFooArrayAllocatesListThenElements(uint8 length) asserting per element fooPointer == ptr + 0x20 + n*0x20 + i*0x80 and the four words 0, 0, 0x60, 0x60 with foo_.c.length == 0, doc 'default-initialised (value members 0, dynamic members pointing at the zero slot)'.

F62 [INFO] dim 5 cat — test/HashPattern.t.sol:160-173

testBytesTrueLength asserts the two data words are equal but not the README's stated value or the rounded-up collision

README says hex"01" and hex"0100" share the word 0x01 followed by 31 zero bytes; the test asserts only wordOne_ == wordTwo_, so two equal but differently padded words would pass and the collision hashing the rounded word is never shown.

EVIDENCE: L173 assertEq(wordOne_, wordTwo_);; README L354-356.

FIX: After L173: assertEq(wordOne_, uint256(bytes32(hex"01"))); and hash both at keccak256(add(x_, 0x20), 0x20) asserting the rounded hashes collide (text in d5:correctness:HashPattern.t.sol#4).

F64 [INFO] dim 5 cat — test/MemoryLayout.t.sol:234-267

Fuzz inputs x and y of testDeeplyNestedStructIsOnePointerWordPerLevel never reach an assertion

x and y flow into the constructors only; the signature advertises value-word coverage that does not exist (the sibling test asserts w0 == x).

EVIDENCE: L234 (uint256 x, uint256 y); assertions L261-266 on pointers/fmp only.

FIX: Add w0 := mload(outermost), midW0 := mload(w1) and assertEq(w0, y); assertEq(midW0, x); (or drop the params).


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/.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

auditAudit findinginfoAudit findingpass5Audit finding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions