Skip to content

Commit 6788a1e

Browse files
Use memchr consistently for NUL checks
`memchr` is used throughout RustPython for searching through bytes expediently. However, for NUL checks, it's scantily used. Instead, our NUL checks either use `contains` or `memchr`. I switched all of the `contains(b'\0')` I could find to using memchr instead. I marked the failure paths as cold to hint to LLVM that interior NULs are truly exceptional. This should help branch prediction a bit which is nice for string/bytes functions since they are likely called a lot.
1 parent 28454cc commit 6788a1e

19 files changed

Lines changed: 136 additions & 116 deletions

File tree

crates/host_env/src/multiprocessing.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ pub enum SemError {
3232
AlreadyExists,
3333
NotFound,
3434
InvalidInput,
35+
InteriorNul,
3536
Other(i32),
3637
}
3738

3839
#[cfg(unix)]
3940
impl SemError {
40-
fn from_errno(err: Errno) -> Self {
41+
const fn from_errno(err: Errno) -> Self {
4142
match err {
4243
Errno::EAGAIN => Self::WouldBlock,
4344
Errno::ETIMEDOUT => Self::TimedOut,
@@ -49,14 +50,14 @@ impl SemError {
4950
}
5051
}
5152

52-
pub fn raw_os_error(self) -> i32 {
53+
pub const fn raw_os_error(self) -> i32 {
5354
match self {
5455
Self::WouldBlock => Errno::EAGAIN as i32,
5556
Self::TimedOut => Errno::ETIMEDOUT as i32,
5657
Self::Interrupted => Errno::EINTR as i32,
5758
Self::AlreadyExists => Errno::EEXIST as i32,
5859
Self::NotFound => Errno::ENOENT as i32,
59-
Self::InvalidInput => Errno::EINVAL as i32,
60+
Self::InvalidInput | Self::InteriorNul => Errno::EINVAL as i32,
6061
Self::Other(code) => code,
6162
}
6263
}
@@ -119,7 +120,7 @@ impl SemHandle {
119120
value: u32,
120121
unlink: bool,
121122
) -> Result<(Self, Option<String>), SemError> {
122-
let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?;
123+
let cname = semaphore_name(name)?;
123124
let raw =
124125
unsafe { libc::sem_open(cname.as_ptr(), libc::O_CREAT | libc::O_EXCL, 0o600, value) };
125126
if raw == libc::SEM_FAILED {
@@ -141,7 +142,7 @@ impl SemHandle {
141142
}
142143

143144
pub fn open_existing(name: &str) -> Result<Self, SemError> {
144-
let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?;
145+
let cname = semaphore_name(name)?;
145146
let raw = unsafe { libc::sem_open(cname.as_ptr(), 0) };
146147
if raw == libc::SEM_FAILED {
147148
Err(SemError::from_errno(Errno::last()))
@@ -305,18 +306,18 @@ pub fn is_too_many_posts(err: u32) -> bool {
305306
}
306307

307308
#[cfg(unix)]
308-
pub fn semaphore_name(name: &str) -> Result<CString, alloc::ffi::NulError> {
309-
let mut full = String::with_capacity(name.len() + 1);
309+
pub fn semaphore_name(name: &str) -> Result<CString, SemError> {
310+
let mut full = String::with_capacity(name.len() + 2);
310311
if !name.starts_with('/') {
311312
full.push('/');
312313
}
313314
full.push_str(name);
314-
CString::new(full)
315+
CString::new(full).map_err(|_| SemError::InteriorNul)
315316
}
316317

317318
#[cfg(unix)]
318319
pub fn sem_unlink(name: &str) -> Result<(), SemError> {
319-
let cname = semaphore_name(name).map_err(|_| SemError::InvalidInput)?;
320+
let cname = semaphore_name(name)?;
320321
let res = unsafe { libc::sem_unlink(cname.as_ptr()) };
321322
if res < 0 {
322323
Err(SemError::from_errno(Errno::last()))

crates/host_env/src/time.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -618,10 +618,9 @@ unsafe extern "C" {
618618

619619
#[cfg(windows)]
620620
pub fn strftime_ascii(fmt: &str, tm: &libc::tm) -> Result<String, CheckedTmError> {
621-
if fmt.contains('\0') {
622-
return Err(CheckedTmError::EmbeddedNul);
623-
}
624-
let fmt_wide: Vec<u16> = fmt.encode_utf16().chain(core::iter::once(0)).collect();
621+
let fmt_wide = widestring::WideCString::from_str(fmt)
622+
.map_err(|_| CheckedTmError::EmbeddedNul)?
623+
.into_vec_with_nul();
625624
let mut size = 1024usize;
626625
let max_scale = 256usize.saturating_mul(fmt.len().max(1));
627626
loop {

crates/host_env/src/winapi.rs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,20 @@
33
reason = "This module mirrors Win32 APIs with raw handle and pointer parameters."
44
)]
55

6+
use core::hint::cold_path;
67
use std::{io, path::Path};
7-
use windows_sys::Win32::{
8-
Foundation::{HANDLE, HMODULE, WAIT_FAILED},
9-
System::Threading::PROCESS_INFORMATION,
10-
};
118

129
use crate::windows::{CheckWin32Bool, CheckWin32Handle};
1310

11+
use memchr::memchr;
1412
pub use windows_sys::Win32::{
1513
Foundation::{
1614
DUPLICATE_CLOSE_SOURCE, DUPLICATE_SAME_ACCESS, ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS,
1715
ERROR_BROKEN_PIPE, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NETNAME_DELETED, ERROR_NO_DATA,
1816
ERROR_NO_SYSTEM_RESOURCES, ERROR_NOT_FOUND, ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY,
1917
ERROR_PIPE_CONNECTED, ERROR_PORT_UNREACHABLE, ERROR_PRIVILEGE_NOT_HELD, ERROR_SEM_TIMEOUT,
20-
ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, STILL_ACTIVE, WAIT_ABANDONED_0, WAIT_OBJECT_0,
21-
WAIT_TIMEOUT,
18+
ERROR_SUCCESS, GENERIC_READ, GENERIC_WRITE, HANDLE, HMODULE, STILL_ACTIVE,
19+
WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT,
2220
},
2321
Globalization::{
2422
LCMAP_FULLWIDTH, LCMAP_HALFWIDTH, LCMAP_HIRAGANA, LCMAP_KATAKANA, LCMAP_LINGUISTIC_CASING,
@@ -57,12 +55,12 @@ pub use windows_sys::Win32::{
5755
ABOVE_NORMAL_PRIORITY_CLASS, BELOW_NORMAL_PRIORITY_CLASS, CREATE_BREAKAWAY_FROM_JOB,
5856
CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP,
5957
CREATE_NO_WINDOW, DETACHED_PROCESS, HIGH_PRIORITY_CLASS, IDLE_PRIORITY_CLASS,
60-
NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, REALTIME_PRIORITY_CLASS,
61-
STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK, STARTF_PREVENTPINNING,
62-
STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID, STARTF_TITLEISLINKNAME,
63-
STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS, STARTF_USEFILLATTRIBUTE,
64-
STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW, STARTF_USESIZE,
65-
STARTF_USESTDHANDLES,
58+
NORMAL_PRIORITY_CLASS, PROCESS_ALL_ACCESS, PROCESS_DUP_HANDLE, PROCESS_INFORMATION,
59+
REALTIME_PRIORITY_CLASS, STARTF_FORCEOFFFEEDBACK, STARTF_FORCEONFEEDBACK,
60+
STARTF_PREVENTPINNING, STARTF_RUNFULLSCREEN, STARTF_TITLEISAPPID,
61+
STARTF_TITLEISLINKNAME, STARTF_UNTRUSTEDSOURCE, STARTF_USECOUNTCHARS,
62+
STARTF_USEFILLATTRIBUTE, STARTF_USEHOTKEY, STARTF_USEPOSITION, STARTF_USESHOWWINDOW,
63+
STARTF_USESIZE, STARTF_USESTDHANDLES,
6664
},
6765
},
6866
UI::WindowsAndMessaging::SW_HIDE,
@@ -312,7 +310,8 @@ pub fn build_environment_block(
312310

313311
let mut last_entry: HashMap<String, Vec<u16>> = HashMap::new();
314312
for (key, value) in entries {
315-
if key.contains('\0') || value.contains('\0') {
313+
if memchr(b'\0', key.as_bytes()) || memchr(b'\0', value.as_bytes()) {
314+
cold_path();
316315
return Err(BuildEnvironmentBlockError::ContainsNul);
317316
}
318317
if key.is_empty() || key[1..].contains('=') {

crates/stdlib/src/grp.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ mod grp {
1010
exceptions,
1111
types::PyStructSequence,
1212
};
13+
use core::hint::cold_path;
1314
use rustpython_host_env::grp as host_grp;
1415

1516
#[pystruct_sequence_data]
@@ -61,10 +62,11 @@ mod grp {
6162

6263
#[pyfunction]
6364
fn getgrnam(name: PyUtf8StrRef, vm: &VirtualMachine) -> PyResult<GroupData> {
64-
let gr_name = name.as_str();
65-
if gr_name.contains('\0') {
65+
if name.as_pystr().contains_nuls() {
66+
cold_path();
6667
return Err(exceptions::nul_char_error(vm));
6768
}
69+
let gr_name = name.as_str();
6870
let group = host_grp::getgrnam(gr_name).map_err(|err| err.into_pyexception(vm))?;
6971
let group = group.ok_or_else(|| {
7072
vm.new_key_error(

crates/stdlib/src/mmap.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ mod mmap {
2424
use core::ops::{Deref, DerefMut};
2525
use crossbeam_utils::atomic::AtomicCell;
2626
use num_traits::Signed;
27-
#[cfg(windows)]
28-
use std::io;
2927
use std::io::Write;
28+
#[cfg(windows)]
29+
use {core::hint::cold_path, memchr::memchr, rustpython_vm::exceptions, std::io};
3030

3131
#[cfg(unix)]
3232
use rustpython_host_env::crt_fd;
@@ -460,8 +460,9 @@ mod mmap {
460460
let s = obj
461461
.try_to_value::<String>(vm)
462462
.map_err(|_| vm.new_type_error("tagname must be a string or None"))?;
463-
if s.contains('\0') {
464-
return Err(vm.new_value_error("tagname must not contain null characters"));
463+
if memchr(b'\0', s.as_bytes()).is_some() {
464+
cold_path();
465+
return Err(exceptions::nul_char_error(vm));
465466
}
466467
Some(s)
467468
}

crates/stdlib/src/multiprocessing.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ mod _multiprocessing {
1010
function::{ArgBytesLike, FuncArgs, KwArgs},
1111
types::Constructor,
1212
};
13-
use core::sync::atomic::{AtomicI32, AtomicU32, Ordering};
13+
use core::{
14+
hint::cold_path,
15+
sync::atomic::{AtomicI32, AtomicU32, Ordering},
16+
};
1417
use rustpython_host_env::multiprocessing as host_multiprocessing;
1518

1619
// These match the values in Lib/multiprocessing/synchronize.py
@@ -811,7 +814,7 @@ mod _multiprocessing {
811814
let value = args.value as u32;
812815
let (handle, name) =
813816
SemHandle::create(&args.name, value, args.unlink).map_err(|err| {
814-
if err == SemError::InvalidInput && args.name.contains('\0') {
817+
if err == SemError::InteriorNul {
815818
exceptions::nul_char_error(vm)
816819
} else {
817820
os_error(vm, err)
@@ -835,7 +838,7 @@ mod _multiprocessing {
835838
#[pyfunction]
836839
fn sem_unlink(name: String, vm: &VirtualMachine) -> PyResult<()> {
837840
host_multiprocessing::sem_unlink(&name).map_err(|err| {
838-
if err == SemError::InvalidInput && name.contains('\0') {
841+
if err == SemError::InteriorNul {
839842
exceptions::nul_char_error(vm)
840843
} else {
841844
os_error(vm, err)

crates/stdlib/src/openssl.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ fn probe() -> &'static ProbeResult {
5353
#[cfg(ossl111)] ossl111,
5454
#[cfg(windows)] windows))]
5555
mod _ssl {
56+
use core::hint::cold_path;
57+
5658
use super::{bio, probe};
5759

5860
// Import error types and helpers used in this module (others are exposed via pymodule(with(...)))
@@ -85,6 +87,7 @@ mod _ssl {
8587
};
8688
use crossbeam_utils::atomic::AtomicCell;
8789
use foreign_types_shared::{ForeignType, ForeignTypeRef};
90+
use memchr::memchr;
8891
use openssl::{
8992
asn1::{Asn1Object, Asn1ObjectRef},
9093
error::ErrorStack,
@@ -1039,12 +1042,13 @@ mod _ssl {
10391042

10401043
#[pymethod]
10411044
fn set_ciphers(&self, cipherlist: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
1042-
let ciphers: &str = cipherlist.as_ref();
1043-
if ciphers.contains('\0') {
1045+
if cipherlist.contains_nuls() {
1046+
cold_path();
10441047
return Err(exceptions::nul_char_error(vm));
10451048
}
1049+
10461050
self.builder()
1047-
.set_cipher_list(ciphers)
1051+
.set_cipher_list(cipherlist.as_ref())
10481052
.map_err(|_| new_ssl_error(vm, "No cipher can be selected."))
10491053
}
10501054

@@ -1096,9 +1100,6 @@ mod _ssl {
10961100
let name_cstr = match name {
10971101
Either::A(s) => {
10981102
let s: &str = s.as_ref();
1099-
if s.contains('\0') {
1100-
return Err(exceptions::nul_char_error(vm));
1101-
}
11021103
s.to_cstring(vm)?
11031104
}
11041105
Either::B(b) => std::ffi::CString::new(b.borrow_buf().to_vec())
@@ -2031,15 +2032,16 @@ mod _ssl {
20312032

20322033
// Configure server hostname
20332034
if let Some(hostname) = &server_hostname {
2035+
if hostname.contains_nuls() {
2036+
cold_path();
2037+
return Err(exceptions::nul_char_type_error(vm));
2038+
}
20342039
let hostname_str: &str = hostname.as_ref();
20352040
if hostname_str.is_empty() || hostname_str.starts_with('.') {
20362041
return Err(vm.new_value_error(
20372042
"server_hostname cannot be an empty string or start with a leading dot.",
20382043
));
20392044
}
2040-
if hostname_str.contains('\0') {
2041-
return Err(exceptions::nul_char_type_error(vm));
2042-
}
20432045
let ip = hostname_str.parse::<core::net::IpAddr>();
20442046
if ip.is_err() {
20452047
ssl.set_hostname(hostname_str)

crates/stdlib/src/ssl.rs

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ mod _ssl {
6767
use alloc::sync::Arc;
6868
use core::{
6969
hash::{Hash, Hasher},
70+
hint::cold_path,
7071
sync::atomic::{AtomicUsize, Ordering},
7172
time::Duration,
7273
};
74+
use memchr::memchr;
7375
use rustpython_vm::exceptions;
7476
use std::{
7577
collections::{HashMap, hash_map::DefaultHasher},
@@ -392,7 +394,8 @@ mod _ssl {
392394
// IP addresses are allowed as server_hostname
393395
// SNI will not be sent for IP addresses
394396

395-
if hostname.contains('\0') {
397+
if memchr(b'\0', hostname.as_bytes()).is_some() {
398+
cold_path();
396399
return Err(exceptions::nul_char_type_error(vm));
397400
}
398401

@@ -1854,25 +1857,7 @@ mod _ssl {
18541857
let hostname = match args.server_hostname.into_option().flatten() {
18551858
Some(hostname_str) => {
18561859
let hostname = hostname_str.as_str();
1857-
1858-
// Validate hostname
1859-
if hostname.is_empty() {
1860-
return Err(vm.new_value_error("server_hostname cannot be an empty string"));
1861-
}
1862-
1863-
// Check if it starts with a dot
1864-
if hostname.starts_with('.') {
1865-
return Err(vm.new_value_error("server_hostname cannot start with a dot"));
1866-
}
1867-
1868-
// IP addresses are allowed
1869-
// SNI will not be sent for IP addresses
1870-
1871-
// Check for NULL bytes
1872-
if hostname.contains('\0') {
1873-
return Err(exceptions::nul_char_error(vm));
1874-
}
1875-
1860+
validate_hostname(hostname, vm)?;
18761861
Some(hostname.to_string())
18771862
}
18781863
None => None,

crates/vm/src/builtins/bytes.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ use crate::{
3131
};
3232
use bstr::ByteSlice;
3333
use core::{mem::size_of, ops::Deref};
34+
use memchr::memchr;
3435

3536
#[pyclass(module = false, name = "bytes")]
3637
#[derive(Clone, Debug)]
@@ -169,6 +170,13 @@ impl PyBytes {
169170
.map(|x| vm.ctx.new_bytes(x).into()),
170171
}
171172
}
173+
174+
/// Check bytes for interior NULs.
175+
#[inline]
176+
#[must_use]
177+
pub fn contains_nuls(&self) -> bool {
178+
memchr(b'\0', self.as_bytes()).is_some()
179+
}
172180
}
173181

174182
impl PyRef<PyBytes> {
@@ -218,7 +226,7 @@ impl PyBytes {
218226

219227
#[inline]
220228
#[must_use]
221-
pub fn as_bytes(&self) -> &[u8] {
229+
pub const fn as_bytes(&self) -> &[u8] {
222230
self.inner.as_bytes()
223231
}
224232

crates/vm/src/builtins/str.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use bstr::ByteSlice;
3737
use core::ffi::CStr;
3838
use core::{char, mem, ops::Range};
3939
use itertools::Itertools;
40+
use memchr::memchr;
4041
use num_traits::ToPrimitive;
4142
use rustpython_common::{
4243
ascii,
@@ -545,6 +546,13 @@ impl PyStr {
545546
}
546547
}
547548

549+
/// Check string bytes for interior NULs.
550+
#[inline]
551+
#[must_use]
552+
pub fn contains_nuls(&self) -> bool {
553+
memchr(b'\0', self.as_bytes()).is_some()
554+
}
555+
548556
pub fn to_string_lossy(&self) -> Cow<'_, str> {
549557
self.to_str()
550558
.map_or_else(|| self.as_wtf8().to_string_lossy(), Cow::Borrowed)
@@ -2150,7 +2158,7 @@ impl PyUtf8Str {
21502158

21512159
impl Py<PyUtf8Str> {
21522160
/// Upcast to PyStr.
2153-
pub fn as_pystr(&self) -> &Py<PyStr> {
2161+
pub const fn as_pystr(&self) -> &Py<PyStr> {
21542162
unsafe {
21552163
// Safety: PyUtf8Str is a wrapper around PyStr, so this cast is safe.
21562164
&*(self as *const Self as *const Py<PyStr>)

0 commit comments

Comments
 (0)