diff --git a/Cargo.lock b/Cargo.lock index 463a7298460..4f113adf091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3431,6 +3431,7 @@ dependencies = [ "libffi", "libloading 0.9.0", "mac_address", + "memchr", "memmap2 0.9.11", "nix 0.31.3", "num-traits", diff --git a/crates/host_env/Cargo.toml b/crates/host_env/Cargo.toml index 07f7d0da60c..128afc59285 100644 --- a/crates/host_env/Cargo.toml +++ b/crates/host_env/Cargo.toml @@ -51,6 +51,7 @@ libffi = { workspace = true, features = ["system"] } system-configuration = { workspace = true } [target.'cfg(windows)'.dependencies] +memchr.workspace = true junction = { workspace = true } schannel = { workspace = true } widestring = { workspace = true } diff --git a/crates/host_env/src/multiprocessing.rs b/crates/host_env/src/multiprocessing.rs index 0030245dbb4..067a8630777 100644 --- a/crates/host_env/src/multiprocessing.rs +++ b/crates/host_env/src/multiprocessing.rs @@ -32,12 +32,13 @@ pub enum SemError { AlreadyExists, NotFound, InvalidInput, + InteriorNul, Other(i32), } #[cfg(unix)] impl SemError { - fn from_errno(err: Errno) -> Self { + const fn from_errno(err: Errno) -> Self { match err { Errno::EAGAIN => Self::WouldBlock, Errno::ETIMEDOUT => Self::TimedOut, @@ -49,14 +50,14 @@ impl SemError { } } - pub fn raw_os_error(self) -> i32 { + pub const fn raw_os_error(self) -> i32 { match self { Self::WouldBlock => Errno::EAGAIN as i32, Self::TimedOut => Errno::ETIMEDOUT as i32, Self::Interrupted => Errno::EINTR as i32, Self::AlreadyExists => Errno::EEXIST as i32, Self::NotFound => Errno::ENOENT as i32, - Self::InvalidInput => Errno::EINVAL as i32, + Self::InvalidInput | Self::InteriorNul => Errno::EINVAL as i32, Self::Other(code) => code, } } @@ -119,7 +120,7 @@ impl SemHandle { value: u32, unlink: bool, ) -> Result<(Self, Option), SemError> { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let raw = unsafe { libc::sem_open(cname.as_ptr(), libc::O_CREAT | libc::O_EXCL, 0o600, value) }; if raw == libc::SEM_FAILED { @@ -141,7 +142,7 @@ impl SemHandle { } pub fn open_existing(name: &str) -> Result { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let raw = unsafe { libc::sem_open(cname.as_ptr(), 0) }; if raw == libc::SEM_FAILED { Err(SemError::from_errno(Errno::last())) @@ -305,18 +306,18 @@ pub fn is_too_many_posts(err: u32) -> bool { } #[cfg(unix)] -pub fn semaphore_name(name: &str) -> Result { - let mut full = String::with_capacity(name.len() + 1); +pub fn semaphore_name(name: &str) -> Result { + let mut full = String::with_capacity(name.len() + 2); if !name.starts_with('/') { full.push('/'); } full.push_str(name); - CString::new(full) + CString::new(full).map_err(|_| SemError::InteriorNul) } #[cfg(unix)] pub fn sem_unlink(name: &str) -> Result<(), SemError> { - let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?; + let cname = semaphore_name(name)?; let res = unsafe { libc::sem_unlink(cname.as_ptr()) }; if res < 0 { Err(SemError::from_errno(Errno::last())) diff --git a/crates/host_env/src/time.rs b/crates/host_env/src/time.rs index dd97a936681..451e4884098 100644 --- a/crates/host_env/src/time.rs +++ b/crates/host_env/src/time.rs @@ -638,10 +638,9 @@ unsafe extern "C" { #[cfg(windows)] pub fn strftime_ascii(fmt: &str, tm: &libc::tm) -> Result { - if fmt.contains('\0') { - return Err(CheckedTmError::EmbeddedNul); - } - let fmt_wide: Vec = fmt.encode_utf16().chain(core::iter::once(0)).collect(); + let fmt_wide = widestring::WideCString::from_str(fmt) + .map_err(|_| CheckedTmError::EmbeddedNul)? + .into_vec_with_nul(); let mut size = 1024usize; let max_scale = 256usize.saturating_mul(fmt.len().max(1)); loop { diff --git a/crates/host_env/src/winapi.rs b/crates/host_env/src/winapi.rs index 4e4536c3518..af53910089e 100644 --- a/crates/host_env/src/winapi.rs +++ b/crates/host_env/src/winapi.rs @@ -3,22 +3,20 @@ reason = "This module mirrors Win32 APIs with raw handle and pointer parameters." )] +use core::hint::cold_path; use std::{io, path::Path}; -use windows_sys::Win32::{ - Foundation::{HANDLE, HMODULE, WAIT_FAILED}, - System::Threading::PROCESS_INFORMATION, -}; use crate::windows::{CheckWin32Bool, CheckWin32Handle}; +use memchr::memchr; pub use windows_sys::Win32::{ Foundation::{ DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NETNAME_DELETED, ERROR_NO_DATA, ERROR_NO_SYSTEM_RESOURCES, ERROR_NOT_FOUND, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_CONNECTED, ERROR_PORT_UNREACHABLE, ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT, - ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, STILL_ACTIVE, WAIT_ABANDONED_0, WAIT_OBJECT_0, - WAIT_TIMEOUT, + ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, HANDLE, HMODULE, STILL_ACTIVE, + WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT, }, Globalization::{ LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, LCMAP_LINGUISTIC_CASING, @@ -57,12 +55,12 @@ pub use windows_sys::Win32::{ ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_BREAKAWAY_FROM_JOB, CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW, DETACHED_PROCESS, HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS, - NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, REALTIME_PRIORITY_CLASS, - STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, STARTF_PREVENTPINNING, - STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID, STARTF_TITLEISLINKNAME, - STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS, STARTF_USEFILLATTRIBUTE, - STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW, STARTF_USESIZE, - STARTF_USESTDHANDLES, + NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, PROCESS_INFORMATION, + REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, + STARTF_PREVENTPINNING, STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID, + STARTF_TITLEISLINKNAME, STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS, + STARTF_USEFILLATTRIBUTE, STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW, + STARTF_USESIZE, STARTF_USESTDHANDLES, }, }, UI::WindowsAndMessaging::SW_HIDE, @@ -312,7 +310,8 @@ pub fn build_environment_block( let mut last_entry: HashMap> = HashMap::new(); for (key, value) in entries { - if key.contains('\0') || value.contains('\0') { + if memchr(b'\0', key.as_bytes()).is_some() || memchr(b'\0', value.as_bytes()).is_some() { + cold_path(); return Err(BuildEnvironmentBlockError::ContainsNul); } if key.is_empty() || key[1..].contains('=') { diff --git a/crates/stdlib/src/grp.rs b/crates/stdlib/src/grp.rs index 7e3dd8ef378..a237bd71043 100644 --- a/crates/stdlib/src/grp.rs +++ b/crates/stdlib/src/grp.rs @@ -10,6 +10,7 @@ mod grp { exceptions, types::PyStructSequence, }; + use core::hint::cold_path; use rustpython_host_env::grp as host_grp; #[pystruct_sequence_data] @@ -61,10 +62,11 @@ mod grp { #[pyfunction] fn getgrnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - let gr_name = name.as_str(); - if gr_name.contains('\0') { + if name.as_pystr().contains_nuls() { + cold_path(); return Err(exceptions::nul_char_error(vm)); } + let gr_name = name.as_str(); let group = host_grp::getgrnam(gr_name).map_err(|err| err.into_pyexception(vm))?; let group = group.ok_or_else(|| { vm.new_key_error( diff --git a/crates/stdlib/src/mmap.rs b/crates/stdlib/src/mmap.rs index dac24b053c2..2a07cf7d86a 100644 --- a/crates/stdlib/src/mmap.rs +++ b/crates/stdlib/src/mmap.rs @@ -24,9 +24,9 @@ mod mmap { use core::ops::{Deref, DerefMut}; use crossbeam_utils::atomic::AtomicCell; use num_traits::Signed; - #[cfg(windows)] - use std::io; use std::io::Write; + #[cfg(windows)] + use {core::hint::cold_path, memchr::memchr, rustpython_vm::exceptions, std::io}; #[cfg(unix)] use rustpython_host_env::crt_fd; @@ -460,8 +460,9 @@ mod mmap { let s = obj .try_to_value::(vm) .map_err(|_| vm.new_type_error("tagname must be a string or None"))?; - if s.contains('\0') { - return Err(vm.new_value_error("tagname must not contain null characters")); + if memchr(b'\0', s.as_bytes()).is_some() { + cold_path(); + return Err(exceptions::nul_char_error(vm)); } Some(s) } diff --git a/crates/stdlib/src/multiprocessing.rs b/crates/stdlib/src/multiprocessing.rs index 9883219dfb1..c79af002b25 100644 --- a/crates/stdlib/src/multiprocessing.rs +++ b/crates/stdlib/src/multiprocessing.rs @@ -811,7 +811,7 @@ mod _multiprocessing { let value = args.value as u32; let (handle, name) = SemHandle::create(&args.name, value, args.unlink).map_err(|err| { - if err == SemError::InvalidInput && args.name.contains('\0') { + if err == SemError::InteriorNul { exceptions::nul_char_error(vm) } else { os_error(vm, err) @@ -835,7 +835,7 @@ mod _multiprocessing { #[pyfunction] fn sem_unlink(name: String, vm: &VirtualMachine) -> PyResult<()> { host_multiprocessing::sem_unlink(&name).map_err(|err| { - if err == SemError::InvalidInput && name.contains('\0') { + if err == SemError::InteriorNul { exceptions::nul_char_error(vm) } else { os_error(vm, err) diff --git a/crates/stdlib/src/openssl.rs b/crates/stdlib/src/openssl.rs index 8b7d1de0639..16ef8fdb19c 100644 --- a/crates/stdlib/src/openssl.rs +++ b/crates/stdlib/src/openssl.rs @@ -53,6 +53,8 @@ fn probe() -> &'static ProbeResult { #[cfg(ossl111)] ossl111, #[cfg(windows)] windows))] mod _ssl { + use core::hint::cold_path; + use super::{bio, probe}; // Import error types and helpers used in this module (others are exposed via pymodule(with(...))) @@ -85,6 +87,7 @@ mod _ssl { }; use crossbeam_utils::atomic::AtomicCell; use foreign_types_shared::{ForeignType, ForeignTypeRef}; + use memchr::memchr; use openssl::{ asn1::{Asn1Object, Asn1ObjectRef}, error::ErrorStack, @@ -1039,12 +1042,13 @@ mod _ssl { #[pymethod] fn set_ciphers(&self, cipherlist: PyStrRef, vm: &VirtualMachine) -> PyResult<()> { - let ciphers: &str = cipherlist.as_ref(); - if ciphers.contains('\0') { + if cipherlist.contains_nuls() { + cold_path(); return Err(exceptions::nul_char_error(vm)); } + self.builder() - .set_cipher_list(ciphers) + .set_cipher_list(cipherlist.as_ref()) .map_err(|_| new_ssl_error(vm, "No cipher can be selected.")) } @@ -1096,9 +1100,6 @@ mod _ssl { let name_cstr = match name { Either::A(s) => { let s: &str = s.as_ref(); - if s.contains('\0') { - return Err(exceptions::nul_char_error(vm)); - } s.to_cstring(vm)? } Either::B(b) => std::ffi::CString::new(b.borrow_buf().to_vec()) @@ -2031,15 +2032,16 @@ mod _ssl { // Configure server hostname if let Some(hostname) = &server_hostname { + if hostname.contains_nuls() { + cold_path(); + return Err(exceptions::nul_char_type_error(vm)); + } let hostname_str: &str = hostname.as_ref(); if hostname_str.is_empty() || hostname_str.starts_with('.') { return Err(vm.new_value_error( "server_hostname cannot be an empty string or start with a leading dot.", )); } - if hostname_str.contains('\0') { - return Err(exceptions::nul_char_type_error(vm)); - } let ip = hostname_str.parse::(); if ip.is_err() { ssl.set_hostname(hostname_str) diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 5e6f35943bd..7e2b4c124d2 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -67,9 +67,11 @@ mod _ssl { use alloc::sync::Arc; use core::{ hash::{Hash, Hasher}, + hint::cold_path, sync::atomic::{AtomicUsize, Ordering}, time::Duration, }; + use memchr::memchr; use rustpython_vm::exceptions; use std::{ collections::{HashMap, hash_map::DefaultHasher}, @@ -392,7 +394,8 @@ mod _ssl { // IP addresses are allowed as server_hostname // SNI will not be sent for IP addresses - if hostname.contains('\0') { + if memchr(b'\0', hostname.as_bytes()).is_some() { + cold_path(); return Err(exceptions::nul_char_type_error(vm)); } @@ -1854,25 +1857,7 @@ mod _ssl { let hostname = match args.server_hostname.into_option().flatten() { Some(hostname_str) => { let hostname = hostname_str.as_str(); - - // Validate hostname - if hostname.is_empty() { - return Err(vm.new_value_error("server_hostname cannot be an empty string")); - } - - // Check if it starts with a dot - if hostname.starts_with('.') { - return Err(vm.new_value_error("server_hostname cannot start with a dot")); - } - - // IP addresses are allowed - // SNI will not be sent for IP addresses - - // Check for NULL bytes - if hostname.contains('\0') { - return Err(exceptions::nul_char_error(vm)); - } - + validate_hostname(hostname, vm)?; Some(hostname.to_string()) } None => None, diff --git a/crates/vm/src/builtins/bytes.rs b/crates/vm/src/builtins/bytes.rs index ca40b3d3b6b..d4c30a7e94d 100644 --- a/crates/vm/src/builtins/bytes.rs +++ b/crates/vm/src/builtins/bytes.rs @@ -31,6 +31,7 @@ use crate::{ }; use bstr::ByteSlice; use core::{mem::size_of, ops::Deref}; +use memchr::memchr; #[pyclass(module = false, name = "bytes")] #[derive(Clone, Debug)] @@ -169,6 +170,13 @@ impl PyBytes { .map(|x| vm.ctx.new_bytes(x).into()), } } + + /// Check bytes for interior NULs. + #[inline] + #[must_use] + pub fn contains_nuls(&self) -> bool { + memchr(b'\0', self.as_bytes()).is_some() + } } impl PyRef { @@ -218,7 +226,7 @@ impl PyBytes { #[inline] #[must_use] - pub fn as_bytes(&self) -> &[u8] { + pub const fn as_bytes(&self) -> &[u8] { self.inner.as_bytes() } diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index b1c2c41973b..07325159a39 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -37,6 +37,7 @@ use bstr::ByteSlice; use core::ffi::CStr; use core::{char, mem, ops::Range}; use itertools::Itertools; +use memchr::memchr; use num_traits::ToPrimitive; use rustpython_common::{ ascii, @@ -545,6 +546,13 @@ impl PyStr { } } + /// Check string bytes for interior NULs. + #[inline] + #[must_use] + pub fn contains_nuls(&self) -> bool { + memchr(b'\0', self.as_bytes()).is_some() + } + pub fn to_string_lossy(&self) -> Cow<'_, str> { self.to_str() .map_or_else(|| self.as_wtf8().to_string_lossy(), Cow::Borrowed) @@ -2150,7 +2158,7 @@ impl PyUtf8Str { impl Py { /// Upcast to PyStr. - pub fn as_pystr(&self) -> &Py { + pub const fn as_pystr(&self) -> &Py { unsafe { // Safety: PyUtf8Str is a wrapper around PyStr, so this cast is safe. &*(self as *const Self as *const Py) diff --git a/crates/vm/src/bytes_inner.rs b/crates/vm/src/bytes_inner.rs index 0bf6c13726a..d144004d66f 100644 --- a/crates/vm/src/bytes_inner.rs +++ b/crates/vm/src/bytes_inner.rs @@ -255,8 +255,8 @@ fn write_bytearray_repr_char(ch: u8, buf: &mut String) { impl PyBytesInner { #[inline] - pub fn as_bytes(&self) -> &[u8] { - &self.elements + pub const fn as_bytes(&self) -> &[u8] { + self.elements.as_slice() } fn new_repr_overflow_error(vm: &VirtualMachine) -> PyBaseExceptionRef { diff --git a/crates/vm/src/function/fspath.rs b/crates/vm/src/function/fspath.rs index cd3cd2276f7..50feef86dd0 100644 --- a/crates/vm/src/function/fspath.rs +++ b/crates/vm/src/function/fspath.rs @@ -6,6 +6,7 @@ use crate::{ protocol::PyBuffer, }; use alloc::borrow::Cow; +use core::hint::cold_path; use std::{ffi::OsStr, path::PathBuf}; /// Helper to implement os.fspath() @@ -36,21 +37,20 @@ impl FsPath { msg: &'static str, vm: &VirtualMachine, ) -> PyResult { - let check_nul = |b: &[u8]| { - if !check_for_nul || memchr::memchr(b'\0', b).is_none() { - Ok(()) - } else { - Err(crate::exceptions::nul_char_error(vm)) - } - }; let match1 = |obj: PyObjectRef| { let pathlike = match_class!(match obj { s @ PyStr => { - check_nul(s.as_bytes())?; + if check_for_nul && s.contains_nuls() { + cold_path(); + return Err(crate::exceptions::nul_char_error(vm)); + } Self::Str(s) } b @ PyBytes => { - check_nul(&b)?; + if check_for_nul && b.contains_nuls() { + cold_path(); + return Err(crate::exceptions::nul_char_error(vm)); + } Self::Bytes(b) } obj => return Ok(Err(obj)), diff --git a/crates/vm/src/ospath.rs b/crates/vm/src/ospath.rs index f2368a28826..05f7b061159 100644 --- a/crates/vm/src/ospath.rs +++ b/crates/vm/src/ospath.rs @@ -7,6 +7,7 @@ use crate::{ convert::{IntoPyException, ToPyException, ToPyObject, TryFromObject}, function::FsPath, }; +use core::hint::cold_path; use std::path::{Path, PathBuf}; /// path_converter @@ -149,6 +150,7 @@ impl PathConverter { if self.non_strict || memchr::memchr(b'\0', b).is_none() { Ok(()) } else { + cold_path(); Err(vm.new_value_error(format!( "{}embedded null character in {}", self.error_prefix(), diff --git a/crates/vm/src/stdlib/_codecs.rs b/crates/vm/src/stdlib/_codecs.rs index 6052350159d..69d9e0e4fde 100644 --- a/crates/vm/src/stdlib/_codecs.rs +++ b/crates/vm/src/stdlib/_codecs.rs @@ -6,6 +6,8 @@ use crate::common::static_cell::StaticCell; #[pymodule(with(#[cfg(windows)] _codecs_windows))] mod _codecs { + use core::hint::cold_path; + use crate::codecs::{ErrorsHandler, PyDecodeContext, PyEncodeContext}; use crate::common::encodings; use crate::common::wtf8::Wtf8Buf; @@ -29,7 +31,8 @@ mod _codecs { #[pyfunction] fn lookup(encoding: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if encoding.as_str().contains('\0') { + if encoding.as_pystr().contains_nuls() { + cold_path(); return Err(nul_char_error(vm)); } vm.state @@ -105,7 +108,8 @@ mod _codecs { #[pyfunction] fn lookup_error(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if name.as_str().contains('\0') { + if name.as_pystr().contains_nuls() { + cold_path(); return Err(nul_char_error(vm)); } vm.state.codec_registry.lookup_error(name.as_str(), vm) @@ -113,7 +117,8 @@ mod _codecs { #[pyfunction] fn _unregister_error(errors: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - if errors.as_str().contains('\0') { + if errors.as_pystr().contains_nuls() { + cold_path(); return Err(nul_char_error(vm)); } vm.state diff --git a/crates/vm/src/stdlib/_io.rs b/crates/vm/src/stdlib/_io.rs index 854a46d8cd6..ce41a942891 100644 --- a/crates/vm/src/stdlib/_io.rs +++ b/crates/vm/src/stdlib/_io.rs @@ -150,6 +150,7 @@ mod _io { use alloc::borrow::Cow; use bstr::ByteSlice; use core::{ + hint::cold_path, ops::Range, sync::atomic::{AtomicBool, Ordering}, }; @@ -2854,7 +2855,8 @@ mod _io { } fn validate_errors(errors: &PyRef, vm: &VirtualMachine) -> PyResult<()> { - if errors.as_str().contains('\0') { + if errors.as_pystr().contains_nuls() { + cold_path(); return Err(nul_char_error(vm)); } vm.state @@ -2894,12 +2896,7 @@ mod _io { } Err(err) => return Err(err), }, - Some(enc) => { - if enc.as_str().contains('\0') { - return Err(nul_char_error(vm)); - } - enc - } + Some(enc) => enc, _ => match vm.import("locale", 0) { Ok(locale) => locale .get_attr("getencoding", vm)? @@ -2914,7 +2911,8 @@ mod _io { Err(err) => return Err(err), }, }; - if encoding.as_str().contains('\0') { + if encoding.as_pystr().contains_nuls() { + cold_path(); return Err(nul_char_error(vm)); } Ok(encoding) @@ -3067,7 +3065,8 @@ mod _io { let mut write_through = None; if let Some(enc) = args.encoding { - if enc.as_str().contains('\0') && enc.as_str().starts_with("locale") { + if enc.as_pystr().contains_nuls() && enc.as_str().starts_with("locale") { + cold_path(); return Err(vm.new_lookup_error(format!("unknown encoding: {enc}"))); } let resolved = Self::resolve_encoding(Some(enc), vm)?; diff --git a/crates/vm/src/stdlib/nt.rs b/crates/vm/src/stdlib/nt.rs index acc9f4ee67e..31a08195c58 100644 --- a/crates/vm/src/stdlib/nt.rs +++ b/crates/vm/src/stdlib/nt.rs @@ -15,6 +15,7 @@ pub(crate) mod module { ospath::{OsPath, OsPathOrFd}, stdlib::os::{_os, DirFd, SupportFunc, TargetIsDirectory}, }; + use core::hint::cold_path; use libc::intptr_t; use rustpython_common::wtf8::Wtf8Buf; use rustpython_host_env::nt as host_nt; @@ -551,7 +552,8 @@ pub(crate) mod module { let value_str = value.expect_str(); // Validate: no null characters in key or value - if key_str.contains('\0') || value_str.contains('\0') { + if key.contains_nuls() || value.contains_nuls() { + cold_path(); return Err(exceptions::nul_char_error(vm)); } // Validate: empty key or '=' in key after position 0 diff --git a/crates/vm/src/stdlib/os.rs b/crates/vm/src/stdlib/os.rs index 73e4918ccd4..a41e9990f12 100644 --- a/crates/vm/src/stdlib/os.rs +++ b/crates/vm/src/stdlib/os.rs @@ -211,7 +211,7 @@ pub(super) mod _os { }; #[cfg(not(windows))] use core::marker::PhantomData; - use core::time::Duration; + use core::{hint::cold_path, time::Duration}; use crossbeam_utils::atomic::AtomicCell; use rustpython_common::wtf8::Wtf8Buf; #[cfg(windows)] @@ -487,10 +487,16 @@ pub(super) mod _os { } #[cfg(not(windows))] - fn env_bytes_as_bytes(obj: &crate::function::Either) -> &[u8] { + fn env_bytes_as_bytes_checked( + obj: &crate::function::Either, + ) -> Option<&[u8]> { match obj { - crate::function::Either::A(s) => s.as_bytes(), - crate::function::Either::B(b) => b.as_bytes(), + crate::function::Either::A(s) if !s.contains_nuls() => Some(s.as_bytes()), + crate::function::Either::B(b) if !b.contains_nuls() => Some(b.as_bytes()), + _ => { + cold_path(); + None + } } } @@ -515,9 +521,10 @@ pub(super) mod _os { // defining hidden environment variables. if key_str.is_empty() || key_str.get(1..).is_some_and(|s| s.contains('=')) - || key_str.contains('\0') - || value_str.contains('\0') + || key.contains_nuls() + || value.contains_nuls() { + cold_path(); return Err(vm.new_value_error("illegal environment variable name")); } let env_str = format!("{key_str}={value_str}"); @@ -537,11 +544,13 @@ pub(super) mod _os { value: crate::function::Either, vm: &VirtualMachine, ) -> PyResult<()> { - let key = env_bytes_as_bytes(&key); - let value = env_bytes_as_bytes(&value); - if key.contains(&b'\0') || value.contains(&b'\0') { + let (Some(key), Some(value)) = ( + env_bytes_as_bytes_checked(&key), + env_bytes_as_bytes_checked(&value), + ) else { + cold_path(); return Err(exceptions::nul_byte_error(vm)); - } + }; if key.is_empty() || key.contains(&b'=') { return Err(vm.new_value_error("illegal environment variable name")); } @@ -560,8 +569,9 @@ pub(super) mod _os { // defining hidden environment variables. if key_str.is_empty() || key_str.get(1..).is_some_and(|s| s.contains('=')) - || key_str.contains('\0') + || key.contains_nuls() { + cold_path(); return Err(vm.new_value_error("illegal environment variable name")); } // "key=" to unset (empty value removes the variable) @@ -581,10 +591,10 @@ pub(super) mod _os { key: crate::function::Either, vm: &VirtualMachine, ) -> PyResult<()> { - let key = env_bytes_as_bytes(&key); - if key.contains(&b'\0') { + let Some(key) = env_bytes_as_bytes_checked(&key) else { + cold_path(); return Err(exceptions::nul_byte_error(vm)); - } + }; if key.is_empty() || key.contains(&b'=') { let x = vm.new_errno_error( 22, diff --git a/crates/vm/src/stdlib/pwd.rs b/crates/vm/src/stdlib/pwd.rs index e181de240f6..cfd571e4c17 100644 --- a/crates/vm/src/stdlib/pwd.rs +++ b/crates/vm/src/stdlib/pwd.rs @@ -11,6 +11,7 @@ mod pwd { exceptions, types::PyStructSequence, }; + use core::hint::cold_path; use rustpython_host_env::pwd as host_pwd; #[cfg(not(target_os = "android"))] @@ -50,15 +51,16 @@ mod pwd { #[pyfunction] fn getpwnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult { - let pw_name = name.as_str(); - if pw_name.contains('\0') { + if name.as_pystr().contains_nuls() { + cold_path(); return Err(exceptions::nul_char_error(vm)); } - let user = host_pwd::getpwnam(name.as_str()); + let name = name.as_str(); + let user = host_pwd::getpwnam(name); let user = user.ok_or_else(|| { vm.new_key_error( vm.ctx - .new_str(format!("getpwnam(): name not found: {pw_name}")) + .new_str(format!("getpwnam(): name not found: {name}")) .into(), ) })?; diff --git a/crates/vm/src/utils.rs b/crates/vm/src/utils.rs index d23cbb689a6..80402480cfd 100644 --- a/crates/vm/src/utils.rs +++ b/crates/vm/src/utils.rs @@ -4,7 +4,6 @@ use crate::{ PyObjectRef, PyResult, VirtualMachine, builtins::{PyStr, PyUtf8Str}, convert::{ToPyException, ToPyObject}, - exceptions::nul_char_error, }; pub fn hash_iter<'a, I: IntoIterator>( @@ -24,13 +23,6 @@ pub trait ToCString: AsRef { fn to_cstring(&self, vm: &VirtualMachine) -> PyResult { alloc::ffi::CString::new(self.as_ref().as_bytes()).map_err(|err| err.to_pyexception(vm)) } - fn ensure_no_nul(&self, vm: &VirtualMachine) -> PyResult<()> { - if self.as_ref().as_bytes().contains(&b'\0') { - Err(nul_char_error(vm)) - } else { - Ok(()) - } - } } impl ToCString for &str {}