From 24a3d91b1e7aba1a185a9d0f3f43472133e4c4e0 Mon Sep 17 00:00:00 2001 From: Sharadhi V Date: Sat, 26 Sep 2026 16:38:34 +0530 Subject: [PATCH] Fix u32 overflow in the Rust duration parser --- rust/src/parsing.rs | 25 +++++++++++++++++++------ tests/parsing/test_parsing_duration.py | 17 +++++++++++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/rust/src/parsing.rs b/rust/src/parsing.rs index 374fe3cfb..01bee1ee2 100644 --- a/rust/src/parsing.rs +++ b/rust/src/parsing.rs @@ -144,6 +144,12 @@ impl<'a> Parser<'a> { } } + fn checked_duration_add(&mut self, current: u32, value: u32) -> Result { + current + .checked_add(value) + .ok_or_else(|| self.parse_error("Duration value is too large".to_string())) + } + fn unexpected_character_error( &mut self, field_name: &str, @@ -632,7 +638,8 @@ impl<'a> Parser<'a> { ); } - duration.hours += value; + duration.hours = + self.checked_duration_add(duration.hours, value)?; if let Some(fraction) = op_fraction { let extra_minutes = fraction * 60_f64; @@ -656,7 +663,8 @@ impl<'a> Parser<'a> { ); } - duration.minutes += value; + duration.minutes = + self.checked_duration_add(duration.minutes, value)?; if let Some(fraction) = op_fraction { let extra_seconds = fraction * 60_f64; @@ -729,7 +737,10 @@ impl<'a> Parser<'a> { if let Some(fraction) = op_fraction { let extra_days = fraction * 7_f64; let extra_full_days = extra_days.trunc(); - duration.days += extra_full_days as u32; + duration.days = self.checked_duration_add( + duration.days, + extra_full_days as u32, + )?; let extra_hours = (extra_days - extra_full_days) * 24.0; let extra_full_hours = extra_hours.trunc(); duration.hours += extra_full_hours as u32; @@ -755,7 +766,7 @@ impl<'a> Parser<'a> { )); } - duration.days += value; + duration.days = self.checked_duration_add(duration.days, value)?; if let Some(fraction) = op_fraction { let extra_hours = fraction * 24.0; let extra_full_hours = extra_hours.trunc(); @@ -820,8 +831,10 @@ impl<'a> Parser<'a> { }; while let Some(digit) = self.inc().and_then(|ch| ch.to_digit(10)) { - value *= 10; - value += digit; + value = value + .checked_mul(10) + .and_then(|value| value.checked_add(digit)) + .ok_or_else(|| self.parse_error("Duration value is too large".to_string()))?; } Ok(value) diff --git a/tests/parsing/test_parsing_duration.py b/tests/parsing/test_parsing_duration.py index d6a0b73d7..2df99d02e 100644 --- a/tests/parsing/test_parsing_duration.py +++ b/tests/parsing/test_parsing_duration.py @@ -4,6 +4,7 @@ from pendulum.parsing import ParserError from pendulum.parsing import parse +from pendulum.parsing import with_extensions def test_parse_duration(): @@ -304,3 +305,19 @@ def test_parse_interval_invalid(): def test_parse_duration_fraction_only_allowed_on_last_component(): with pytest.raises(ParserError): parse("P2Y3M4DT5.5H6M7S") + + +@pytest.mark.skipif( + not with_extensions, reason="Only the Rust parser bounds duration values" +) +def test_parse_duration_value_too_large(): + # 2**32 seconds does not fit the parser's accumulator + with pytest.raises(ParserError): + parse("PT4294967296S") + + # Repeating a unit must not wrap the accumulated value either + with pytest.raises(ParserError): + parse("PT4294967295H1H") + + with pytest.raises(ParserError): + parse("P4294967295D0.5W")