GeezerVison is showing github.com — Fix Windows runtime validation and 32-bit string size overflows · RustPython/RustPython@bd68b55
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: Lib/test/test_asyncio/test_sock_lowlevel.py -4Lines changed: 0 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -663,10 +663,6 @@ async def recvfrom_into(socket): | 663 | 663 | self._basetest_datagram_send_to_non_listening_address( | 664 | 664 | recvfrom_into)) | 665 | 665 |
| 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 | | - | 670 | 666 | else: | 671 | 667 | import selectors | 672 | 668 |
| |
Collapse file Copy file name to clipboardExpand all lines: Lib/test/test_os/test_os.py -1Lines changed: 0 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -3520,7 +3520,6 @@ def test_spawnve_noargs(self): | 3520 | 3520 | self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, ('',), {}) | 3521 | 3521 | self.assertRaises(ValueError, os.spawnve, os.P_NOWAIT, program, [''], {}) | 3522 | 3522 |
| 3523 | | - @unittest.expectedFailureIfWindows("TODO: RUSTPYTHON; AssertionError: \"must contain only strings\" does not match \"expected str, bytes or os.PathLike object, not int\"") | 3524 | 3523 | @requires_native_spawnv | 3525 | 3524 | def test_spawnv_arg_conversion_errors(self): | 3526 | 3525 | # A non-path argv item gets a TypeError naming the argument... | |
Collapse file Copy file name to clipboardExpand all lines: crates/common/src/str.rs +13-7Lines changed: 13 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -743,24 +743,31 @@ pub mod levenshtein { | 743 | 743 | /// Replace all tabs in a string with spaces, using the given tab size. | 744 | 744 | #[must_use] | 745 | 745 | 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) { | 746 | 754 | // A tab size of zero, which is also where a negative one lands, leaves no | 747 | 755 | // column for a tab to advance to: the tabs come out and nothing else moves. | 748 | 756 | // Going through the arithmetic anyway subtracts the current column from a | 749 | 757 | // 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; | 753 | 762 | } | 754 | 763 |
| 755 | | - let tab_stop = tab_size; | 756 | | - let mut expanded_str = Wtf8Buf::with_capacity(input.len()); | 757 | 764 | let mut tab_size = tab_stop; | 758 | 765 | let mut col_count = 0usize; | 759 | 766 | for ch in input.code_points() { | 760 | 767 | if ch == '\t' { | 761 | 768 | let num_spaces = tab_size - col_count; | 762 | 769 | col_count += num_spaces; | 763 | | - expanded_str.push_str(&" ".repeat(num_spaces)); | | 770 | + expanded_str.extend(core::iter::repeat_n(CodePoint::from(' '), num_spaces)); | 764 | 771 | } else { | 765 | 772 | expanded_str.push(ch); | 766 | 773 | if ch == '\r' || ch == '\n' { | @@ -774,7 +781,6 @@ pub fn expandtabs(input: &Wtf8, tab_size: usize) -> Wtf8Buf { | 774 | 781 | tab_size += tab_stop; | 775 | 782 | } | 776 | 783 | } | 777 | | - expanded_str | 778 | 784 | } | 779 | 785 |
| 780 | 786 | /// Creates an [`AsciiStr`][ascii::AsciiStr] from a string literal, throwing a compile error if the | |
Collapse file Copy file name to clipboardExpand all lines: crates/stdlib/src/overlapped.rs +40Lines changed: 40 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1048,6 +1048,10 @@ mod _overlapped { | 1048 | 1048 | return Err(vm.new_value_error("buffer too large")); | 1049 | 1049 | } | 1050 | 1050 |
| | 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 | + | 1051 | 1055 | let address: host_overlapped::SocketAddrV6 = unsafe { core::mem::zeroed() }; | 1052 | 1056 | let address_length = core::mem::size_of::<host_overlapped::SocketAddrV6>() as i32; | 1053 | 1057 |
| @@ -1293,4 +1297,40 @@ mod _overlapped { | 1293 | 1297 | host_winapi::reset_event(handle as host_winapi::Handle) | 1294 | 1298 | .map_err(|err| set_from_windows_err(err.raw_os_error().unwrap_or(0) as u32, vm)) | 1295 | 1299 | } | | 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 | + } | 1296 | 1336 | } |
Collapse file Copy file name to clipboardExpand all lines: crates/vm/src/anystr.rs +71Lines changed: 71 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -10,6 +10,45 @@ use crate::{ | 10 | 10 | function::{ArgIndex, PySsize}, | 11 | 11 | }; | 12 | 12 |
| | 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 | + | 13 | 52 | #[derive(FromArgs)] | 14 | 53 | pub struct SplitArgs<T: TryFromObject> { | 15 | 54 | #[pyarg(any, optional)] | @@ -515,3 +554,35 @@ where | 515 | 554 | })?; | 516 | 555 | (predicate)(single) | 517 | 556 | } | | 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 Copy file name to clipboardExpand all lines: crates/vm/src/builtins/bytearray.rs +6-2Lines changed: 6 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -999,8 +999,12 @@ impl Py<PyByteArray> { | 999 | 999 | } | 1000 | 1000 |
| 1001 | 1001 | #[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) | 1004 | 1008 | } | 1005 | 1009 |
| 1006 | 1010 | #[pymethod] | |
Collapse file Copy file name to clipboardExpand all lines: crates/vm/src/builtins/bytes.rs +6-2Lines changed: 6 additions & 2 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -541,8 +541,12 @@ impl Py<PyBytes> { | 541 | 541 | } | 542 | 542 |
| 543 | 543 | #[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) | 546 | 550 | } | 547 | 551 |
| 548 | 552 | #[pymethod] | |
Collapse file Copy file name to clipboardExpand all lines: crates/vm/src/builtins/str.rs +48-10Lines changed: 48 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -825,17 +825,29 @@ impl PyStr { | 825 | 825 | }) | 826 | 826 | } | 827 | 827 | 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"); | 829 | 830 | 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 | + } | 831 | 840 | } | 832 | 841 | if let [only] = items { | 833 | 842 | let only = item_str(0, only, vm)?; | 834 | 843 | if only.class().is(vm.ctx.types.str_type) { | 835 | 844 | return Ok(only.to_owned()); | 836 | 845 | } | 837 | 846 | } | 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(""))?; | 839 | 851 | for (i, obj) in items.iter().enumerate() { | 840 | 852 | if i > 0 { | 841 | 853 | joined.push_wtf8(sep); | @@ -1250,10 +1262,14 @@ impl Py<PyStr> { | 1250 | 1262 | } | 1251 | 1263 |
| 1252 | 1264 | #[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>> { | 1254 | 1270 | let ReplaceArgs { old, new, count } = args; | 1255 | 1271 | 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)); | 1257 | 1273 | } | 1258 | 1274 |
| 1259 | 1275 | let s = zelf.as_wtf8(); | @@ -1262,15 +1278,26 @@ impl Py<PyStr> { | 1262 | 1278 | } else { | 1263 | 1279 | count as usize | 1264 | 1280 | }; | | 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 | + }; | 1265 | 1292 | let replaced = if old.is_empty() { | 1266 | 1293 | s.replacen(old.as_wtf8(), new.as_wtf8(), limit) | 1267 | 1294 | } else { | 1268 | 1295 | let mut matches = s.find_iter(old.as_wtf8()).take(limit); | 1269 | 1296 | let Some(first) = matches.next() else { | 1270 | 1297 | drop(matches); | 1271 | | - return PyStr::result_unchanged(zelf, vm); | | 1298 | + return Ok(PyStr::result_unchanged(zelf, vm)); | 1272 | 1299 | }; | 1273 | | - let mut result = Vec::with_capacity(s.len()); | | 1300 | + let mut result = Vec::with_capacity(capacity); | 1274 | 1301 | let mut last = 0; | 1275 | 1302 | for start in core::iter::once(first).chain(matches) { | 1276 | 1303 | result.extend_from_slice(&s.as_bytes()[last..start]); | @@ -1281,7 +1308,7 @@ impl Py<PyStr> { | 1281 | 1308 | // SAFETY: A nonempty WTF-8 needle matches only at code point boundaries. | 1282 | 1309 | unsafe { Wtf8Buf::from_bytes_unchecked(result) } | 1283 | 1310 | }; | 1284 | | - vm.ctx.new_str(replaced) | | 1311 | + Ok(vm.ctx.new_str(replaced)) | 1285 | 1312 | } | 1286 | 1313 |
| 1287 | 1314 | #[pymethod] | @@ -1500,8 +1527,19 @@ impl Py<PyStr> { | 1500 | 1527 | } | 1501 | 1528 |
| 1502 | 1529 | #[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) | 1505 | 1543 | } | 1506 | 1544 |
| 1507 | 1545 | #[pymethod] | |
|