From b842a6c6c6aabe38c552d7230cfeac683b7e411e Mon Sep 17 00:00:00 2001 From: Changjoon Date: Sun, 19 Apr 2026 22:32:57 +0900 Subject: [PATCH] Fix struct_time field overflow to raise OverflowError in time module (#7627) * Fix struct_time field overflow to raise OverflowError in time module * Address CodeRabbit review: cover tm_gmtoff and chain AssertionError * Fix ruff format: single space before inline comment --- crates/vm/src/stdlib/time.rs | 86 +++++++++++------------------ extra_tests/snippets/stdlib_time.py | 32 +++++++++++ 2 files changed, 64 insertions(+), 54 deletions(-) diff --git a/crates/vm/src/stdlib/time.rs b/crates/vm/src/stdlib/time.rs index 2cc2f5f50..3f2c3ba82 100644 --- a/crates/vm/src/stdlib/time.rs +++ b/crates/vm/src/stdlib/time.rs @@ -27,7 +27,7 @@ unsafe extern "C" { mod decl { use crate::{ AsObject, Py, PyObjectRef, PyResult, VirtualMachine, - builtins::{PyStrRef, PyTypeRef}, + builtins::{PyBaseExceptionRef, PyStrRef, PyTypeRef}, function::{Either, FuncArgs, OptionalArg}, types::{PyStructSequence, struct_sequence_new}, }; @@ -354,6 +354,13 @@ mod decl { ) -> PyResult { let invalid_tuple = || vm.new_type_error(format!("{func_name}(): illegal time tuple argument")); + let classify_err = |e: PyBaseExceptionRef| { + if e.class().is(vm.ctx.exceptions.overflow_error) { + vm.new_overflow_error(format!("{func_name} argument out of range")) + } else { + invalid_tuple() + } + }; let year: i64 = t.tm_year.clone().try_into_value(vm).map_err(|e| { if e.class().is(vm.ctx.exceptions.overflow_error) { @@ -370,46 +377,30 @@ mod decl { .tm_mon .clone() .try_into_value::(vm) - .map_err(|_| invalid_tuple())? + .map_err(classify_err)? - 1; - let tm_mday = t - .tm_mday - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - let tm_hour = t - .tm_hour - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - let tm_min = t - .tm_min - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - let tm_sec = t - .tm_sec - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + let tm_mday = t.tm_mday.clone().try_into_value(vm).map_err(classify_err)?; + let tm_hour = t.tm_hour.clone().try_into_value(vm).map_err(classify_err)?; + let tm_min = t.tm_min.clone().try_into_value(vm).map_err(classify_err)?; + let tm_sec = t.tm_sec.clone().try_into_value(vm).map_err(classify_err)?; let tm_wday = (t .tm_wday .clone() .try_into_value::(vm) - .map_err(|_| invalid_tuple())? + .map_err(classify_err)? + 1) % 7; let tm_yday = t .tm_yday .clone() .try_into_value::(vm) - .map_err(|_| invalid_tuple())? + .map_err(classify_err)? - 1; let tm_isdst = t .tm_isdst .clone() .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + .map_err(classify_err)?; let mut tm: libc::tm = unsafe { core::mem::zeroed() }; tm.tm_year = year - 1900; @@ -474,7 +465,7 @@ mod decl { .tm_gmtoff .clone() .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + .map_err(classify_err)?; tm.tm_gmtoff = gmtoff as _; } @@ -963,41 +954,28 @@ mod decl { vm: &VirtualMachine, ) -> PyResult { let invalid_tuple = || vm.new_type_error("mktime(): illegal time tuple argument"); - let year: i32 = t - .tm_year - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + let classify_err = |e: PyBaseExceptionRef| { + if e.class().is(vm.ctx.exceptions.overflow_error) { + vm.new_overflow_error("mktime argument out of range") + } else { + invalid_tuple() + } + }; + let year: i32 = t.tm_year.clone().try_into_value(vm).map_err(classify_err)?; if year < i32::MIN + 1900 { return Err(vm.new_overflow_error("year out of range")); } let mut tm: libc::tm = unsafe { core::mem::zeroed() }; - tm.tm_sec = t - .tm_sec - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - tm.tm_min = t - .tm_min - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - tm.tm_hour = t - .tm_hour - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; - tm.tm_mday = t - .tm_mday - .clone() - .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + tm.tm_sec = t.tm_sec.clone().try_into_value(vm).map_err(classify_err)?; + tm.tm_min = t.tm_min.clone().try_into_value(vm).map_err(classify_err)?; + tm.tm_hour = t.tm_hour.clone().try_into_value(vm).map_err(classify_err)?; + tm.tm_mday = t.tm_mday.clone().try_into_value(vm).map_err(classify_err)?; tm.tm_mon = t .tm_mon .clone() .try_into_value::(vm) - .map_err(|_| invalid_tuple())? + .map_err(classify_err)? - 1; tm.tm_year = year - 1900; tm.tm_wday = -1; @@ -1005,13 +983,13 @@ mod decl { .tm_yday .clone() .try_into_value::(vm) - .map_err(|_| invalid_tuple())? + .map_err(classify_err)? - 1; tm.tm_isdst = t .tm_isdst .clone() .try_into_value(vm) - .map_err(|_| invalid_tuple())?; + .map_err(classify_err)?; Ok(tm) } diff --git a/extra_tests/snippets/stdlib_time.py b/extra_tests/snippets/stdlib_time.py index 9a92969f5..1629443a9 100644 --- a/extra_tests/snippets/stdlib_time.py +++ b/extra_tests/snippets/stdlib_time.py @@ -17,3 +17,35 @@ assert x2.tm_min == 16 s = time.asctime(x) # print(s) assert s == "Thu Jan 1 00:16:40 1970" + + +# Regression test for RustPython issue #4938: +# struct_time field overflow should raise OverflowError (matching CPython), +# not TypeError. Covers mktime, asctime, and strftime. +I32_MAX_PLUS_1 = 2147483648 +overflow_cases = [ + (I32_MAX_PLUS_1, 1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in year + (2024, I32_MAX_PLUS_1, 1, 0, 0, 0, 0, 0, 0), # i32 overflow in month + (2024, 1, I32_MAX_PLUS_1, 0, 0, 0, 0, 0, 0), # i32 overflow in mday + (2024, 1, 1, 0, 0, I32_MAX_PLUS_1, 0, 0, 0), # i32 overflow in sec + (88888888888,) * 9, # multi-field i32 overflow +] + +for case in overflow_cases: + for func_name, call in [ + ("mktime", lambda c=case: time.mktime(c)), + ("asctime", lambda c=case: time.asctime(c)), + ("strftime", lambda c=case: time.strftime("%Y", c)), + ]: + try: + call() + except OverflowError: + pass # expected, matches CPython + except TypeError as e: + raise AssertionError( + f"{func_name}({case}) raised TypeError (should be OverflowError): {e}" + ) from e + else: + raise AssertionError( + f"{func_name}({case}) did not raise — expected OverflowError" + )