Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 28 additions & 21 deletions crates/host_env/src/ctypes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use alloc::borrow::Cow;
#[cfg(unix)]
use alloc::ffi::CString;
use core::ffi::{
CStr, c_char, c_double, c_float, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint,
c_ulong, c_ulonglong, c_ushort, c_void,
Expand Down Expand Up @@ -32,8 +34,8 @@ use libloading::Library;
use libloading::os::unix::Library as UnixLibrary;
#[cfg(any(unix, windows))]
use parking_lot::{Mutex, RwLock};
use rustpython_wtf8::Wtf8;
use rustpython_wtf8::Wtf8Buf;
use rustpython_wtf8::{InteriorNulError, Wtf8};
#[cfg(any(unix, windows))]
use std::{collections::HashMap, ffi::OsStr, sync::OnceLock};

Expand Down Expand Up @@ -383,7 +385,7 @@ pub fn dlopen_mode(load_flags: Option<i32>) -> i32 {

#[cfg(target_os = "macos")]
pub fn dyld_shared_cache_contains_path(path: &str) -> Result<bool, alloc::ffi::NulError> {
let c_path = alloc::ffi::CString::new(path)?;
let c_path = CString::new(path)?;

unsafe extern "C" {
fn _dyld_shared_cache_contains_path(path: *const c_char) -> bool;
Expand Down Expand Up @@ -523,21 +525,24 @@ pub fn encode_wtf8_to_wchar_padded(s: &Wtf8, size: usize) -> Vec<u8> {
wchar_bytes
}

pub fn wchar_null_terminated_bytes(s: &Wtf8) -> Vec<u8> {
let wchars: Vec<WChar> = s
.code_points()
.map(|cp| cp.to_u32() as WChar)
.chain(core::iter::once(0))
.collect();
vec_into_bytes(wchars)
/// Encode `s` into a NUL terminated wide string without any interior NULs.
///
/// The result of this function is suitable for Windows FFI.
pub fn wchar_nul_terminated_bytes(s: &Wtf8) -> Result<Vec<u8>, InteriorNulError> {
let v: Vec<u16> = s.encode_wide_ffi().collect::<Result<_, _>>()?;
// SAFETY: u16 is POD.
Ok(unsafe { vec_into_bytes(v) })
}

pub fn vec_into_bytes<T>(vec: Vec<T>) -> Vec<u8> {
let len = vec.len() * core::mem::size_of::<T>();
let cap = vec.capacity() * core::mem::size_of::<T>();
let ptr = vec.as_ptr() as *mut u8;
core::mem::forget(vec);
unsafe { Vec::from_raw_parts(ptr, len, cap) }
// Reinterpret Vec<T> into Vec<u8>.
//
// # Safety:
// T must be a plain old data type. No pointers. No destructors. If this function ever needs to be
// public, switch to using bytemuck instead.
unsafe fn vec_into_bytes<T: Copy>(vec: Vec<T>) -> Vec<u8> {
assert!(size_of::<T>() != 0);
let (ptr, len, cap) = vec.into_raw_parts();
unsafe { Vec::from_raw_parts(ptr.cast::<u8>(), len * size_of::<T>(), cap * size_of::<T>()) }
}
Comment on lines +542 to 546

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Vec::from_raw_parts alignment requirement different type u8 u16

💡 Result:

Using Vec::from_raw_parts to change the type of a Vec from T to U is generally unsafe if T and U have different alignment requirements [1][2]. For Vec::from_raw_parts to be sound, the new type T must have the same alignment as the type that was originally used to allocate the memory [1][2]. This is because Rust's allocator must deallocate memory using the same layout (size and alignment) that was used during allocation [1][2]. When a Vec is dropped, it uses its type's alignment to deallocate the buffer; if you construct a Vec (alignment 1) from a pointer originally allocated for a Vec (alignment 2), the deallocation will be performed with incorrect alignment, which results in undefined behavior and potential corruption of the allocator's internal structures [1][2]. Even if the size in bytes (size_of:: * capacity) is preserved, the alignment requirement remains strict [1][2]. Therefore, you cannot simply cast a pointer from a Vec to a Vec and pass it to Vec::from_raw_parts [1][2]. If you need to view data as a different type, the recommended, safe approach is to use slice::from_raw_parts to create a slice of the desired type rather than constructing a new Vec, provided the pointer is properly aligned for the new type [1][2]. Creating a new Vec with a different type should only be done if you are certain the alignment requirements are identical or if you are managing the allocation manually in a way that respects the original allocation's layout [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- file excerpt ---\n'
sed -n '520,560p' crates/host_env/src/ctypes.rs

printf '\n--- call sites ---\n'
rg -n "vec_into_bytes|wchar_nul_terminated_bytes|utf16z_bytes|into_raw_parts" crates/host_env/src/ctypes.rs

printf '\n--- toolchain hints ---\n'
git ls-files | rg '(^|/)(rust-toolchain(\.toml)?|Cargo\.toml)$' || true
if [ -f rust-toolchain.toml ]; then cat -n rust-toolchain.toml; fi
if [ -f rust-toolchain ]; then cat -n rust-toolchain; fi
if [ -f Cargo.toml ]; then rg -n 'rust-version|edition' Cargo.toml; fi
find . -maxdepth 2 \( -name 'rust-toolchain' -o -name 'rust-toolchain.toml' -o -name 'Cargo.toml' \) -print | sort

Repository: RustPython/RustPython

Length of output: 250


🌐 Web query:

Rust Vec::into_raw_parts stabilized version stable or nightly

💡 Result:

The Rust method Vec::into_raw_parts is stable as of Rust 1.93.0 [1][2]. It is no longer an unstable or nightly-only feature [3][4]. The method decomposes a Vec into its raw components: (pointer, length, capacity) [3][2]. As of the current date of July 9, 2026, it is available in the stable release of the Rust standard library [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- file excerpt ---'
sed -n '520,560p' crates/host_env/src/ctypes.rs

echo
echo '--- call sites ---'
rg -n "vec_into_bytes|wchar_nul_terminated_bytes|utf16z_bytes|encode_wide_ffi|into_raw_parts" crates/host_env/src/ctypes.rs

echo
echo '--- toolchain files ---'
find . -maxdepth 2 \( -name 'rust-toolchain' -o -name 'rust-toolchain.toml' -o -name 'Cargo.toml' \) -print | sort
if [ -f rust-toolchain.toml ]; then echo 'rust-toolchain.toml:'; cat -n rust-toolchain.toml; fi
if [ -f rust-toolchain ]; then echo 'rust-toolchain:'; cat -n rust-toolchain; fi
if [ -f Cargo.toml ]; then echo 'Cargo.toml hints:'; rg -n 'rust-version|edition' Cargo.toml; fi

Repository: RustPython/RustPython

Length of output: 2118


🌐 Web query:

Rust std Vec::from_raw_parts deallocate same layout alignment documentation

💡 Result:

Yes, Vec::from_raw_parts requires that the memory be deallocated using the exact same layout (size and alignment) that was used during the original allocation [1][2]. This is a strict safety requirement because the Rust allocator (GlobalAlloc/Allocator) must be called with a layout that matches the one used to allocate the memory [1][2][3]. The safety documentation for Vec::from_raw_parts explicitly states [1][2]: - T must have the same alignment as what the pointer was originally allocated with. Providing a type with a less strict alignment is insufficient; the alignment must be exactly the same [1][2]. - The total size (size of T multiplied by the capacity) must be the same as the size used during the original allocation [1][2]. - Capacity must be the same as the capacity used when the memory was originally allocated [1][4][2]. Violating these requirements, such as by casting the pointer to a type with a different alignment or size, causes undefined behavior because it results in the Vec being dropped with an incorrect layout, which can corrupt the allocator's internal data structures [1][2]. For example, if a buffer is allocated as Vec (alignment 2) and subsequently turned into a Vec (alignment 1), the Vec will attempt to deallocate the memory with alignment 1, which violates the requirement to match the original allocation [1][2]. To avoid these issues, if you need to work with a different type or layout, it is generally recommended to use slice::from_raw_parts instead of constructing a new Vec [1][2].

Citations:


Rebuild the wide-string bytes with a copy, not Vec::from_raw_parts

Vec::from_raw_parts must use the same allocation layout, including alignment. Casting the Vec<u16> buffer to Vec<u8> and dropping it deallocates with a different layout, which is undefined behavior. Convert the data by copying the bytes instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/host_env/src/ctypes.rs` around lines 542 - 546, The vec_into_bytes
helper currently reinterprets the original allocation with Vec::from_raw_parts,
which can deallocate with the wrong layout for wide-string buffers. Update
vec_into_bytes in ctypes.rs to copy the bytes out of the source Vec<T> instead
of casting the allocation; keep the existing size_of::<T>() guard, but replace
the raw-parts reconstruction with a safe byte copy approach so the returned
Vec<u8> owns a correctly laid-out allocation.


pub enum IntegerValue {
Expand Down Expand Up @@ -1122,14 +1127,16 @@ pub fn simple_storage_value_to_bytes_endian(
}
}

pub fn utf16z_bytes(s: &Wtf8) -> Vec<u8> {
vec_into_bytes::<u16>(s.encode_wide().chain(core::iter::once(0)).collect())
#[inline]
pub fn utf16z_bytes(s: &Wtf8) -> Result<Vec<u8>, InteriorNulError> {
let v: Vec<u16> = s.encode_wide_ffi().collect::<Result<_, _>>()?;
// SAFETY: u16 is POD.
Ok(unsafe { vec_into_bytes::<u16>(v) })
}

pub fn null_terminated_bytes(bytes: &[u8]) -> Vec<u8> {
let mut buffer = bytes.to_vec();
buffer.push(0);
buffer
#[inline]
pub fn null_terminated_bytes(bytes: &[u8]) -> Result<Vec<u8>, alloc::ffi::NulError> {
CString::new(bytes).map(CString::into_bytes_with_nul)
}

pub fn decode_type_code(type_code: &str, bytes: &[u8]) -> DecodedValue {
Expand Down
9 changes: 7 additions & 2 deletions crates/host_env/src/fileutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod windows {
use alloc::ffi::CString;
use libc::{S_IFCHR, S_IFDIR, S_IFMT};
use std::ffi::{OsStr, OsString};
use std::io;
use std::os::windows::io::AsRawHandle;
use std::sync::OnceLock;
use windows_sys::Win32::Foundation::{
Expand Down Expand Up @@ -72,10 +73,14 @@ pub mod windows {

impl StatStruct {
// update_st_mode_from_path in cpython
pub fn update_st_mode_from_path(&mut self, path: &OsStr, attr: u32) {
pub fn update_st_mode_from_path(
&mut self,
path: &OsStr,
attr: u32,
) -> Result<(), io::Error> {
if attr & FILE_ATTRIBUTE_DIRECTORY == 0 {
let file_extension = path
.to_wide()
.to_wide()?
.split(|&c| c == '.' as u16)
.next_back()
.and_then(|s| String::from_utf16(s).ok());
Expand Down
56 changes: 22 additions & 34 deletions crates/host_env/src/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use rustpython_wtf8::Wtf8;
use std::{
ffi::{OsStr, OsString},
io,
os::windows::ffi::{OsStrExt, OsStringExt},
};
use widestring::{WideCString, error::NulError};
use windows_sys::Win32::{
Foundation::{
E_POINTER, ERROR_INSUFFICIENT_BUFFER, ERROR_INVALID_FLAGS, ERROR_NO_UNICODE_TRANSLATION,
Expand Down Expand Up @@ -394,54 +394,42 @@ pub fn multi_byte_to_wide(
}
}

/// [`OsStr`] to [`WideCString`] for Windows FFI.
///
/// Prefer using this trait when encoding bytes to pass to Windows. Interior NULs are memory safe
/// but possibly a security hazard for FFI.
///
/// https://github.com/python/cpython/issues/111656
pub trait ToWideString {
fn to_wide(&self) -> Vec<u16>;
fn to_wide_with_nul(&self) -> Vec<u16>;
fn to_wide_cstring(&self) -> widestring::WideCString {
widestring::WideCString::from_vec_truncate(self.to_wide())
}
fn to_wide(&self) -> Result<Vec<u16>, io::Error>;
fn to_wide_with_nul(&self) -> Result<Vec<u16>, io::Error>;
fn to_wide_cstring(&self) -> Result<WideCString, io::Error>;
}

impl<T> ToWideString for T
where
T: AsRef<OsStr>,
{
fn to_wide(&self) -> Vec<u16> {
self.as_ref().encode_wide().collect()
fn to_wide(&self) -> Result<Vec<u16>, io::Error> {
WideCString::from_os_str(self)
.map(WideCString::into_vec)
.map_err(io::Error)
}
Comment on lines +413 to 417

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant section of crates/host_env/src/windows.rs with line numbers.
sed -n '390,450p' crates/host_env/src/windows.rs | cat -n

# Show the trait/impl context around to_wide/to_wide_with_nul if present.
rg -n "fn to_wide|fn to_wide_with_nul|impl .*Wide" crates/host_env/src/windows.rs

Repository: RustPython/RustPython

Length of output: 2417


io::Error can't be used as a mapper here. .map_err(io::Error) won't compile; use io::Error::other like the other methods.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/host_env/src/windows.rs` around lines 413 - 417, The `to_wide` method
in `windows.rs` is using `io::Error` as a direct mapper, which won’t compile in
this context. Update the `WideCString::from_os_str(self)` error handling to use
`io::Error::other`, matching the pattern used by the other conversion methods,
while keeping the rest of `to_wide` unchanged.

fn to_wide_with_nul(&self) -> Vec<u16> {
self.as_ref().encode_wide().chain(Some(0)).collect()
fn to_wide_with_nul(&self) -> Result<Vec<u16>, io::Error> {
WideCString::from_os_str(self)
.map(WideCString::into_vec_with_nul)
.map_err(io::Error::other)
}
}

impl ToWideString for OsStr {
fn to_wide(&self) -> Vec<u16> {
self.encode_wide().collect()
}
fn to_wide_with_nul(&self) -> Vec<u16> {
self.encode_wide().chain(Some(0)).collect()
fn to_wide_cstring(&self) -> Result<WideCString, io::Error> {
WideCString::from_os_str(self).map_err(io::Error::other)
}
}

impl ToWideString for Wtf8 {
fn to_wide(&self) -> Vec<u16> {
fn to_wide(&self) -> Result<Vec<u16>, io::Error> {
self.encode_wide().collect()
}
fn to_wide_with_nul(&self) -> Vec<u16> {
fn to_wide_with_nul(&self) -> Result<Vec<u16>, io::Error> {
self.encode_wide().chain(Some(0)).collect()
}
}
Comment on lines 428 to 435

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show file size and relevant region around the reported lines.
wc -l crates/host_env/src/windows.rs
sed -n '360,520p' crates/host_env/src/windows.rs

# Map the file structure for the relevant symbols.
ast-grep outline crates/host_env/src/windows.rs --view expanded

Repository: RustPython/RustPython

Length of output: 5098


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print the exact trait definition and the Wtf8 impl with line numbers.
grep -n "trait ToWideString\|impl ToWideString for Wtf8\|encode_wide_ffi\|to_wide_cstring" -n crates/host_env/src/windows.rs || true

nl -ba crates/host_env/src/windows.rs | sed -n '1,260p'
nl -ba crates/host_env/src/windows.rs | sed -n '380,470p'

Repository: RustPython/RustPython

Length of output: 400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate Wtf8 definitions and wide-encoding methods.
rg -n "impl .*AsRef<OsStr> for Wtf8|impl .*Wtf8|fn encode_wide_ffi|fn encode_wide\(" . --glob '!target' --glob '!**/target/**'

# Show the likely source file(s) around Wtf8 method definitions if present.
python3 - <<'PY'
from pathlib import Path
for path in Path('.').rglob('*.rs'):
    txt = path.read_text(errors='ignore')
    if 'encode_wide_ffi' in txt or 'encode_wide(' in txt and 'Wtf8' in txt:
        print(path)
PY

Repository: RustPython/RustPython

Length of output: 7295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the Wtf8 encoder definitions and nearby iterator types.
wc -l crates/wtf8/src/lib.rs
sed -n '880,960p' crates/wtf8/src/lib.rs
sed -n '1,120p' crates/host_env/src/windows.rs

# Show the exact Wtf8 encoders and any type names they return.
rg -n "pub fn encode_wide\(|pub fn encode_wide_ffi\(|struct EncodeWide|struct EncodeWideForFfi|type EncodeWide|type EncodeWideForFfi" crates/wtf8/src/lib.rs

Repository: RustPython/RustPython

Length of output: 7661


Implement the missing trait method and switch to the checked encoder. crates/host_env/src/windows.rs:428-435 still needs to_wide_cstring, and the current encode_wide().collect() / .chain(Some(0)).collect() calls don't satisfy Result<Vec<u16>, io::Error>; use encode_wide_ffi() so interior NULs still fail.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/host_env/src/windows.rs` around lines 428 - 435, The Wtf8
implementation of ToWideString is incomplete and uses the wrong encoder for the
checked Result-based API. In the ToWideString impl for Wtf8, add the missing
to_wide_cstring method alongside to_wide and to_wide_with_nul, and update all
three methods to use encode_wide_ffi() so they return Result<Vec<u16>,
io::Error> correctly and reject interior NULs. Keep the fix localized to the
Wtf8 trait implementation in windows.rs.


pub trait FromWideString
where
Self: Sized,
{
fn from_wides_until_nul(wide: &[u16]) -> Self;
}

impl FromWideString for OsString {
fn from_wides_until_nul(wide: &[u16]) -> Self {
let len = wide.iter().take_while(|&&c| c != 0).count();
Self::from_wide(&wide[..len])
}
}
19 changes: 11 additions & 8 deletions crates/vm/src/stdlib/_ctypes/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use core::fmt::Debug;
use crossbeam_utils::atomic::AtomicCell;
use num_traits::{Signed, ToPrimitive};
use rustpython_common::lock::PyRwLock;
use rustpython_common::wtf8::Wtf8;
use rustpython_common::wtf8::{InteriorNulError, Wtf8};
use rustpython_host_env::ctypes::{
CTypeParamKind, FfiArg, FfiType, FfiValue, char_array_assignment_bytes, char_array_field_value,
ffi_arg_from_value, ffi_type_for_layout, wchar_array_field_value, write_cow_bytes_at_offset,
Expand Down Expand Up @@ -365,19 +365,22 @@ pub(super) static CDATA_BUFFER_METHODS: BufferMethods = BufferMethods {
pub(super) fn ensure_z_null_terminated(
bytes: &PyBytes,
vm: &VirtualMachine,
) -> (PyObjectRef, usize) {
let buffer = rustpython_host_env::ctypes::null_terminated_bytes(bytes.as_bytes());
) -> Result<(PyObjectRef, usize), alloc::ffi::NulError> {
let buffer = rustpython_host_env::ctypes::null_terminated_bytes(bytes.as_bytes())?;
let ptr = buffer.as_ptr() as usize;
let kept_alive: PyObjectRef = vm.ctx.new_bytes(buffer).into();
(kept_alive, ptr)
Ok((kept_alive, ptr))
}

/// Convert str to null-terminated wchar_t buffer. Returns (PyBytes holder, pointer).
pub(super) fn str_to_wchar_bytes(s: &Wtf8, vm: &VirtualMachine) -> (PyObjectRef, usize) {
let bytes = rustpython_host_env::ctypes::wchar_null_terminated_bytes(s);
pub(super) fn str_to_wchar_bytes(
s: &Wtf8,
vm: &VirtualMachine,
) -> Result<(PyObjectRef, usize), InteriorNulError> {
let bytes = rustpython_host_env::ctypes::wchar_nul_terminated_bytes(s)?;
let ptr = bytes.as_ptr() as usize;
let holder: PyObjectRef = vm.ctx.new_bytes(bytes).into();
(holder, ptr)
Ok((holder, ptr))
}

/// PyCData - base type for all ctypes data types
Expand Down Expand Up @@ -957,7 +960,7 @@ impl PyCData {
if field_type_code.as_deref() == Some("z")
&& let Some(bytes_val) = value.downcast_ref::<PyBytes>()
{
let (kept_alive, ptr) = ensure_z_null_terminated(bytes_val, vm);
let (kept_alive, ptr) = ensure_z_null_terminated(bytes_val, vm)?;
let result =
rustpython_host_env::ctypes::pointer_to_sized_bytes_endian(ptr, size, needs_swap);
self.write_bytes_at_offset(offset, &result);
Expand Down
4 changes: 2 additions & 2 deletions crates/vm/src/stdlib/_ctypes/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult<Argument> {

// 4. Python str -> wide string pointer (like PyUnicode_AsWideCharString)
if let Some(s) = value.downcast_ref::<PyStr>() {
let wide_bytes = rustpython_host_env::ctypes::utf16z_bytes(s.as_wtf8());
let wide_bytes = rustpython_host_env::ctypes::utf16z_bytes(s.as_wtf8())?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'From<\s*(InteriorNulError|alloc::ffi::NulError|NulError)\s*>' crates

Repository: RustPython/RustPython

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'crates/vm/src/stdlib/_ctypes/*' 'crates/vm/src/*.rs' | sed -n '1,120p'

printf '\n== search for conversion impls ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'From<[^>]*(InteriorNulError|NulError|alloc::ffi::NulError)[^>]*>' crates || true

printf '\n== search for the relevant helpers and call sites ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'utf16z_bytes|null_terminated_bytes|conv_param|InteriorNulError|NulError' \
  crates/vm/src/stdlib/_ctypes crates/vm/src | sed -n '1,220p'

Repository: RustPython/RustPython

Length of output: 6491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== PyErr conversion impls ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'impl\s+From<.*for\s+PyErr|impl\s+From<.*for\s+PyBaseExceptionRef|ToPyException' \
  crates/vm/src crates/common/src crates -g '*.rs' | sed -n '1,240p'

printf '\n== exception type aliases / definitions ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'type\s+PyResult|type\s+PyBaseExceptionRef|struct\s+PyErr|enum\s+PyErr|trait\s+ToPyException' \
  crates/vm/src crates/common/src crates -g '*.rs' | sed -n '1,220p'

printf '\n== explicit InteriorNulError coverage ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'InteriorNulError' crates -g '*.rs' | sed -n '1,220p'

Repository: RustPython/RustPython

Length of output: 10543


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== conversion plumbing ==\n'
sed -n '1,220p' crates/vm/src/convert/to_pyobject.rs

printf '\n== exception conversions around NulError ==\n'
sed -n '1190,1245p' crates/vm/src/exceptions.rs

printf '\n== any InteriorNulError conversion impls ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'impl\s+(ToPyException|From<.*>)\s+for\s+.*InteriorNulError|InteriorNulError.*ToPyException|From<InteriorNulError>' \
  crates -g '*.rs' | sed -n '1,200p'

Repository: RustPython/RustPython

Length of output: 3264


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== generic From<E> for PyBaseExceptionRef / PyErr ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'impl\s*<[^>]*\bE\b[^>]*>\s*From<\s*E\s*>\s*for\s*(PyBaseExceptionRef|PyErr)|impl\s*<[^>]*>\s*From<[^>]*ToPyException[^>]*>\s*for\s*(PyBaseExceptionRef|PyErr)|FromResidual' \
  crates -g '*.rs' | sed -n '1,240p'

printf '\n== convert module files ==\n'
git ls-files crates/vm/src/convert/*.rs crates/vm/src/exceptions.rs | sed -n '1,120p'

printf '\n== any impl blocks involving PyBaseExceptionRef in exceptions.rs ==\n'
rg -n --hidden --glob '!target' --glob '!**/target/**' \
  'PyBaseExceptionRef|PyErr' crates/vm/src/exceptions.rs crates/vm/src/convert/*.rs | sed -n '1,240p'

Repository: RustPython/RustPython

Length of output: 213


Add an explicit conversion for these ?s
utf16z_bytes(...) and null_terminated_bytes(...) need an explicit conversion into PyBaseExceptionRef; there’s no matching From<InteriorNulError> / From<alloc::ffi::NulError> path here, so ? won’t work as written. The same pattern appears in crates/vm/src/stdlib/_ctypes/base.rs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm/src/stdlib/_ctypes/function.rs` at line 158, In the ctypes
conversion helpers, the `?` operator is being used on errors that do not
automatically convert into `PyBaseExceptionRef`, so fix the error handling in
the functions that call `rustpython_host_env::ctypes::utf16z_bytes` and
`null_terminated_bytes` by explicitly mapping those `InteriorNulError` and
`NulError` values into the Python exception type before propagating them. Apply
the same change in the corresponding logic in `function.rs` and `base.rs`,
keeping the conversion localized near the existing `utf16z_bytes` /
`null_terminated_bytes` calls.

let keep = vm.ctx.new_bytes(wide_bytes);
let addr = keep.as_bytes().as_ptr() as usize;
return Ok(Argument {
Expand All @@ -168,7 +168,7 @@ fn conv_param(value: &PyObject, vm: &VirtualMachine) -> PyResult<Argument> {
// 9. Python bytes -> null-terminated buffer pointer
// Need to ensure null termination like c_char_p
if let Some(bytes) = value.downcast_ref::<PyBytes>() {
let buffer = rustpython_host_env::ctypes::null_terminated_bytes(bytes.as_bytes());
let buffer = rustpython_host_env::ctypes::null_terminated_bytes(bytes.as_bytes())?;
let keep = vm.ctx.new_bytes(buffer);
let addr = keep.as_bytes().as_ptr() as usize;
return Ok(Argument {
Expand Down
63 changes: 63 additions & 0 deletions crates/wtf8/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,15 @@ impl Wtf8 {
}
}

/// Converts the WTF-8 string to potentially ill-formed UTF-8 while checking for interior
/// NULs.
Comment on lines +915 to +916

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Doc comment names the wrong encoding.

encode_wide_ffi produces potentially ill-formed UTF-16 wide code units (same as encode_wide), not UTF-8. The doc text was likely copied and should say UTF-16.

📝 Proposed doc fix
-    /// Converts the WTF-8 string to potentially ill-formed UTF-8 while checking for interior
-    /// NULs.
+    /// Converts the WTF-8 string to potentially ill-formed UTF-16 while checking for interior
+    /// NULs.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Converts the WTF-8 string to potentially ill-formed UTF-8 while checking for interior
/// NULs.
/// Converts the WTF-8 string to potentially ill-formed UTF-16 while checking for interior
/// NULs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/wtf8/src/lib.rs` around lines 915 - 916, The doc comment on
encode_wide_ffi names the wrong encoding: it currently says the function
converts to potentially ill-formed UTF-8, but this helper returns potentially
ill-formed UTF-16 wide code units like encode_wide. Update the comment text for
encode_wide_ffi to describe UTF-16 instead of UTF-8, keeping the note about
checking for interior NULs.

pub fn encode_wide_ffi(&self) -> EncodeWideForFfi<'_> {
EncodeWideForFfi {
iter: self.encode_wide(),
complete: false,
}
}

pub const fn chunks(&self) -> Wtf8Chunks<'_> {
Wtf8Chunks { wtf8: self }
}
Expand Down Expand Up @@ -1494,6 +1503,60 @@ impl Iterator for EncodeWide<'_> {

impl FusedIterator for EncodeWide<'_> {}

pub struct EncodeWideForFfi<'s> {
iter: EncodeWide<'s>,
complete: bool,
}

impl Iterator for EncodeWideForFfi<'_> {
type Item = Result<u16, InteriorNulError>;

fn next(&mut self) -> Option<Self::Item> {
if self.complete {
return None;
}

match self.iter.next() {
Some(v) if v != 0 => Some(Ok(v)),
Some(v) => {
core::hint::cold_path();

// Scan if the rest of the buffer is NUL. This is technically incorrect as any
// interior NUL should fail, but this implementation is defensive in case NULs
// are filled in anywhere else.
while let Some(next) = self.iter.next() {
if next != 0 {
self.complete = true;
return Some(Err(InteriorNulError));
}
}

// If there aren't any valid u16s past the current NUL, then the original
// buffer is NUL capped and therefore valid.
self.complete = true;
Some(Ok(0))
}
None => {
self.complete = true;
Some(Ok(0))
}
}
}
}

impl FusedIterator for EncodeWideForFfi<'_> {}

#[derive(Debug)]
pub struct InteriorNulError;

impl core::error::Error for InteriorNulError {}

impl fmt::Display for InteriorNulError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "interior NULs are invalid for FFI")
}
}

pub struct Wtf8Chunks<'a> {
wtf8: &'a Wtf8,
}
Expand Down
Loading