Skip to content

Coefficient and exponent signs use skip-many for an optional-single dash: multi-dash input is rejected only by accidental arithmetic wrap, with a misleading overflow selector #272

Description

@thedavidmeister

Unit

src/lib/parse/LibParseDecimalFloat.sol parseDecimalFloatInline: coefficient sign at ~line 46 and exponent sign at ~line 144 are both consumed with LibParseChar.skipMask(cursor, end, CMASK_NEGATIVE_SIGN) — skip-MANY — for a token that is optional-single.

Violated property

A sign is zero-or-one dash; --5, ---5, 1e--2 should be malformed-input errors. What actually happens (traced against current source):

  1. skipMask consumes every dash; isNegative = cursor != start collapses any count to a boolean (used only to sign the fractional part).
  2. The coefficient region handed to LibParseDecimal.unsafeDecimalStringToSignedInt is (start, cursor) — dash-inclusive. The signed parser consumes exactly ONE leading dash; each surviving dash flows into the inner unsigned parser's unchecked byte - '0' arithmetic as −3·10^k, wrapping the accumulated value into the top of the uint256 range, which then trips the negative overflow bound.
  3. Result: --5(ParseDecimalOverflow.selector, ...). Rejection is reliable for every multi-dash form, but only by accident of the wrap landing above the bound, and the selector says "overflow" for input that is malformed, not large.

Why it deserves a real guard

The rejection chain couples three unrelated behaviors (skip-many width, the signed parser's single-dash contract, unchecked-wrap magnitudes). Any refactor of the inner parser's garbage handling — e.g. validating chars, or changing failure-value conventions — can silently turn accidental rejection into acceptance. rain.string's campaign already touched those overflow paths this week (rain.string PR #60).

Proposed fix

Replace both skip-many calls with a single-char check (isMask + cursor += isNeg, mirroring unsafeDecimalStringToSignedInt itself), so a second dash lands in the digit check and returns the existing malformed/empty selectors. Tests: --5, ---5, 1e--2, -.5, lone - pinned to exact selectors; -5, 1e-2 unchanged.

Found by the 2026-08-24 rain.string CMask consumer-oracle audit (CMASK_NEGATIVE_SIGN's value is correct; the primitive choice at this consumer is the defect).

🤖 Generated with Claude Code

Activity

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions