8000 Handle negative time.sleep values by ever0de · Pull Request #5906 · RustPython/RustPython · GitHub
[go: up one dir, main page]

Skip to content

Handle negative time.sleep values #5906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
fix(time): Handle negative sleep values
  • Loading branch information
ever0de committed Jul 6, 2025
commit 0e6e6b9ccb177a26d2f53c67c1afa1fb0026716b
16 changes: 13 additions & 3 deletions vm/src/convert/try_from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
builtins::PyFloat,
object::{AsObject, PyObject, PyObjectRef, PyPayload, PyRef, PyResult},
};
use malachite_bigint::Sign;
use num_traits::ToPrimitive;

/// Implemented by any type that can be created from a Python object.
Expand Down Expand Up @@ -124,10 +125,19 @@ impl<'a, T: PyPayload> TryFromBorrowedObject<'a> for &'a Py<T> {
impl TryFromObject for std::time::Duration {
fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
if let Some(float) = obj.payload::<PyFloat>() {
Ok(Self::from_secs_f64(float.to_f64()))
let f = float.to_f64();
if f < 0.0 {
return Err(vm.new_value_error("negative duration"));
}
Ok(Self::from_secs_f64(f))
} else if let Some(int) = obj.try_index_opt(vm) {
let sec = int?
.as_bigint()
let int = int?;
let bigint = int.as_bigint();
if bigint.sign() == Sign::Minus {
return Err(vm.new_value_error("negative duration"));
}

let sec = bigint
.to_u64()
.ok_or_else(|| vm.new_value_error("value out of range"))?;
Ok(Self::from_secs(sec))
Expand Down
39 changes: 35 additions & 4 deletions vm/src/stdlib/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,25 @@ mod decl {

#[cfg(not(unix))]
#[pyfunction]
fn sleep(dur: Duration) {
fn sleep(seconds: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
let dur = seconds.try_into_value::<Duration>(vm).map_err(|e| {
if e.class().is(vm.ctx.exceptions.value_error) {
// Check if this is a "negative duration" error by examining the args
if let Some(args) = e.args().first() {
if let Ok(s) = args.str(vm) {
if s.as_str() == "negative duration" {
return vm.new_value_error("sleep length must be non-negative");
}
}
}
e
} else {
e
}
})?;

Choose a reason for hiding this comment

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

medium

This error mapping logic is verbose and duplicated in the unix implementation of sleep (lines 712-726). Relying on string matching for error messages can be fragile. Consider extracting this logic into a helper function to avoid duplication.

        let dur = seconds.try_into_value::<Duration>(vm).map_err(|e| {
            if e.class().is(vm.ctx.exceptions.value_error) {
                if let Some(s) = e.args().first().and_then(|arg| arg.str(vm).ok()) {
                    if s.as_str() == "negative duration" {
                        return vm.new_value_error("sleep length must be non-negative");
                    }
                }
            }
            e
        })?;


std::thread::sleep(dur);
Ok(())
}

#[cfg(not(target_os = "wasi"))]
Expand Down Expand Up @@ -525,7 +542,7 @@ mod platform {
use super::decl::{SEC_TO_NS, US_TO_NS};
#[cfg_attr(target_os = "macos", allow(unused_imports))]
use crate::{
PyObject, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine,
AsObject, PyObject, PyObjectRef, PyRef, PyResult, TryFromBorrowedObject, VirtualMachine,
builtins::{PyNamespace, PyStrRef},
convert::IntoPyException,
};
Expand Down Expand Up @@ -691,8 +708,22 @@ mod platform {
}

#[pyfunction]
fn sleep(dur: Duration, vm: &VirtualMachine) -> PyResult<()> {
// this is basically std::thread::sleep, but that catches interrupts and we don't want to;
Copy link
Member

Choose a reason for hiding this comment

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

This comment is not expected to be removed

fn sleep(seconds: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
let dur = seconds.try_into_value::<Duration>(vm).map_err(|e| {
if e.class().is(vm.ctx.exceptions.value_error) {
// Check if this is a "negative duration" error by examining the args
if let Some(args) = e.args().first() {
if let Ok(s) = args.str(vm) {
if s.as_str() == "negative duration" {
return vm.new_value_error("sleep length must be non-negative");
}
}
}
e
} else {
e
}
})?;

Choose a reason for hiding this comment

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

medium

This duplicates the error mapping logic in the non-unix sleep function (lines 94-108). Code duplication can lead to maintenance issues. Extract this logic into a shared private helper function used by both sleep implementations.

        let dur = seconds.try_into_value::<Duration>(vm).map_err(|e| {
            if e.class().is(vm.ctx.exceptions.value_error) {
                if let Some(s) = e.args().first().and_then(|arg| arg.str(vm).ok()) {
                    if s.as_str() == "negative duration" {
                        return vm.new_value_error("sleep length must be non-negative");
                    }
                }
            }
            e
        })?;


let ts = TimeSpec::from(dur);
let res = unsafe { libc::nanosleep(ts.as_ref(), std::ptr::null_mut()) };
Expand Down
0