Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 0 additions & 1 deletion Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -1057,7 +1057,6 @@ def test_repr(self) -> None:
with self.subTest(test_input=test):
self.assertEqual(repr(ast.parse(test)), snapshot)

@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: ValueError not raised
def test_repr_large_input_crash(self):
# gh-125010: Fix use-after-free in ast repr()
source = "0x0" + "e" * 10_000
Expand Down
8 changes: 5 additions & 3 deletions Lib/test/test_int.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
import time

import unittest
# TODO: RUSTPYTHON
Expand Down Expand Up @@ -573,7 +574,6 @@ def check(self, i, base=None):
else:
self.int_class(i, base)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_max_str_digits(self):
maxdigits = sys.get_int_max_str_digits()

Expand All @@ -588,7 +588,10 @@ def test_max_str_digits(self):
with self.assertRaises(ValueError):
str(i)

@unittest.expectedFailure # TODO: RUSTPYTHON
@unittest.expectedFailureIf(
not hasattr(time, "get_clock_info"),
"TODO: RUSTPYTHON; time.get_clock_info is not available on wasm",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

how CPython wasm build handle this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's verified via CPython source

the "time_get_clock_info" in Modules/timemodule.c (v3.14.0 lines 1630, 1946) has no cfg guards, so cpython exposes it unconditionally including wasm builds. The "expectedFailureIf" will surface as unexpected success once we lift RustPython's #[cfg(unix)]/#[cfg(windows)] gating in crates/vm/src/stdlib/time.rs

thanks for catching !

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for checking

)
def test_denial_of_service_prevented_int_to_str(self):
"""Regression test: ensure we fail before performing O(N**2) work."""
maxdigits = sys.get_int_max_str_digits()
Expand Down Expand Up @@ -713,7 +716,6 @@ def _other_base_helper(self, base):
with self.assertRaises(ValueError) as err:
int_class(f'{s}1', base)

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_int_from_other_bases(self):
base = 3
with self.subTest(base=base):
Expand Down
1 change: 0 additions & 1 deletion Lib/test/test_reprlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ def test_frozenset(self):
eq(r(frozenset({1, 2, 3, 4, 5, 6})), "frozenset({1, 2, 3, 4, 5, 6})")
eq(r(frozenset({1, 2, 3, 4, 5, 6, 7})), "frozenset({1, 2, 3, 4, 5, 6, ...})")

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_numbers(self):
for x in [123, 1.0 / 3]:
self.assertEqual(r(x), repr(x))
Expand Down
11 changes: 11 additions & 0 deletions crates/common/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,17 @@ impl FormatSpec {
matches!(self.format_type, Some(FormatType::Number(Case::Lower)))
}

/// Returns true if this format spec produces a decimal int representation
/// subject to `sys.get_int_max_str_digits()` (no spec, 'd', or 'n').
/// Binary bases ('b', 'o', 'x', 'X') are exempt per CPython. 'N' is rejected
/// later in `format_int` as `UnknownFormatCode`, so it is not included here.
pub fn is_decimal_int_format(&self) -> bool {
matches!(
self.format_type,
None | Some(FormatType::Decimal) | Some(FormatType::Number(Case::Lower))
)
}

/// Insert locale-aware thousands separators into an integer string.
/// Follows CPython's GroupGenerator logic for variable-width grouping.
fn insert_locale_grouping(int_part: &str, locale: &LocaleInfo) -> String {
Expand Down
30 changes: 29 additions & 1 deletion crates/vm/src/builtins/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,9 @@ impl PyInt {
}
let format_spec =
FormatSpec::parse(spec.as_str()).map_err(|err| err.into_pyexception(vm))?;
if format_spec.is_decimal_int_format() {
check_int_to_str_digits(&zelf.value, vm)?;
}
let result = if format_spec.has_locale_format() {
let locale = crate::format::get_locale_info();
format_spec.format_int_locale(&zelf.value, &locale)
Expand Down Expand Up @@ -655,9 +658,34 @@ impl Comparable for PyInt {
}
}

/// Pre-format check enforcing `sys.get_int_max_str_digits()` on int → str conversions.
/// Mirrors CPython's PEP 644 DoS mitigation. Cheap fast-path for small values via
/// bit-count upper bound on decimal digits.
pub(crate) fn check_int_to_str_digits(value: &BigInt, vm: &VirtualMachine) -> PyResult<()> {
let limit = vm.state.int_max_str_digits.load();
if limit == 0 {
return Ok(());
}
let bits = value.bits();
// Below ~452 decimal digits: definitely under any reasonable limit.
if bits < 1500 {
return Ok(());
}
// Upper bound on decimal digit count: ⌈bits × log10(2)⌉ + 1, with log10(2) ≈ 0.30103.
let digits_upper = (bits as usize * 30103 / 100000) + 1;
if digits_upper > limit {
return Err(vm.new_value_error(format!(
"Exceeds the limit ({limit} digits) for integer string conversion; \
use sys.set_int_max_str_digits() to increase the limit"
)));
}
Ok(())
}

impl Representable for PyInt {
#[inline]
fn repr_str(zelf: &Py<Self>, _vm: &VirtualMachine) -> PyResult<String> {
fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
check_int_to_str_digits(&zelf.value, vm)?;
Ok(zelf.to_str_radix_10())
}
}
Expand Down
19 changes: 12 additions & 7 deletions crates/vm/src/cformat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ use crate::{
AsObject, PyObject, PyObjectRef, PyResult, TryFromBorrowedObject, TryFromObject,
VirtualMachine,
builtins::{
PyBaseExceptionRef, PyByteArray, PyBytes, PyFloat, PyInt, PyStr, try_f64_to_bigint, tuple,
PyBaseExceptionRef, PyByteArray, PyBytes, PyFloat, PyInt, PyStr,
int::check_int_to_str_digits, try_f64_to_bigint, tuple,
},
function::ArgIntoFloat,
protocol::PyBuffer,
Expand Down Expand Up @@ -54,17 +55,19 @@ fn spec_format_bytes(
CNumberType::DecimalD | CNumberType::DecimalI | CNumberType::DecimalU => {
match_class!(match &obj {
ref i @ PyInt => {
check_int_to_str_digits(i.as_bigint(), vm)?;
Ok(spec.format_number(i.as_bigint()).into_bytes())
}
ref f @ PyFloat => {
Ok(spec
.format_number(&try_f64_to_bigint(f.to_f64(), vm)?)
.into_bytes())
let bigint = try_f64_to_bigint(f.to_f64(), vm)?;
check_int_to_str_digits(&bigint, vm)?;
Ok(spec.format_number(&bigint).into_bytes())
}
obj => {
if let Some(method) = vm.get_method(obj.clone(), identifier!(vm, __int__)) {
let result = method?.call((), vm)?;
if let Some(i) = result.downcast_ref::<PyInt>() {
check_int_to_str_digits(i.as_bigint(), vm)?;
return Ok(spec.format_number(i.as_bigint()).into_bytes());
}
}
Expand Down Expand Up @@ -149,17 +152,19 @@ fn spec_format_string(
CNumberType::DecimalD | CNumberType::DecimalI | CNumberType::DecimalU => {
match_class!(match &obj {
ref i @ PyInt => {
check_int_to_str_digits(i.as_bigint(), vm)?;
Ok(spec.format_number(i.as_bigint()).into())
}
ref f @ PyFloat => {
Ok(spec
.format_number(&try_f64_to_bigint(f.to_f64(), vm)?)
.into())
let bigint = try_f64_to_bigint(f.to_f64(), vm)?;
check_int_to_str_digits(&bigint, vm)?;
Ok(spec.format_number(&bigint).into())
}
obj => {
if let Some(method) = vm.get_method(obj.clone(), identifier!(vm, __int__)) {
let result = method?.call((), vm)?;
if let Some(i) = result.downcast_ref::<PyInt>() {
check_int_to_str_digits(i.as_bigint(), vm)?;
return Ok(spec.format_number(i.as_bigint()).into());
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/protocol/object.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine,
builtins::{
PyBytes, PyDict, PyDictRef, PyGenericAlias, PyInt, PyList, PyStr, PyTuple, PyTupleRef,
PyType, PyTypeRef, PyUtf8Str, pystr::AsPyStr,
PyType, PyTypeRef, PyUtf8Str, int::check_int_to_str_digits, pystr::AsPyStr,
},
common::{hash::PyHash, str::to_ascii},
convert::{ToPyObject, ToPyResult},
Expand Down Expand Up @@ -392,6 +392,7 @@ impl PyObject {
// Fast path for exact int: skip __str__ method resolution
let obj = match obj.downcast_exact::<PyInt>(vm) {
Ok(int) => {
check_int_to_str_digits(int.as_bigint(), vm)?;
return Ok(vm.ctx.new_str(int.to_str_radix_10()));
}
Err(obj) => obj,
Expand Down
35 changes: 35 additions & 0 deletions extra_tests/snippets/builtin_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,3 +366,38 @@ class SubInt(int):
subint = int.__new__(SubInt, 11)
assert subint.real is not subint
assert type(subint.real) is int


# sys.set_int_max_str_digits enforced on int → str conversions (PEP 644).
# Decimal paths (str, repr, f-string, %d, format(d/n/empty)) raise ValueError;
# binary bases ('x', 'o', 'b') are exempt.
import sys

_orig_limit = sys.get_int_max_str_digits()
try:
sys.set_int_max_str_digits(4000)
huge = 10**4001 # 4002 decimal digits, well over the limit

for fn in [
lambda: str(huge),
lambda: repr(huge),
lambda: f"{huge}",
lambda: "%d" % huge,
lambda: b"%d" % huge,
lambda: format(huge, ""),
lambda: format(huge, "d"),
lambda: format(huge, ",d"),
]:
with assert_raises(ValueError):
fn()

# Binary bases must NOT raise.
assert format(huge, "x")
assert format(huge, "o")
assert format(huge, "b")

# Limit disabled: no check.
sys.set_int_max_str_digits(0)
assert str(huge)
finally:
sys.set_int_max_str_digits(_orig_limit)