From 8ce3d16e827d006954ab863aecca3048fdcb80a7 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Fri, 28 Aug 2026 11:17:25 -0500 Subject: [PATCH] fix(builtins): guard against exponent-too-large in units.parse/parse_bytes Inputs like "1e9999999K" caused Number::from_str to fail after silently constructing a string with an enormous exponent, risking numeric overflow or downstream panics. Add an early check that rejects any number whose exponent field exceeds 6 digits. - Add exponent_too_large() helper: find 'e'/'E', strip optional sign, reject if digit count > 6 - Call it in parse() before the ten_exp/two_exp dispatch - Call it in parse_bytes() before the twob_exp/tenb_exp dispatch - Add YAML regression test covering: units.parse too-large exponent (error), units.parse_bytes too-large exponent (error), and a normal 1e6 exponent that must still succeed Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/builtins/units.rs | 18 +++++++++++ .../cases/builtins/units/exponent.yaml | 30 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/interpreter/cases/builtins/units/exponent.yaml diff --git a/src/builtins/units.rs b/src/builtins/units.rs index bdc1b7026..18209f156 100644 --- a/src/builtins/units.rs +++ b/src/builtins/units.rs @@ -95,6 +95,10 @@ fn parse(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Re // a stable representation that includes an explicit leading digit. let canonical_part = canonicalize_number_part(number_part); + if exponent_too_large(canonical_part.as_ref()) { + bail!(params[0].span().error("units.parse: exponent too large")); + } + if let Some(e) = ten_exp(suffix) { let combined = combine_decimal_exponent(canonical_part.as_ref(), e) .ok_or_else(|| params[0].span().error("could not parse number"))?; @@ -169,6 +173,12 @@ fn parse_bytes(span: &Span, params: &[Ref], args: &[Value], strict: bool) // a stable representation that includes an explicit leading digit. let canonical_part = canonicalize_number_part(number_part); + if exponent_too_large(canonical_part.as_ref()) { + bail!(params[0] + .span() + .error("units.parse_bytes: exponent too large")); + } + if let Some(e) = twob_exp(suffix) { let mut number = match Number::from_str(canonical_part.as_ref()) { Ok(n) => n, @@ -245,3 +255,11 @@ fn split_decimal_exponent(number: &str) -> Option<(&str, i32)> { let exponent = exp_part.parse::().ok()?; Some((mantissa, exponent)) } + +fn exponent_too_large(number: &str) -> bool { + number + .find(['e', 'E']) + .and_then(|idx| number.get(idx + 1..)) + .map(|exponent| exponent.strip_prefix(['+', '-']).unwrap_or(exponent)) + .is_some_and(|digits| digits.len() > 6) +} diff --git a/tests/interpreter/cases/builtins/units/exponent.yaml b/tests/interpreter/cases/builtins/units/exponent.yaml new file mode 100644 index 000000000..30562d1b9 --- /dev/null +++ b/tests/interpreter/cases/builtins/units/exponent.yaml @@ -0,0 +1,30 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: parse-exponent-too-large + data: {} + modules: + - | + package test + x = units.parse("1e9999999K") + query: data.test.x + error: "units.parse: exponent too large" + + - note: parse-bytes-exponent-too-large + data: {} + modules: + - | + package test + x = units.parse_bytes("1e9999999Ki") + query: data.test.x + error: "units.parse_bytes: exponent too large" + + - note: parse-normal-exponent-ok + data: {} + modules: + - | + package test + x = units.parse("1e6K") + query: data.test.x + want_result: 1e9