Visitar URL original
Fix crashes found hunting the last open fuzzing record by youknowone · Pull Request #8524 · RustPython/RustPython · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
f24691e
specialize: check the member descriptor's type before caching its slo…
youknowone Aug 14, 2026
40e67e1
socket: reserve recv()'s buffer fallibly
youknowone Aug 14, 2026
40a839b
types: count the __call__ and __get__ slot dispatches as recursion
youknowone Aug 14, 2026
bb744d6
typevar: show a ParamSpecArgs origin by its repr
youknowone Aug 14, 2026
7e4b96e
Do not hold a lock across a call back into Python
youknowone Aug 14, 2026
6e47646
Validate memoryview.cast() arguments and export negative strides corr…
youknowone Aug 14, 2026
2432fd7
Charge native recursion to the stack, not to the frame limit
youknowone Aug 14, 2026
02a6193
Report a size that cannot be allocated instead of aborting on it
youknowone Aug 14, 2026
ff52fe0
marshal: answer allow_code where a code object is written or read
youknowone Aug 14, 2026
d7cf971
Do not lock an object while running code that can reach it
youknowone Aug 14, 2026
61b989b
Stop asserting a pbkdf2 message that depends on the width of a C long
youknowone Aug 14, 2026
c9449a6
Publish and read the same pointer for a thread's top frame
youknowone Aug 15, 2026
ee782e0
Decide stop-the-world parking under the thread registry lock
youknowone Aug 15, 2026
fab2b3b
Keep an atexit callback alive while it is being compared
youknowone Aug 15, 2026
e3dcd8e
Hold atexit entries in PyRc rather than Arc
youknowone Aug 15, 2026
773ae23
Do not hold a buffer's storage while waiting for a peer
youknowone Aug 15, 2026
125d5aa
Do not hold a buffer's storage while waiting to hand it over
youknowone Aug 15, 2026
62ed618
Check select()'s descriptor limit while the sequence is walked
youknowone Aug 15, 2026
c65fe42
Bound native recursion where the C stack cannot be measured
youknowone Aug 15, 2026
00b64c3
Report the failure to allocate pbkdf2's key and Take.readinto's scratch
youknowone Aug 15, 2026
7c40cb9
Read the frozen-code tuple length as a signed length
youknowone Aug 15, 2026
ce43008
Make the snippets from the fuzzer sweep assert what they check
youknowone Aug 15, 2026
3d69690
Ask for a marshal container's room instead of assuming it
youknowone Aug 15, 2026
366ef3e
Compare the blocking-buffer snippet against something
youknowone Aug 15, 2026
ab0d4ff
Refuse array.frombytes() a source whose items are not bytes
youknowone Aug 15, 2026
7fad3b0
Tell apart the ways marshal data can be bad
youknowone Aug 15, 2026
a7f7021
Hash the collector's tables by address rather than by SipHash
youknowone Aug 16, 2026
691d6ca
Keep the collection's candidates and their counts in one table
youknowone Aug 16, 2026
3139b95
gc: collect referents into one buffer instead of a vector per object
youknowone Aug 16, 2026
dce42a3
memoryview and struct: match the checks and errors of the reference
youknowone Aug 16, 2026
5a5ba17
io: decide the readinto path by file type, not by seekability
youknowone Aug 16, 2026
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
Prev Previous commit
Next Next commit
Report a size that cannot be allocated instead of aborting on it
A size taken from Python went straight into an infallible allocation in
several places, so the process aborted through handle_alloc_error before
any exception could be raised:

- str/bytes/bytearray center(), ljust(), rjust() and zfill() reserved
  the padded result for the caller's width
- expandtabs() built its runs of spaces from a tabsize of any width;
  the argument is a C int, and a wider one does not fit
- Buffered{Reader,Writer,Random} allocated buffer_size, and read(),
  read1() and FileIO.read() their read size
- bytes(n) and bytearray(n) allocated n
- pbkdf2_hmac() allocated the derived key length, which is a C int

Each of these now reports MemoryError, or OverflowError where the
argument does not fit the type it is declared with.

new_zeroed_bytes() leaves the zeroing to the allocator, so a large
request costs the pages that are written to rather than all of them.

Assisted-by: Claude
  • Loading branch information
youknowone committed Aug 16, 2026
commit 02a6193099e1a87b84e8ae59d64a2937f48d9113
25 changes: 13 additions & 12 deletions crates/common/src/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,20 +592,21 @@ pub fn codepoint_range_end(s: &Wtf8, n_chars: usize) -> Option<usize> {
}

#[must_use]
pub fn zfill(bytes: &[u8], width: usize) -> Vec<u8> {
/// Returns `None` for a width whose result cannot be allocated.
pub fn zfill(bytes: &[u8], width: usize) -> Option<Vec<u8>> {
if width <= bytes.len() {
bytes.to_vec()
} else {
let (sign, s) = match bytes.first() {
Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]),
_ => (&b""[..], bytes),
};
let mut filled = Vec::new();
filled.extend_from_slice(sign);
filled.extend(core::iter::repeat_n(b'0', width - bytes.len()));
filled.extend_from_slice(s);
filled
return Some(bytes.to_vec());
}
let (sign, s) = match bytes.first() {
Some(_sign @ (b'+' | b'-')) => (unsafe { bytes.get_unchecked(..1) }, &bytes[1..]),
_ => (&b""[..], bytes),
};
let mut filled = Vec::new();
filled.try_reserve_exact(width).ok()?;
filled.extend_from_slice(sign);
filled.extend(core::iter::repeat_n(b'0', width - bytes.len()));
filled.extend_from_slice(s);
Some(filled)
}

/// Convert a string to ascii compatible, escaping unicode-s into escape
Expand Down
4 changes: 2 additions & 2 deletions crates/stdlib/src/hashlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,8 +847,8 @@ pub(crate) mod _hashlib {
if len < 1 {
return Err(vm.new_value_error("key length must be greater than 0."));
}
usize::try_from(len)
.map_err(|_| vm.new_overflow_error("key length is too great."))?
i32::try_from(len).map_err(|_| vm.new_overflow_error("key length is too great."))?
as usize
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
None => hash_digest_size(&name).ok_or_else(|| unsupported_hash(&name, vm))?,
};
Expand Down
27 changes: 17 additions & 10 deletions crates/vm/src/anystr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub struct SplitLinesArgs {
#[derive(FromArgs)]
pub struct ExpandTabsArgs {
#[pyarg(any, default = 8)]
tabsize: isize,
tabsize: i32,
}

impl ExpandTabsArgs {
Expand Down Expand Up @@ -132,6 +132,11 @@ where
{
fn new() -> Self;
fn with_capacity(capacity: usize) -> Self;
/// `with_capacity`, reporting a capacity that cannot be allocated instead
/// of aborting the process on it.
fn try_with_capacity(capacity: usize) -> Option<Self>
where
Self: Sized;
fn push_str(&mut self, s: &S);
}

Expand Down Expand Up @@ -285,27 +290,29 @@ pub(crate) trait AnyStr {
}
}

fn py_pad(&self, left: usize, right: usize, fillchar: Self::Char) -> Self::Container {
let mut u = Self::Container::with_capacity(
(left + right) * fillchar.bytes_len() + self.bytes_len(),
);
fn py_pad(&self, left: usize, right: usize, fillchar: Self::Char) -> Option<Self::Container> {
let capacity = left
.checked_add(right)?
.checked_mul(fillchar.bytes_len())?
.checked_add(self.bytes_len())?;
let mut u = Self::Container::try_with_capacity(capacity)?;
u.extend(core::iter::repeat_n(fillchar, left));
u.push_str(self);
u.extend(core::iter::repeat_n(fillchar, right));
u
Some(u)
}

fn py_center(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container {
fn py_center(&self, width: usize, fillchar: Self::Char, len: usize) -> Option<Self::Container> {
let marg = width - len;
let left = marg / 2 + (marg & width & 1);
self.py_pad(left, marg - left, fillchar)
}

fn py_ljust(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container {
fn py_ljust(&self, width: usize, fillchar: Self::Char, len: usize) -> Option<Self::Container> {
self.py_pad(0, width - len, fillchar)
}

fn py_rjust(&self, width: usize, fillchar: Self::Char, len: usize) -> Self::Container {
fn py_rjust(&self, width: usize, fillchar: Self::Char, len: usize) -> Option<Self::Container> {
self.py_pad(width - len, 0, fillchar)
}

Expand Down Expand Up @@ -402,7 +409,7 @@ pub(crate) trait AnyStr {
elements
}

fn py_zfill(&self, width: isize) -> Vec<u8> {
fn py_zfill(&self, width: isize) -> Option<Vec<u8>> {
let width = width.to_usize().unwrap_or(0);
let char_len = self.elements().count();
let width = self
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/builtins/bytearray.rs
Original file line number Diff line number Diff line change
Expand Up @@ -497,8 +497,8 @@ impl PyByteArray {
}

#[pymethod]
fn zfill(&self, width: isize) -> Self {
self.inner().zfill(width).into()
fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult<Self> {
Ok(self.inner().zfill(width, vm)?.into())
}

#[pymethod]
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/builtins/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,8 @@ impl PyBytes {
}

#[pymethod]
fn zfill(&self, width: isize) -> Self {
self.inner.zfill(width).into()
fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult<Self> {
Ok(self.inner.zfill(width, vm)?.into())
}

#[pymethod]
Expand Down
42 changes: 31 additions & 11 deletions crates/vm/src/builtins/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,31 +1320,33 @@ impl PyStr {
}

#[pymethod]
fn zfill(&self, width: isize) -> Wtf8Buf {
unsafe {
// SAFETY: this is safe-guaranteed because the original self.as_wtf8() is valid wtf8
Wtf8Buf::from_bytes_unchecked(self.as_wtf8().py_zfill(width))
}
fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
let filled = self
.as_wtf8()
.py_zfill(width)
.ok_or_else(|| vm.new_memory_error(""))?;
// SAFETY: this is safe-guaranteed because the original self.as_wtf8() is valid wtf8
Ok(unsafe { Wtf8Buf::from_bytes_unchecked(filled) })
}

#[inline]
fn _pad(
&self,
width: isize,
fillchar: OptionalArg<PyStrRef>,
pad: fn(&Wtf8, usize, CodePoint, usize) -> Wtf8Buf,
pad: fn(&Wtf8, usize, CodePoint, usize) -> Option<Wtf8Buf>,
vm: &VirtualMachine,
) -> PyResult<Wtf8Buf> {
let fillchar = fillchar.map_or(Ok(' '.into()), |ref s| {
s.as_wtf8().code_points().exactly_one().map_err(|_| {
vm.new_type_error("The fill character must be exactly one character long")
})
})?;
Ok(if self.len() as isize >= width {
self.as_wtf8().to_owned()
} else {
pad(self.as_wtf8(), width as usize, fillchar, self.len())
})
if self.len() as isize >= width {
return Ok(self.as_wtf8().to_owned());
}
pad(self.as_wtf8(), width as usize, fillchar, self.len())
.ok_or_else(|| vm.new_memory_error(""))
}

#[pymethod]
Expand Down Expand Up @@ -2214,6 +2216,12 @@ impl AnyStrContainer<str> for String {
Self::with_capacity(capacity)
}

fn try_with_capacity(capacity: usize) -> Option<Self> {
let mut s = Self::new();
s.try_reserve_exact(capacity).ok()?;
Some(s)
}

fn push_str(&mut self, other: &str) {
Self::push_str(self, other)
}
Expand Down Expand Up @@ -2327,6 +2335,12 @@ impl AnyStrContainer<Wtf8> for Wtf8Buf {
Self::with_capacity(capacity)
}

fn try_with_capacity(capacity: usize) -> Option<Self> {
let mut s = Self::new();
s.try_reserve_exact(capacity).ok()?;
Some(s)
}

fn push_str(&mut self, other: &Wtf8) {
self.push_wtf8(other)
}
Expand Down Expand Up @@ -2447,6 +2461,12 @@ impl AnyStrContainer<AsciiStr> for AsciiString {
Self::with_capacity(capacity)
}

fn try_with_capacity(capacity: usize) -> Option<Self> {
let mut v = Vec::new();
v.try_reserve_exact(capacity).ok()?;
Some(Self::from(v))
}

fn push_str(&mut self, other: &AsciiStr) {
Self::push_str(self, other)
}
Expand Down
29 changes: 20 additions & 9 deletions crates/vm/src/bytes_inner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl ByteInnerNewOptions {
} else {
size as usize
};
Ok(vec![0; size].into())
Ok(vm.new_zeroed_bytes(size)?.into())
}

fn handle_object_fallback(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyBytesInner> {
Expand Down Expand Up @@ -576,16 +576,15 @@ impl PyBytesInner {
fn _pad(
&self,
options: ByteInnerPaddingOptions,
pad: fn(&[u8], usize, u8, usize) -> Vec<u8>,
pad: PadFn,
vm: &VirtualMachine,
) -> PyResult<Vec<u8>> {
let (width, fillchar) = options.get_value("center", vm)?;
let len = self.len();
Ok(if len as isize >= width {
Vec::from(&self.elements[..])
} else {
pad(&self.elements, width as usize, fillchar, len)
})
if len as isize >= width {
return Ok(Vec::from(&self.elements[..]));
}
pad(&self.elements, width as usize, fillchar, len).ok_or_else(|| vm.new_memory_error(""))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

pub fn center(
Expand Down Expand Up @@ -821,8 +820,10 @@ impl PyBytesInner {
self.elements.py_bytes_splitlines(options, into_wrapper)
}

pub fn zfill(&self, width: isize) -> Vec<u8> {
self.elements.py_zfill(width)
pub fn zfill(&self, width: isize, vm: &VirtualMachine) -> PyResult<Vec<u8>> {
self.elements
.py_zfill(width)
.ok_or_else(|| vm.new_memory_error(""))
}

// len(self)>=1, from="", len(to)>=1, max_count>=1
Expand Down Expand Up @@ -1077,11 +1078,21 @@ impl AnyStrContainer<[u8]> for Vec<u8> {
Self::with_capacity(capacity)
}

fn try_with_capacity(capacity: usize) -> Option<Self> {
let mut v = Self::new();
v.try_reserve_exact(capacity).ok()?;
Some(v)
}

fn push_str(&mut self, other: &[u8]) {
self.extend(other)
}
}

/// A padding function from `AnyStr`, returning `None` for a width whose result
/// cannot be allocated.
type PadFn = fn(&[u8], usize, u8, usize) -> Option<Vec<u8>>;

const ASCII_WHITESPACES: [u8; 6] = [0x20, 0x09, 0x0a, 0x0c, 0x0d, 0x0b];

impl anystr::AnyChar for u8 {
Expand Down
8 changes: 4 additions & 4 deletions crates/vm/src/stdlib/_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1258,7 +1258,7 @@ mod _io {

let current_size = self.readahead() as usize;

let mut out = vec![0u8; n];
let mut out = vm.new_zeroed_bytes(n)?;
let mut remaining = n;
let mut written = 0;
if current_size > 0 {
Expand Down Expand Up @@ -1673,7 +1673,7 @@ mod _io {
check_writable(&raw, vm)?;
}

data.buffer = vec![0; buffer_size];
data.buffer = vm.new_zeroed_bytes(buffer_size)?;

if Self::READABLE {
data.reset_read();
Expand Down Expand Up @@ -1938,7 +1938,7 @@ mod _io {
if data.writable() {
data.flush_rewind(vm)?;
}
let mut v = vec![0; n];
let mut v = vm.new_zeroed_bytes(n)?;
data.reset_read();
let r = data
.raw_read(Either::A(Some(&mut v)), 0..n, vm)?
Expand Down Expand Up @@ -5767,7 +5767,7 @@ mod fileio {
}
let handle = zelf.get_fd(vm)?;
let bytes = if let Some(read_byte) = read_byte.to_usize() {
let mut bytes = vec![0; read_byte];
let mut bytes = vm.new_zeroed_bytes(read_byte)?;
// Loop on EINTR (PEP 475)
let n = loop {
match vm.allow_threads(|| host_io::read_once(handle, &mut bytes)) {
Expand Down
21 changes: 21 additions & 0 deletions crates/vm/src/vm/vm_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,27 @@ impl VirtualMachine {
}
}

/// `vec![0; len]` for a length that came from Python, where a request too
/// large to satisfy is a `MemoryError` rather than an aborted process.
///
/// The bytes are left for the allocator to zero, so a large request costs
/// no more than the pages that are actually written to.
pub fn new_zeroed_bytes(&self, len: usize) -> PyResult<Vec<u8>> {
if len == 0 {
return Ok(Vec::new());
}
let layout =
core::alloc::Layout::array::<u8>(len).map_err(|_| self.new_memory_error(""))?;
// SAFETY: `len` is not zero, so neither is the layout's size.
let ptr = unsafe { alloc::alloc::alloc_zeroed(layout) };
if ptr.is_null() {
return Err(self.new_memory_error(""));
}
// SAFETY: `ptr` was just allocated by the global allocator for exactly
// this many bytes, and every one of them is initialized to zero.
Ok(unsafe { Vec::from_raw_parts(ptr, len, len) })
}

/// Calling scheme used for binary operations:
///
/// Order operations are tried until either a valid result or error:
Expand Down
19 changes: 19 additions & 0 deletions extra_tests/snippets/builtin_bytes.py
Original file line number Diff line number Diff line change
Expand Up @@ -747,3 +747,22 @@ def __new__(cls, value):
assert "123A".istitle(), f"{s}"
assert not "123a".istitle(), f"{s}"
assert not "123A\ta".istitle(), f"{s}"


def test_huge_size():
# sizes that cannot be allocated are MemoryError, not an aborted process
for factory in (bytes, bytearray):
assert_raises(MemoryError, lambda factory=factory: factory(2**62))
for meth in ("center", "ljust", "rjust", "zfill"):
assert_raises(
MemoryError,
lambda factory=factory, meth=meth: getattr(factory(b"a"), meth)(
1 << 62
),
)
assert_raises(
OverflowError, lambda factory=factory: factory(b"\ta").expandtabs(2**31)
)


test_huge_size()
13 changes: 13 additions & 0 deletions extra_tests/snippets/builtin_str.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,3 +900,16 @@ class MyString(str):
assert id(b) != id(b * 1)
assert id(b) != id(1 * b)
assert id(b) != id(b * 2)


def test_huge_width():
# A width that cannot be allocated is a MemoryError, not an aborted
# process, and a tabsize wider than a C int does not fit at all.
for meth in ("center", "ljust", "rjust", "zfill"):
assert_raises(MemoryError, lambda meth=meth: getattr("a", meth)(1 << 62))
assert_raises(OverflowError, lambda: "\ta".expandtabs(1 << 62))
assert_raises(OverflowError, lambda: "\ta".expandtabs(2**31))
assert "\ta".expandtabs(2**31 - 1)[-1] == "a"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated


test_huge_width()
Loading