Skip to content

Commit c49f3a8

Browse files
committed
Preserve Windows CRT errno and avoid bare-drive prefix fallback
getpath: on Windows, default_prefix falls back to the executable directory instead of a bare "C:" when landmark search fails. host_env: errno_io_error carries the raw CRT errno as an io::Error payload (CrtErrno) instead of translating it to a Win32 error code and back. The round-trip collapsed every errno outside the errno_to_winerror table to EINVAL and attached a spurious winerror to the OSError. posix_errno recovers the exact errno from the payload; ftruncate reuses the same path. fstat now reports ERROR_INVALID_HANDLE for an invalid fd.
1 parent 150990e commit c49f3a8

4 files changed

Lines changed: 80 additions & 24 deletions

File tree

crates/host_env/src/crt_fd.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -355,9 +355,8 @@ pub fn ftruncate(fd: Borrowed<'_>, len: Offset) -> io::Result<()> {
355355
cfg_select! {
356356
windows => {
357357
if ret != 0 {
358-
// _chsize_s returns errno directly, convert to Windows error code
359-
let winerror = crate::os::errno_to_winerror(ret);
360-
return Err(io::Error::from_raw_os_error(winerror));
358+
// _chsize_s returns errno directly; preserve it exactly.
359+
return Err(crate::os::io_error_from_errno(ret));
361360
}
362361
}
363362
_ => cvt(ret)?,

crates/host_env/src/fileutils.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,15 @@ pub mod windows {
9494

9595
// _Py_fstat_noraise in cpython
9696
pub fn fstat(fd: crt_fd::Borrowed<'_>) -> std::io::Result<StatStruct> {
97-
let h = crt_fd::as_handle(fd);
98-
if h.is_err() {
99-
unsafe { SetLastError(ERROR_INVALID_HANDLE) };
100-
}
101-
let h = h?;
97+
let h = match crt_fd::as_handle(fd) {
98+
Ok(h) => h,
99+
Err(_) => {
100+
// An invalid fd is reported as a Win32 handle error so the
101+
// OSError carries winerror = ERROR_INVALID_HANDLE.
102+
unsafe { SetLastError(ERROR_INVALID_HANDLE) };
103+
return Err(std::io::Error::last_os_error());
104+
}
105+
};
102106
let h = h.as_raw_handle();
103107
// reset stat?
104108

crates/host_env/src/os.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,53 @@ impl ErrorExt for io::Error {
377377
}
378378
#[cfg(windows)]
379379
fn posix_errno(&self) -> i32 {
380+
// A C runtime error carries its exact errno as the payload; report it
381+
// directly instead of round-tripping through a Win32 error code.
382+
if let Some(crt) = self.get_ref().and_then(|e| e.downcast_ref::<CrtErrno>()) {
383+
return crt.0;
384+
}
380385
let winerror = self.raw_os_error().unwrap_or(0);
381386
winerror_to_errno(winerror)
382387
}
383388
}
384389

390+
/// Wraps a raw C runtime `errno` inside an [`io::Error`].
391+
///
392+
/// CRT functions (`open`, `read`, `dup`, ...) report failures through `errno`,
393+
/// not `GetLastError`. Translating that `errno` into a Win32 error code is
394+
/// lossy — any value missing from [`errno_to_winerror`] collapses to `EINVAL` —
395+
/// and also attaches a spurious `winerror` to the resulting `OSError`. Carrying
396+
/// the `errno` as the error payload lets [`ErrorExt::posix_errno`] recover it
397+
/// exactly while leaving `raw_os_error()` empty, so no `winerror` is reported.
398+
#[cfg(windows)]
399+
#[derive(Debug)]
400+
struct CrtErrno(i32);
401+
402+
#[cfg(windows)]
403+
impl core::fmt::Display for CrtErrno {
404+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
405+
match crate::errno::strerror_string(self.0) {
406+
Some(msg) => f.write_str(&msg),
407+
None => write!(f, "os error {}", self.0),
408+
}
409+
}
410+
}
411+
412+
#[cfg(windows)]
413+
impl core::error::Error for CrtErrno {}
414+
415+
/// Build an [`io::Error`] that preserves a raw C runtime `errno`.
416+
///
417+
/// The [`io::ErrorKind`] is derived from the closest Win32 mapping so callers
418+
/// matching on `kind()` keep working, while the exact `errno` is preserved for
419+
/// [`ErrorExt::posix_errno`].
420+
#[cfg(windows)]
421+
#[must_use]
422+
pub fn io_error_from_errno(errno: i32) -> io::Error {
423+
let kind = io::Error::from_raw_os_error(errno_to_winerror(errno)).kind();
424+
io::Error::new(kind, CrtErrno(errno))
425+
}
426+
385427
#[cfg(all(not(windows), not(target_arch = "wasm32")))]
386428
impl ErrorExt for rustix::io::Errno {
387429
fn posix_errno(&self) -> i32 {
@@ -394,9 +436,7 @@ impl ErrorExt for rustix::io::Errno {
394436
#[cfg(windows)]
395437
#[must_use]
396438
pub fn errno_io_error() -> io::Error {
397-
let errno: i32 = get_errno();
398-
let winerror = errno_to_winerror(errno);
399-
io::Error::from_raw_os_error(winerror)
439+
io_error_from_errno(get_errno())
400440
}
401441

402442
#[cfg(not(windows))]

crates/vm/src/getpath.rs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,30 @@ pub fn init_path_config(settings: &Settings) -> Paths {
180180
paths
181181
}
182182

183-
/// Get default prefix value
184-
fn default_prefix() -> String {
185-
std::option_env!("RUSTPYTHON_PREFIX")
186-
.map(String::from)
187-
.unwrap_or_else(|| {
188-
if cfg!(windows) {
189-
"C:".to_owned()
190-
} else {
191-
"/usr/local".to_owned()
192-
}
193-
})
183+
/// Get default prefix value used when landmark search fails.
184+
///
185+
/// A compile-time `RUSTPYTHON_PREFIX` always wins. Otherwise POSIX uses the
186+
/// conventional install prefix, while Windows has no meaningful compile-time
187+
/// prefix and falls back to the executable's directory (ref: getpath.py).
188+
///
189+
/// A bare drive root must never be returned on Windows: pip walks up from
190+
/// `<prefix>/Lib/site-packages` and would otherwise probe the drive root for
191+
/// writability, which fails for standard users (see issue #8246).
192+
fn default_prefix(exe_dir: Option<&PathBuf>) -> String {
193+
if let Some(prefix) = std::option_env!("RUSTPYTHON_PREFIX") {
194+
return prefix.to_owned();
195+
}
196+
197+
if cfg!(windows) {
198+
if let Some(dir) = exe_dir {
199+
return dir.to_string_lossy().into_owned();
200+
}
201+
// Executable directory is unknown; use a valid absolute root as a last
202+
// resort rather than a drive-relative bare "C:".
203+
"C:\\".to_owned()
204+
} else {
205+
"/usr/local".to_owned()
206+
}
194207
}
195208

196209
/// Detect virtual environment by looking for pyvenv.cfg
@@ -262,7 +275,7 @@ fn calculate_prefix(exe_dir: Option<&PathBuf>, build_prefix: Option<&PathBuf>) -
262275
}
263276

264277
// 4. Fallback to default
265-
default_prefix()
278+
default_prefix(exe_dir)
266279
}
267280

268281
/// Calculate exec_prefix
@@ -410,7 +423,7 @@ mod tests {
410423

411424
#[test]
412425
fn default_prefix_basic() {
413-
let prefix = default_prefix();
426+
let prefix = default_prefix(None);
414427
assert!(!prefix.is_empty());
415428
}
416429
}

0 commit comments

Comments
 (0)