Skip to content

fix(string): reject signed Infinity and non-decimal literals in StringToNumber - #5509

Open
MaxFreedomPollard wants to merge 1 commit into
boa-dev:mainfrom
MaxFreedomPollard:fix/string-to-number-signed-literals
Open

fix(string): reject signed Infinity and non-decimal literals in StringToNumber#5509
MaxFreedomPollard wants to merge 1 commit into
boa-dev:mainfrom
MaxFreedomPollard:fix/string-to-number-signed-literals

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown

Number("+inf") returns Infinity and Number("0x+1") returns 1. Both must be NaN. The StringNumericLiteral grammar behind StringToNumber spells the infinite StrUnsignedDecimalLiteral as exactly Infinity, and gives NonDecimalIntegerLiteral no sign of its own. I found these by running Number() over a 1256 case corpus of signs, casings, prefixes and surrounding whitespace and diffing against Node 22. There were 144 divergences and every one traced back to one of the two causes below.

The infinity guard at core/string/src/str.rs:335 matches on the first byte of the string, so it only catches unsigned spellings. Infinity, +Infinity and -Infinity have already returned by that point, so anything reaching the guard that starts with i or I is invalid, but when a sign is present the first byte is the sign and the guard does not fire. Control then reaches fast_float2::parse, which accepts inf and infinity case insensitively with an optional sign (parse_inf_nan in fast-float2-0.2.4/src/number.rs). So Number("+inf"), Number("-Inf"), Number("+infinity") and Number("-INFINITY") all returned an infinity instead of NaN.

The 0b/0o/0x branch at core/string/src/str.rs:342 strips the prefix and passes the rest straight to u32::from_str_radix on line 349, and from_str_radix accepts a leading +. So Number("0x+1") and Number("0b+1") returned 1, and Number("0x+0") returned 0. Only the fast path is affected: Number("0x+FFFFFFFFFF") is too wide for u32 and falls through to the slow path, which already rejects the sign because + is not a digit. A leading - was already rejected because from_str_radix refuses one for an unsigned type.

This reaches user code through every string to number coercion, so Number("+inf"), +"+inf" and "+inf" * 1 were all affected. parseFloat and parseInt are separate code paths, already agree with Node on the same inputs, and are untouched here.

It changes the following:

  • The infinity guard also matches a sign followed by i or I, so signed non-canonical spellings return NaN alongside the unsigned ones it already caught.
  • The non-decimal branch returns NaN when the text after the 0b/0o/0x prefix starts with + or -, before from_str_radix sees it.
  • Adds a to_number test to core/string/src/tests.rs covering both families, the valid Infinity spellings, the valid prefixed literals, the u32 slow path via 0x1FFFFFFFF, and surrounding StrWhiteSpace.

Verification on aarch64-apple-darwin:

  • With only the test applied to unmodified main, cargo test -p boa_string fails at tests::to_number with "+inf is not a StringNumericLiteral". With the fix, cargo test -p boa_string reports 25 passed; 0 failed plus 2 passed doctests.
  • Rebuilding boa_cli and rerunning the 1256 case corpus against Node 22 gives zero divergences, down from 144.
  • cargo fmt --all --check is clean, and typos v1.50.1, the version pinned in .github/workflows/rust.yml, reports nothing on the changed files or the whole repository.
  • cargo clippy -p boa_string --all-features --all-targets -- -D warnings and the same with --no-default-features are clean on rustc 1.98.0.
  • cargo check -p boa_string --all-features --all-targets exits 0 on the 1.91.0 MSRV, and cargo doc -p boa_string --document-private-items --all-features exits 0 with RUSTDOCFLAGS=-D warnings.

…gToNumber

`JsStr::to_number` accepted two families of strings that the
`StringNumericLiteral` grammar rejects, so `Number(x)`, unary `+` and
arithmetic coercion returned a number where they must return `NaN`.

The infinity guard in `core/string/src/str.rs` matched on the first byte of
the string, so it only caught unsigned spellings. When a sign is present the
first byte is the sign, the guard does not fire, and `fast_float2::parse`
accepts `inf` and `infinity` case insensitively with an optional sign. That
made `Number("+inf")` return `Infinity` and `Number("-INFINITY")` return
`-Infinity`.

The `0b`/`0o`/`0x` branch of the same function passed the text after the
prefix straight to `u32::from_str_radix`, which accepts a leading `+`. A
`NonDecimalIntegerLiteral` is a bare sequence of digits, so `Number("0x+1")`
and `Number("0b+1")` both returned `1`. Only the fast path was affected:
values too wide for `u32` fall through to the slow path, which already
rejects a sign because `+` is not a digit.

The infinity guard now also matches a sign followed by `i` or `I`, and the
non-decimal branch rejects a leading sign before parsing. Adds a `to_number`
test to `core/string/src/tests.rs` covering both families together with the
valid spellings and the slow path.
@MaxFreedomPollard
MaxFreedomPollard requested a review from a team as a code owner September 6, 2026 02:58
@github-actions github-actions Bot added the Waiting On Review Waiting on reviews from the maintainers label Sep 6, 2026
@github-actions github-actions Bot added this to the v0.23 milestone Sep 6, 2026
@github-actions github-actions Bot added the C-Tests Issues and PRs related to the tests. label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Test262 conformance changes

Test result main count PR count difference
Total 53,578 53,578 0
Passed 51,426 51,426 0
Ignored 1,648 1,648 0
Failed 504 504 0
Panics 0 0 0
Conformance 95.98% 95.98% 0.00%

Tested main commit: 4b61183443403997a98da829dae287ce10358e1c
Tested PR commit: 858b3d534fccebfab9c04014e683f907e972718d
Compare commits: 4b61183...858b3d5

@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.91%. Comparing base (6ddc2b4) to head (858b3d5).
⚠️ Report is 1051 commits behind head on main.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #5509       +/-   ##
===========================================
+ Coverage   47.24%   62.91%   +15.66%     
===========================================
  Files         476      536       +60     
  Lines       46892    60286    +13394     
===========================================
+ Hits        22154    37928    +15774     
+ Misses      24738    22358     -2380     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread core/string/src/str.rs
Comment on lines +335 to +340
//
// `Infinity`, `+Infinity` and `-Infinity` are the only spellings accepted by
// `StrUnsignedDecimalLiteral`, and all three already returned above. Anything else
// starting with `i` or `I`, with or without a sign, is not a `StringNumericLiteral`,
// but `fast_float2` would still parse it as an infinity.
(Some(b'i' | b'I'), _) | (Some(b'+' | b'-'), Some(b'i' | b'I')) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just filter for non-finite numbers after fast_float2::parse? What I'm thinking is that we already covered all types of parseable infinities, so if fast_float2::parse returns infinity we can just return NaN in that specific case, right?

@jedel1043 jedel1043 Sep 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like this at the end

        match fast_float2::parse::<f64, &str>(string) {
            Ok(f) if f.is_finite() => f,
            // Rejects any other strings parsed as infinity
            Ok(_) | Err(_) => f64::NAN,
        }

Comment thread core/string/src/str.rs
Comment on lines +350 to +353
// A `NonDecimalIntegerLiteral` is a bare sequence of digits. A sign is only part of
// `StrDecimalLiteral`, which cannot carry a `0b`, `0o` or `0x` prefix, so a sign here
// makes the whole string invalid. `u32::from_str_radix` accepts a leading `+`, so
// without this check `0x+1` would parse as `1`.

@jedel1043 jedel1043 Sep 6, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// A `NonDecimalIntegerLiteral` is a bare sequence of digits. A sign is only part of
// `StrDecimalLiteral`, which cannot carry a `0b`, `0o` or `0x` prefix, so a sign here
// makes the whole string invalid. `u32::from_str_radix` accepts a leading `+`, so
// without this check `0x+1` would parse as `1`.
// Rejects things like `0x+1` or `0o-1`

Simple is better

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

Labels

C-Tests Issues and PRs related to the tests. Waiting On Review Waiting on reviews from the maintainers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants