Skip to content

json_decode silently truncates long numbers to a wrong magnitude (padded 1e50 decodes to 1) #628

Description

@InauguralPhysicist

Finding

json_decode caps a number token at 63 characters and keeps scanning, so the dropped tail is silently discarded and atof parses whatever prefix survived. The result is a valid-looking number of the wrong magnitude, with rc=0.

src/builtins.c:958:

#define JSON_NUM_PUSH() do { if (len < 63) numbuf[len++] = s[p]; p++; } while (0)

That's the if (count < CAP) { write }-with-no-else shape from the #490#512 silent-tolerance train, in a builtin that sweep didn't reach.

Reproduction

a is json_decode of "{\"v\": 1.000000000000000000000000000000000000000000000000000000000000e50}"
print of ["padded 1e50 ->", a["v"]]
b is json_decode of "{\"v\": 1000000000000000000000000000000000000000000000000000000000000000000000}"
print of ["70-digit int ->", b["v"]]
print of ["the lexer parses the same literals as", 1e50, 1e69]
["padded 1e50 ->", 1]                              <- off by 50 orders of magnitude
["70-digit int ->", 1e+62]                         <- off by 7 orders of magnitude
["the lexer parses the same literals as", 1e+50, 1e+69]

The first case is the sharp one: the e50 exponent falls off the end of numbuf, so 1.000…000e50 decodes to 1. Both inputs are valid JSON, and the runtime's own lexer parses the identical literals correctly — so json_decode diverges from the language's own number oracle.

Invariant violated

json_decode must yield the correctly-rounded double for any grammatically valid JSON number, or set g_json_parse_err. It does neither.

The function's own header states the rule it breaks — from #495: "a malformed tail (1e, 1e+, 1.) sets g_json_parse_err instead of letting atof() guess silently". The rationalizing comment ("beyond double's ~24-char maximum the extra digits carry no value anyway") is false precisely when the truncated tail is the exponent or significant integer digits.

This matters because json_decode is the primary untrusted-input boundary. A producer emitting padded or full-precision numbers gets a silently wrong value.

Not load-bearing

Checked tests/test_json*.eigs, test_json_hard, test_json_roundtrip, test_json_large, test_corpus — none exercise a >63-char number and nothing asserts on truncation. Round-trip is unaffected (json_encode emits the compact form), which is why the suite is green.

Fix

Don't truncate. The scanner already validates the grammar, so strtod cannot guess: record the token span (start = *pos, end = p) and strtod directly from s + start, dropping numbuf entirely. Strictly more correct at every length.

(Setting g_json_parse_err at len >= 63 is the minimal alternative, but it rejects valid JSON — prefer the in-place strtod.)

Test to add

json_decode of "[1.<60 zeros>e50]" asserted against the literal 1e50, and the 70-digit integer against 1e69.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions