Skip to content

[AUD-011] [LOW] testUnsafeStrToSignedIntEmpty bounds its fuzz domain to uint16 on a premise the guard hoist made stale #81

Closed

Description

@thedavidmeister

Whole-repo audit 2026-08-24 at commit 3e76bab (audit skill 0.35.0). Pass 2 status: ledgered (audit/mutation-test-scans.json, scan of 2026-08-24T08:55:39Z at 35496d8).

test/src/lib/parse/LibParseDecimal.unsafeDecimalStringToSignedInt.t.sol:64-74

testUnsafeStrToSignedIntEmpty narrows the fuzz domain to uint16 (end = bound(end, 0, type(uint16).max); start = bound(start, end, type(uint16).max)) on a premise the comment states and that is false of current source: "The negative sign check reads memory at start". In LibParseDecimal.unsafeDecimalStringToSignedInt the start >= end branch returns ParseEmptyDecimalString before the isMask sign check runs (src/lib/parse/LibParseDecimal.sol lines 151-153), and isMask is itself branchless-bounds-guarded, so no memory at start is ever read for an empty region. The unsigned counterpart (testUnsafeDecimalStrToIntEmpty) fuzzes the full uint256 pointer domain; the signed empty behavior is asserted only for pointers <= 65535, a narrowing justified by a misdescription of the code under test.

Proposed fix

test/src/lib/parse/LibParseDecimal.unsafeDecimalStringToSignedInt.t.sol lines 64-71:

  • /// Test that when start is greater than or equal to end, the signed
  • /// conversion returns the empty string error selector and a zero value.
  • /// The negative sign check reads memory at start, so start is bound to
  • /// pointers that can be read without excessive memory expansion.
  • function testUnsafeStrToSignedIntEmpty(uint256 start, uint256 end) external pure {
  •    end = bound(end, 0, type(uint16).max);
    
  •    start = bound(start, end, type(uint16).max);
    
  • /// Test that when start is greater than or equal to end, the signed
  • /// conversion returns the empty string error selector and a zero value.
  • /// The empty check returns before the negative sign check, so no memory
  • /// is read and the full pointer domain is safe to fuzz.
  • function testUnsafeStrToSignedIntEmpty(uint256 start, uint256 end) external pure {
  •    start = bound(start, end, type(uint256).max);
    

This matches the unsigned test's domain exactly (start == 0, end == 0 included, which exercises the documented empty-before-zero-pointer ordering).

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

Metadata

Metadata

Labels

auditAudit findinglowAudit findingpass5Audit finding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions