-
Notifications
You must be signed in to change notification settings - Fork 1.5k
ffi: No interior NULs (part 1) #8245
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.rsRepository: RustPython/RustPython Length of output: 2417
🤖 Prompt for AI Agents |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 expandedRepository: 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)
PYRepository: 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.rsRepository: RustPython/RustPython Length of output: 7661 Implement the missing trait method and switch to the checked encoder. 🤖 Prompt for AI Agents |
||
|
|
||
| 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]) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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())?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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*>' cratesRepository: 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 🤖 Prompt for AI Agents |
||
| let keep = vm.ctx.new_bytes(wide_bytes); | ||
| let addr = keep.as_bytes().as_ptr() as usize; | ||
| return Ok(Argument { | ||
|
|
@@ -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 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Doc comment names the wrong encoding.
📝 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| pub fn encode_wide_ffi(&self) -> EncodeWideForFfi<'_> { | ||||||||||
| EncodeWideForFfi { | ||||||||||
| iter: self.encode_wide(), | ||||||||||
| complete: false, | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| pub const fn chunks(&self) -> Wtf8Chunks<'_> { | ||||||||||
| Wtf8Chunks { wtf8: self } | ||||||||||
| } | ||||||||||
|
|
@@ -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, | ||||||||||
| } | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
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:
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:
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_partsVec::from_raw_partsmust use the same allocation layout, including alignment. Casting theVec<u16>buffer toVec<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