GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Fix Windows runtime validation and 32-bit string size overflows · RustPython/RustPython@bd68b55

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • Lib/test
    • test_asyncio
      • test_sock_lowlevel.py
    • test_os
      • test_os.py
  • crates
    • common/src
      • str.rs
    • stdlib/src
      • overlapped.rs
    • vm/src
      • anystr.rs
      • builtins
        • bytearray.rs
        • bytes.rs
        • str.rs
      • bytes_inner.rs
      • stdlib
        • nt.rs
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎Lib/test/test_asyncio/test_sock_lowlevel.py‎

Copy file name to clipboardExpand all lines: Lib/test/test_asyncio/test_sock_lowlevel.py
-4Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -663,10 +663,6 @@ async def recvfrom_into(socket):
663663
self._basetest_datagram_send_to_non_listening_address(
664664
recvfrom_into))
665665

666-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; AssertionError: ValueError not raised")
667-
def test_recvfrom_into_wrong_size(self):
668-
return super().test_recvfrom_into_wrong_size()
669-
670666
else:
671667
import selectors
672668

Collapse file

‎Lib/test/test_os/test_os.py‎

Copy file name to clipboardExpand all lines: Lib/test/test_os/test_os.py
-1Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3520,7 +3520,6 @@ def test_spawnve_noargs(self):
35203520
self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, ('',), {})
35213521
self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, [''], {})
35223522

3523-
@unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; AssertionError: \"must contain only strings\" does not match \"expected str, bytes or os.PathLike object, not int\"")
35243523
@requires_native_spawnv
35253524
def test_spawnv_arg_conversion_errors(self):
35263525
# A non-path argv item gets a TypeError naming the argument...
Collapse file

‎crates/common/src/str.rs‎

Copy file name to clipboardExpand all lines: crates/common/src/str.rs
+13-7Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -743,24 +743,31 @@ pub mod levenshtein {
743743
/// Replace all tabs in a string with spaces, using the given tab size.
744744
#[must_use]
745745
pub fn expandtabs(input: &Wtf8, tab_size: usize) -> Wtf8Buf {
746+
let capacity = if tab_size == 0 { 0 } else { input.len() };
747+
let mut expanded_str = Wtf8Buf::with_capacity(capacity);
748+
expandtabs_into(input, tab_size, &mut expanded_str);
749+
expanded_str
750+
}
751+
752+
/// Append tab-expanded input to a buffer whose capacity the caller may reserve.
753+
pub fn expandtabs_into(input: &Wtf8, tab_stop: usize, expanded_str: &mut Wtf8Buf) {
746754
// A tab size of zero, which is also where a negative one lands, leaves no
747755
// column for a tab to advance to: the tabs come out and nothing else moves.
748756
// Going through the arithmetic anyway subtracts the current column from a
749757
// tab stop of zero and underflows on the first tab, so the width asked for
750-
// next is `usize::MAX`. The bytes version of this already returns here.
751-
if tab_size == 0 {
752-
return input.code_points().filter(|ch| *ch != '\t').collect();
758+
// next is `usize::MAX`.
759+
if tab_stop == 0 {
760+
expanded_str.extend(input.code_points().filter(|ch| *ch != '\t'));
761+
return;
753762
}
754763

755-
let tab_stop = tab_size;
756-
let mut expanded_str = Wtf8Buf::with_capacity(input.len());
757764
let mut tab_size = tab_stop;
758765
let mut col_count = 0usize;
759766
for ch in input.code_points() {
760767
if ch == '\t' {
761768
let num_spaces = tab_size - col_count;
762769
col_count += num_spaces;
763-
expanded_str.push_str(&" ".repeat(num_spaces));
770+
expanded_str.extend(core::iter::repeat_n(CodePoint::from(' '), num_spaces));
764771
} else {
765772
expanded_str.push(ch);
766773
if ch == '\r' || ch == '\n' {
@@ -774,7 +781,6 @@ pub fn expandtabs(input: &Wtf8, tab_size: usize) -> Wtf8Buf {
774781
tab_size += tab_stop;
775782
}
776783
}
777-
expanded_str
778784
}
779785

780786
/// Creates an [`AsciiStr`][ascii::AsciiStr] from a string literal, throwing a compile error if the
Collapse file

‎crates/stdlib/src/overlapped.rs‎

Copy file name to clipboardExpand all lines: crates/stdlib/src/overlapped.rs
+40Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,6 +1048,10 @@ mod _overlapped {
10481048
return Err(vm.new_value_error("buffer too large"));
10491049
}
10501050

1051+
if size as usize > buf_len {
1052+
return Err(vm.new_value_error("nbytes is greater than the length of the buffer"));
1053+
}
1054+
10511055
let address: host_overlapped::SocketAddrV6 = unsafe { core::mem::zeroed() };
10521056
let address_length = core::mem::size_of::<host_overlapped::SocketAddrV6>() as i32;
10531057

@@ -1293,4 +1297,40 @@ mod _overlapped {
12931297
host_winapi::reset_event(handle as host_winapi::Handle)
12941298
.map_err(|err| set_from_windows_err(err.raw_os_error().unwrap_or(0) as u32, vm))
12951299
}
1300+
1301+
#[cfg(test)]
1302+
mod tests {
1303+
use super::*;
1304+
use crate::vm::{TryFromObject, builtins::PyByteArray};
1305+
1306+
#[test]
1307+
fn recvfrom_into_rejects_oversized_request_before_starting_io() {
1308+
let builder = crate::vm::Interpreter::builder(Default::default());
1309+
let defs = crate::stdlib_module_defs(&builder.ctx);
1310+
let interp = builder.add_native_modules(&defs).build();
1311+
interp.enter(|vm| {
1312+
let module = vm.import("_overlapped", 0).unwrap();
1313+
let overlapped = module
1314+
.get_attr("Overlapped", vm)
1315+
.unwrap()
1316+
.call((0,), vm)
1317+
.unwrap()
1318+
.downcast::<Overlapped>()
1319+
.unwrap();
1320+
let buffer = PyByteArray::from(vec![0; 8]).into_ref(&vm.ctx);
1321+
let buffer = ArgMemoryBuffer::try_from_object(vm, buffer.into()).unwrap();
1322+
let err = Overlapped::WSARecvFromInto(
1323+
&overlapped,
1324+
0,
1325+
buffer,
1326+
9,
1327+
OptionalArg::Missing,
1328+
vm,
1329+
)
1330+
.unwrap_err();
1331+
assert!(err.fast_isinstance(vm.ctx.exceptions.value_error));
1332+
assert!(matches!(overlapped.inner.lock().data, OverlappedData::None));
1333+
});
1334+
}
1335+
}
12961336
}
Collapse file

‎crates/vm/src/anystr.rs‎

Copy file name to clipboardExpand all lines: crates/vm/src/anystr.rs
+71Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,45 @@ use crate::{
1010
function::{ArgIndex, PySsize},
1111
};
1212

13+
pub(crate) fn checked_replace_len(
14+
original: usize,
15+
old: usize,
16+
new: usize,
17+
count: usize,
18+
) -> Option<usize> {
19+
let result = if new >= old {
20+
original.checked_add(count.checked_mul(new - old)?)?
21+
} else {
22+
original.checked_sub(count.checked_mul(old - new)?)?
23+
};
24+
isize::try_from(result).ok().map(|_| result)
25+
}
26+
27+
pub(crate) fn checked_expandtabs_len(
28+
original: usize,
29+
chars: impl Iterator<Item = u32>,
30+
tabsize: usize,
31+
) -> Option<usize> {
32+
let mut result = original;
33+
let mut column = 0;
34+
for ch in chars {
35+
if ch == u32::from(b'\t') {
36+
let spaces = if tabsize == 0 {
37+
0
38+
} else {
39+
tabsize - column % tabsize
40+
};
41+
result = checked_replace_len(result, 1, spaces, 1)?;
42+
column += spaces;
43+
} else if ch == u32::from(b'\r') || ch == u32::from(b'\n') {
44+
column = 0;
45+
} else {
46+
column += 1;
47+
}
48+
}
49+
Some(result)
50+
}
51+
1352
#[derive(FromArgs)]
1453
pub struct SplitArgs<T: TryFromObject> {
1554
#[pyarg(any, optional)]
@@ -515,3 +554,35 @@ where
515554
})?;
516555
(predicate)(single)
517556
}
557+
558+
#[cfg(test)]
559+
mod tests {
560+
use super::{checked_expandtabs_len, checked_replace_len};
561+
562+
#[test]
563+
fn replacement_length_checks_signed_and_unsigned_overflow() {
564+
let max = isize::MAX as usize;
565+
assert_eq!(checked_replace_len(4, 0, 1, 5), Some(9));
566+
assert_eq!(checked_replace_len(6, 1, 0, 4), Some(2));
567+
assert_eq!(checked_replace_len(max, 0, 1, 0), Some(max));
568+
assert_eq!(checked_replace_len(max, 0, 1, 1), None);
569+
assert_eq!(checked_replace_len(1, 0, 2, usize::MAX), None);
570+
}
571+
572+
#[test]
573+
fn expanded_tabs_check_the_complete_output_before_allocating() {
574+
let max = isize::MAX as usize;
575+
assert_eq!(
576+
checked_expandtabs_len(1, "\t".chars().map(u32::from), max),
577+
Some(max)
578+
);
579+
assert_eq!(
580+
checked_expandtabs_len(3, "\t\n\t".chars().map(u32::from), max),
581+
None
582+
);
583+
assert_eq!(
584+
checked_expandtabs_len(3, "a\tb".chars().map(u32::from), 0),
585+
Some(2)
586+
);
587+
}
588+
}
Collapse file

‎crates/vm/src/builtins/bytearray.rs‎

Copy file name to clipboardExpand all lines: crates/vm/src/builtins/bytearray.rs
+6-2Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -999,8 +999,12 @@ impl Py<PyByteArray> {
999999
}
10001000

10011001
#[pymethod]
1002-
fn expandtabs(&self, options: anystr::ExpandTabsArgs) -> PyByteArray {
1003-
self.inner().expandtabs(options).into()
1002+
fn expandtabs(
1003+
&self,
1004+
options: anystr::ExpandTabsArgs,
1005+
vm: &VirtualMachine,
1006+
) -> PyResult<PyByteArray> {
1007+
self.inner().expandtabs(options, vm).map(Into::into)
10041008
}
10051009

10061010
#[pymethod]
Collapse file

‎crates/vm/src/builtins/bytes.rs‎

Copy file name to clipboardExpand all lines: crates/vm/src/builtins/bytes.rs
+6-2Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,12 @@ impl Py<PyBytes> {
541541
}
542542

543543
#[pymethod]
544-
fn expandtabs(&self, options: anystr::ExpandTabsArgs) -> PyBytes {
545-
self.inner.expandtabs(options).into()
544+
fn expandtabs(
545+
&self,
546+
options: anystr::ExpandTabsArgs,
547+
vm: &VirtualMachine,
548+
) -> PyResult<PyBytes> {
549+
self.inner.expandtabs(options, vm).map(Into::into)
546550
}
547551

548552
#[pymethod]
Collapse file

‎crates/vm/src/builtins/str.rs‎

Copy file name to clipboardExpand all lines: crates/vm/src/builtins/str.rs
+48-10Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -825,17 +825,29 @@ impl PyStr {
825825
})
826826
}
827827
let sep = zelf.as_wtf8();
828-
let mut len = sep.len().saturating_mul(items.len().saturating_sub(1));
828+
let mut len = 0usize;
829+
let overflow = || vm.new_overflow_error("join() result is too long for a Python string");
829830
for (i, obj) in items.iter().enumerate() {
830-
len = len.saturating_add(item_str(i, obj, vm)?.as_wtf8().len());
831+
len = len
832+
.checked_add(item_str(i, obj, vm)?.as_wtf8().len())
833+
.ok_or_else(overflow)?;
834+
if i > 0 {
835+
len = len.checked_add(sep.len()).ok_or_else(overflow)?;
836+
}
837+
if isize::try_from(len).is_err() {
838+
return Err(overflow());
839+
}
831840
}
832841
if let [only] = items {
833842
let only = item_str(0, only, vm)?;
834843
if only.class().is(vm.ctx.types.str_type) {
835844
return Ok(only.to_owned());
836845
}
837846
}
838-
let mut joined = Wtf8Buf::with_capacity(len);
847+
let mut joined = Wtf8Buf::new();
848+
joined
849+
.try_reserve_exact(len)
850+
.map_err(|_| vm.new_memory_error(""))?;
839851
for (i, obj) in items.iter().enumerate() {
840852
if i > 0 {
841853
joined.push_wtf8(sep);
@@ -1250,10 +1262,14 @@ impl Py<PyStr> {
12501262
}
12511263

12521264
#[pymethod]
1253-
fn replace(zelf: PyRef<PyStr>, args: ReplaceArgs, vm: &VirtualMachine) -> PyRef<PyStr> {
1265+
fn replace(
1266+
zelf: PyRef<PyStr>,
1267+
args: ReplaceArgs,
1268+
vm: &VirtualMachine,
1269+
) -> PyResult<PyRef<PyStr>> {
12541270
let ReplaceArgs { old, new, count } = args;
12551271
if count == 0 || old.byte_len() > zelf.byte_len() || old.as_wtf8() == new.as_wtf8() {
1256-
return PyStr::result_unchanged(zelf, vm);
1272+
return Ok(PyStr::result_unchanged(zelf, vm));
12571273
}
12581274

12591275
let s = zelf.as_wtf8();
@@ -1262,15 +1278,26 @@ impl Py<PyStr> {
12621278
} else {
12631279
count as usize
12641280
};
1281+
let capacity = if new.byte_len() > old.byte_len() {
1282+
let matches = if old.is_empty() {
1283+
(zelf.char_len() + 1).min(limit)
1284+
} else {
1285+
s.find_iter(old.as_wtf8()).take(limit).count()
1286+
};
1287+
anystr::checked_replace_len(s.len(), old.byte_len(), new.byte_len(), matches)
1288+
.ok_or_else(|| vm.new_overflow_error("replace string is too long"))?
1289+
} else {
1290+
s.len()
1291+
};
12651292
let replaced = if old.is_empty() {
12661293
s.replacen(old.as_wtf8(), new.as_wtf8(), limit)
12671294
} else {
12681295
let mut matches = s.find_iter(old.as_wtf8()).take(limit);
12691296
let Some(first) = matches.next() else {
12701297
drop(matches);
1271-
return PyStr::result_unchanged(zelf, vm);
1298+
return Ok(PyStr::result_unchanged(zelf, vm));
12721299
};
1273-
let mut result = Vec::with_capacity(s.len());
1300+
let mut result = Vec::with_capacity(capacity);
12741301
let mut last = 0;
12751302
for start in core::iter::once(first).chain(matches) {
12761303
result.extend_from_slice(&s.as_bytes()[last..start]);
@@ -1281,7 +1308,7 @@ impl Py<PyStr> {
12811308
// SAFETY: A nonempty WTF-8 needle matches only at code point boundaries.
12821309
unsafe { Wtf8Buf::from_bytes_unchecked(result) }
12831310
};
1284-
vm.ctx.new_str(replaced)
1311+
Ok(vm.ctx.new_str(replaced))
12851312
}
12861313

12871314
#[pymethod]
@@ -1500,8 +1527,19 @@ impl Py<PyStr> {
15001527
}
15011528

15021529
#[pymethod]
1503-
fn expandtabs(&self, args: anystr::ExpandTabsArgs) -> Wtf8Buf {
1504-
rustpython_common::str::expandtabs(self.as_wtf8(), args.tabsize())
1530+
fn expandtabs(&self, args: anystr::ExpandTabsArgs, vm: &VirtualMachine) -> PyResult<Wtf8Buf> {
1531+
let capacity = anystr::checked_expandtabs_len(
1532+
self.byte_len(),
1533+
self.as_wtf8().code_points().map(|ch| ch.to_u32()),
1534+
args.tabsize(),
1535+
)
1536+
.ok_or_else(|| vm.new_overflow_error("new string is too long"))?;
1537+
let mut result = Wtf8Buf::new();
1538+
result
1539+
.try_reserve_exact(capacity)
1540+
.map_err(|_| vm.new_memory_error(""))?;
1541+
rustpython_common::str::expandtabs_into(self.as_wtf8(), args.tabsize(), &mut result);
1542+
Ok(result)
15051543
}
15061544

15071545
#[pymethod]

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.