Validate sleep against signed nanosecond time bounds (#9051)

Assisted-by: OpenAI Codex:model identifier unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-10 12:52:23 +00:00
1 parent 1889ee627f
commit 146bb809bb
2 files changed
+100 -19

No files matched your search

+68 -19
View File
@@ -14,10 +14,13 @@ mod decl {
#[cfg(any(unix, windows))]
use crate::builtins::PyBaseExceptionRef;
#[cfg(any(unix, windows))]
use crate::convert::{ToPyException, ToPyObject};
use crate::{
AsObject, Py, PyObjectRef, PyResult, VirtualMachine,
builtins::{PyStr, PyStrRef, PyTypeRef},
builtins::{PyFloat, PyStr, PyStrRef, PyTypeRef},
class::PyClassDef,
common::wtf8::{Wtf8Buf, wtf8_concat},
function::{Either, FuncArgs, OptionalArg, OptionalOption},
types::{PyStructSequence, PyStructSequenceData, struct_sequence_new},
};
@@ -26,14 +29,10 @@ mod decl {
PyRef,
builtins::{PyNamespace, PyUtf8StrRef},
};
#[cfg(any(unix, windows))]
use crate::{
common::wtf8::Wtf8Buf,
convert::{ToPyException, ToPyObject},
};
use core::time::Duration;
#[cfg(not(any(unix, windows)))]
use jiff::{Timestamp, Zoned, civil::DateTime, tz::TimeZone};
use num_traits::ToPrimitive;
#[cfg(target_os = "wasi")]
use rustpython_host_env::time::ClockId;
#[cfg(any(unix, windows))]
@@ -128,21 +127,71 @@ mod decl {
fn sleep(object: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
vm.audit("time.sleep", || (object.clone(),))?;
let seconds_type_name = object.class().name().to_owned();
let dur = object.try_into_value::<Duration>(vm).map_err(|e| {
if e.class().is(vm.ctx.exceptions.value_error)
&& let Some(s) = e.args().as_slice().first().and_then(|arg| arg.str(vm).ok())
&& s.as_bytes() == b"negative duration"
{
return vm.new_value_error("sleep length must be non-negative");
let overflow = || vm.new_overflow_error("timestamp out of range for C PyTime_t");
let nanoseconds = if let Some(float) = object.downcast_ref::<PyFloat>() {
let seconds = float.to_f64();
if seconds.is_nan() {
return Err(vm.new_value_error("Invalid value NaN (not a number)"));
}
if e.class().is(vm.ctx.exceptions.type_error) {
return vm.new_type_error(format!(
"'{seconds_type_name}' object cannot be interpreted as an integer or float"
));
// _PyTime_ROUND_TIMEOUT rounds away from zero.
let nanoseconds = seconds * SEC_TO_NS as f64;
let nanoseconds = if nanoseconds >= 0.0 {
nanoseconds.ceil()
} else {
nanoseconds.floor()
};
let minimum = i64::MIN as f64;
// The signed maximum rounds up to 2**63 as a double.
if !(minimum..-minimum).contains(&nanoseconds) {
return Err(overflow());
}
e
})?;
nanoseconds as i64
} else {
let seconds = object.try_index(vm).or_else(|e| {
if e.fast_isinstance(vm.ctx.exceptions.overflow_error) {
Err(overflow())
} else if e.fast_isinstance(vm.ctx.exceptions.type_error) {
// CPython's %T reads stored type metadata, preserving surrogates.
let class = object.class();
let name = if let Some(heap_type) = &class.heaptype_ext {
let qualname = heap_type.qualname.read().clone();
let module = if let Some(dict) = class.attributes.as_dict() {
dict.get_item_opt(identifier!(vm, __module__), vm)?
.ok_or_else(|| vm.new_attribute_error("__module__"))?
} else {
class.__module__(vm)?
};
match module.downcast_ref::<PyStr>() {
Some(module)
if module.as_wtf8() != "builtins"
&& module.as_wtf8() != "__main__" =>
{
wtf8_concat!(module.as_wtf8(), ".", qualname.as_wtf8())
}
_ => qualname.as_wtf8().to_owned(),
}
} else {
Wtf8Buf::from(class.slot_name().to_owned())
};
Err(vm.new_type_error(wtf8_concat!(
"'",
name,
"' object cannot be interpreted as an integer or float"
)))
} else {
Err(e)
}
})?;
seconds
.as_bigint()
.to_i64()
.and_then(|seconds| seconds.checked_mul(SEC_TO_NS))
.ok_or_else(overflow)?
};
if nanoseconds < 0 {
return Err(vm.new_value_error("sleep length must be non-negative"));
}
let dur = Duration::from_nanos(nanoseconds as u64);
#[cfg(unix)]
{
+32
View File
@@ -1,6 +1,8 @@
import sys
import time
from testutils import assert_raises
if sys.platform == "win32":
# Record the native runtime before evaluating the range regression below.
print("Windows time runtime:", sys.version, sys.implementation, sys.flags)
@@ -165,6 +167,36 @@ perf_elapsed = time.perf_counter() - perf_start
assert monotonic_elapsed >= 0.01
assert perf_elapsed >= 0.01
# Sleep validates signed nanoseconds before rejecting negative durations.
for duration in (
2**63,
-(2**63) - 1,
2**34,
-(2**34),
2**63 / 1_000_000_000,
-1e10,
float("inf"),
-float("inf"),
):
with assert_raises(OverflowError) as error:
time.sleep(duration)
assert str(error.exception) == "timestamp out of range for C PyTime_t"
assert_raises(ValueError, time.sleep, float("nan"))
class SleepTypeNamespace:
class Invalid:
pass
for module, prefix in (("__main__", ""), ("sleep_test", "sleep_test.")):
SleepTypeNamespace.Invalid.__module__ = module
with assert_raises(TypeError) as error:
time.sleep(SleepTypeNamespace.Invalid())
assert str(error.exception) == (
f"'{prefix}SleepTypeNamespace.Invalid' object cannot be interpreted as an integer or float"
)
# The optional second argument fills the fields that are not part of the
# sequence.
fields = (2024, 1, 2, 3, 4, 5, 6, 7, 0)