Skip to content

Install the posix module as nt on Windows - #780

Merged
youknowone merged 5 commits into
mainfrom
nt-module
Jul 25, 2026
Merged

Install the posix module as nt on Windows#780
youknowone merged 5 commits into
mainfrom
nt-module

Conversation

@youknowone

@youknowone youknowone commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

On Windows there is no module literally named posix; PyPy's
pypy/module/posix/moduledef.py sets applevel_name = os.name, installing the
one posix module as nt. pyre had always registered it as posix, so
os.name was "posix" on Windows and the nt/ntpath code paths stayed
dormant. This branch registers the module as nt on Windows, adds the
Windows-only nt functions, and clears the bootstrap prerequisites the flip
surfaces.

All changes are gated #[cfg(windows)] / not(windows) (or Windows + host_env),
so unix and wasm builds are unaffected by construction.

Commits

posix: install as nt on Windows and add the nt-only calls

  • Register the posix module as nt on Windows (pyre_install_module!("nt"(posix)));
    the posix arm becomes #[cfg(all(not(wasm32), not(windows)))]. os.name
    is now "nt" and os.py selects ntpath.
  • New #[cfg(windows)] mod win_nt porting the if os.name == 'nt' moduledef
    block via raw unsafe extern "system" Win32 FFI (no windows-sys dependency,
    matching the module/_winapi pattern): _getfullpathname, _getfinalpathname,
    _getfileinformation, _getdiskusage, get_handle_inheritable,
    set_handle_inheritable, _add_dll_directory, _remove_dll_directory,
    _supports_virtual_terminal.
  • environ is populated str->str on Windows (was bytes->bytes): os.py's nt
    _create_environ_mapping requires str keys, mirroring the newtext vs
    newbytes split in PyPy _convertenviron.
  • Windows O_* flags: O_BINARY, O_TEXT, O_NOINHERIT, O_TEMPORARY,
    O_SHORT_LIVED, O_RANDOM, O_SEQUENTIAL.

winreg: register a constants-only module on Windows

  • importlib._bootstrap_external eagerly import winreg on sys.platform == "win32". Register a constants-only winreg (HKEY_*, KEY_*, REG_*) so
    bootstrap completes. WindowsRegistryFinder is deprecated and never added to
    sys.meta_path, so the RegOpenKeyEx family is a follow-up.

sys: expose sys.winver on Windows

  • site._get_path reads sys.winver on nt; expose it ("3.14").

open: eagerly create the file for write modes on the Windows open() path

  • The not(unix) open() path buffers writes in memory and only writes to
    disk on a dirty flush, so open(p, "w").close() (no write) never created the
    file and "x" did not enforce exclusivity. Create the file at open time to
    match the fd-backed unix path (W_FileIO.descr_init): "x" via create_new
    (EEXIST on exist), "a" via append+create, "w"/"w+" via a truncating
    write.

Validation

  • check.py --backend dynasm: ALL PASSED 308/308 on Windows (both PYRE_JIT=0
    and JIT on).
  • import os (os.name == "nt"), import nt, ntpath, str environ, all nt
    functions, winreg constants, tempfile, import site verified working;
    startup is clean.
  • cargo fmt --all -- --check clean.

Out of scope (pre-existing Windows gaps, not caused by this flip)

subprocess needs an msvcrt module and a fuller _winapi; missing
_socket/_bz2/_lzma/_tkinter C extensions. These fail identically before
this change and are left as follow-ups.

Summary by CodeRabbit

  • New Features
    • Improved Windows compatibility with expanded os/nt/ntpath behavior, including additional posix helpers and correct environment typing.
    • Added a winreg module with registry-related integer constants, plus Windows sys.winver.
    • Exported additional Windows open() mode flags and improved built-in module registration for early imports.
  • Bug Fixes
    • Write-mode files are now created/truncated immediately on open() (with correct w, a, and exclusive x semantics), and common OS errors are mapped to appropriate OSError values.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR makes pathname-backed writes eagerly create or truncate host files and adds Windows-specific interpreter support, including nt helpers, winreg constants, environment handling, open flags, builtin registration, and sys.winver.

Changes

Filesystem open semantics

Layer / File(s) Summary
Eager host file creation
pyre/pyre-interpreter/src/builtins.rs
Writing modes now create, truncate, append, or exclusively create pathname-backed files during open(), mapping filesystem failures to OSError.

Windows compatibility surface

Layer / File(s) Summary
Windows module contracts
pyre/pyre-interpreter/src/module/winreg/mod.rs, pyre/pyre-interpreter/src/module/mod.rs
Adds the Windows-only winreg module with registry constants and registers it in the module registry.
Windows nt APIs
pyre/pyre-interpreter/Cargo.toml, pyre/pyre-interpreter/src/module/posix/interp_posix.rs
Adds the Windows API dependency and implements Windows path, file, disk, handle, DLL-directory, terminal-support, environment, and open-mode functionality.
Windows builtin registration
pyre/pyre-interpreter/src/importing.rs, pyre/pyre-interpreter/src/module/_winapi/mod.rs
Registers winreg and nt, excludes posix on Windows, and updates _winapi platform documentation.
Windows runtime metadata
pyre/pyre-interpreter/src/module/sys/vm.rs
Defines sys.winver as "3.14" on Windows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PythonStdlib
  participant BuiltinRegistry
  participant PosixNt
  participant WindowsAPI
  PythonStdlib->>BuiltinRegistry: import nt or winreg
  BuiltinRegistry->>PosixNt: register Windows nt helpers
  PythonStdlib->>PosixNt: call path or handle helper
  PosixNt->>WindowsAPI: execute Windows operation
  WindowsAPI-->>PosixNt: result or error
  PosixNt-->>PythonStdlib: Python value or OSError
Loading

Possibly related PRs

  • youknowone/pyre#85: Adds related host_env filesystem wiring used by eager file-opening behavior.
  • youknowone/pyre#731: Modifies the builtin-module registration path involved in the Windows additions.

Poem

A rabbit taps “open,” files bloom on the floor,
Windows gains helpers and constants galore.
nt finds each pathway, winreg guards the gate,
sys.winver whispers the version’s new state.
Hop, hop—imports now know where to go!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main Windows change: registering the posix module as nt.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch nt-module

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 25, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit bc710ff).
Updated: 2026-07-25T10:14:06.483Z

Files in the reviewed diff
Cargo.lock
pyre/pyre-interpreter/Cargo.toml
pyre/pyre-interpreter/src/builtins.rs
pyre/pyre-interpreter/src/importing.rs
pyre/pyre-interpreter/src/module/_winapi/mod.rs
pyre/pyre-interpreter/src/module/mod.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
pyre/pyre-interpreter/src/module/sys/vm.rs
pyre/pyre-interpreter/src/module/winreg/mod.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/winreg/mod.rs:9 ↔ pypy/module/_winreg/moduledef.py:45 — Pyre deliberately exports only constants; PyPy exports error, HKEYType, and the complete registry API (OpenKey, QueryValue, CreateKey, CloseKey, etc.). Pyre also omits upstream’s ERROR_MORE_DATA constant.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:357 ↔ pypy/module/posix/interp_posix.py:3205_add_dll_directory returns a public integer pointer value, while PyPy returns an opaque W_DLLCapsule; consequently Pyre’s _remove_dll_directory accepts forged integers rather than rejecting any value not returned by _add_dll_directory (pyre/.../interp_posix.rs:368 ↔ pypy/.../interp_posix.py:3219).

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:351 ↔ pypy/module/posix/interp_posix.py:3203_add_dll_directory omits PyPy’s observable os.add_dll_directory audit event and accepts bytes through fsencode_w, whereas PyPy calls space.utf8_w(w_path).

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:230 ↔ pypy/module/posix/interp_posix.py:867 — newly exposed _getfullpathname/_getfinalpathname normalize path arguments through a lossy Rust String and rebuild error filenames as a new str; PyPy preserves the original path object in wrap_oserror2, including bytes and PathLike identity/type (pypy/.../interp_posix.py:880).

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:1283 ↔ pypy/module/posix/moduledef.py:184 — all newly added Windows nt functions disappear when the host_env Cargo feature is disabled. PyPy exposes its Windows module surface based on os.name == 'nt', not an optional host-I/O feature.

3. Pre-existing mismatches (already present before this patch)

  • pyre/pyre-interpreter/src/builtins.rs:11786 ↔ pypy/module/_io/interp_fileio.py:211 — non-Unix pathname open() remains an in-memory buffer wrapper rather than a live W_FileIO descriptor. PyPy retains the descriptor for the object lifetime; Pyre later flushes by pathname, so descriptor identity, replacement/unlink behavior, append semantics, and concurrent changes do not match.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:571 ↔ pypy/module/sys/moduledef.py:121 — adding sys.winver does not address the already-missing Windows-only sys.getwindowsversion and sys._vpath exports.

4. Structural adaptations

  • pyre/pyre-interpreter/src/importing.rs:537 ↔ pypy/module/posix/moduledef.py:15 — registering the Rust posix implementation under "nt" on Windows is the Rust registry equivalent of PyPy’s applevel_name = os.name; this is structurally different but parity-preserving.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:350 ↔ pypy/module/posix/interp_posix.py:3204 — direct windows-sys calls substitute for PyPy’s RPython rwin32 bindings. This is an implementation-language adaptation; the observable deviations are listed above.

  • pyre/pyre-interpreter/src/module/sys/vm.rs:553 ↔ pypy/module/sys/version.py:81 — Pyre’s 3.14 version/winver values intentionally target its CPython-compatible 3.14 surface rather than this checkout’s PyPy version baseline.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e9086cf30f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if cookie.is_null() {
return Err(last_error(&path));
}
Ok(pyre_object::w_int_new(cookie as usize as i64))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the opaque DLL-directory capsule

Return and validate the PyPy-equivalent W_DLLCapsule instead of exposing the DLL_DIRECTORY_COOKIE as an integer. As written, Python code can forge a value such as nt._remove_dll_directory(1), which is cast to a pointer and passed to RemoveDllDirectory; the Windows API only accepts cookies returned by AddDllDirectory, whereas PyPy rejects non-capsules with TypeError, so this can invoke the host API with an invalid opaque pointer.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

} else {
pyre_object::w_str_new(path)
};
crate::PyError::os_error_syscall(errno, filename)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expose the Windows error code on raised OSErrors

Preserve the raw GetLastError value and populate OSError.winerror when any of these Win32 calls fails. This helper translates the code to a POSIX errno and then calls a builder that only sets errno, so Windows recovery code that inspects exc.winerror gets AttributeError instead of the Win32 code; this notably conflicts with the checked-in ntpath error handling and with PyPy's lastSavedWindowsError/wrap_oserror2 path.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

win_nt::_getfullpathname as crate::gateway::BuiltinCodeFn,
1u16,
),
("_getfinalpathname", win_nt::_getfinalpathname, 1),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Provide the complete native realpath import surface

On Windows, lib-python/3/ntpath.py enables native realpath through one grouped import of _findfirstfile, _getfinalpathname, and readlink; a repo-wide source search finds no _findfirstfile implementation or registration. Consequently that import always raises ImportError and ntpath.realpath is defined as abspath, so Windows symlinks and junctions are never resolved despite registering _getfinalpathname here.

Useful? React with 👍 / 👎.

Comment on lines +302 to +304
let as_bytes = unsafe { pyre_object::bytesobject::is_bytes_like(arg) };
let path = crate::gateway::fsencode_w(arg)?;
Ok((path, as_bytes))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve bytes returned by a path-like object

Determine as_bytes from the result of os.fspath, not from the original wrapper object. For a custom PathLike whose __fspath__ returns bytes, this records false, so nt._getfullpathname(pathlike) returns str; PyPy explicitly resolves fspath before choosing the result type, and its checked-in test requires a bytes-returning path-like to produce bytes.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with 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.

Inline comments:
In `@pyre/pyre-interpreter/src/builtins.rs`:
- Around line 11740-11754: Update the file-opening logic in the builtins
implementation so the handle returned by OpenOptions::open for the “x” and “a”
modes is retained by the returned file wrapper instead of discarded via map(|_|
()). Populate the wrapper’s fd-backed field, such as __file_fd__, or reuse the
existing fd-backed implementation, while preserving the requested create-new and
append semantics for later write and flush operations.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 386-405: Validate argument 0 as an integer before calling
pyre_object::w_int_get_value in _getfileinformation, get_handle_inheritable,
set_handle_inheritable, and _remove_dll_directory, returning the standard
TypeError for non-integer inputs via crate::baseobjspace::int_w or the module’s
established is_int pattern. In _getfileinformation, also reject
_get_osfhandle(fd) results of -1 or -2 before invoking
GetFileInformationByHandle and return EBADF for those invalid descriptors.
- Around line 365-376: Update both failure paths around
GetFinalPathNameByHandleW to capture the Windows last-error value before calling
CloseHandle, then close the handle and construct the error from the captured
value. Preserve the existing successful-path cleanup and use the same
error-preservation pattern as disk_free.
- Around line 318-342: Update _getfullpathname to reject paths containing an
embedded NUL character before calling to_wide, matching the validation in
_getfinalpathname and _getdiskusage. Return the same ValueError behavior and
message used by those sibling functions, while preserving the existing Win32
path-resolution flow for valid paths.
- Around line 438-442: Update the parent-path computation around the cut match
to retain the separator when the matched separator is the root boundary, so
drive-root paths such as C:\file.txt produce C:\ and rooted paths such as
\file.txt produce \. Preserve the existing parent extraction for non-root nested
paths and ensure the resulting path remains valid for GetDiskFreeSpaceExW.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 980eca4a-0500-476f-8b55-1b940d80ba9a

📥 Commits

Reviewing files that changed from the base of the PR and between c38f0f2 and e9086cf.

📒 Files selected for processing (7)
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_winapi/mod.rs
  • pyre/pyre-interpreter/src/module/mod.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/module/winreg/mod.rs

Comment on lines +11740 to +11754
let create_res = if mode.contains('x') {
std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
.map(|_| ())
} else if mode.contains('a') {
std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(&path)
.map(|_| ())
} else {
std::fs::write(&path, b"")
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)builtins\.rs$|pyre-interpreter/src/builtins.rs|pyre/pyre-interpreter/src/builtins.rs' || true

echo "== target range =="
if [ -f pyre/pyre-interpreter/src/builtins.rs ]; then
  sed -n '11700,11810p' pyre/pyre-interpreter/src/builtins.rs | cat -n -v
fi

echo "== related wrappers/usages =="
rg -n "struct .*File|FdFile|OwnedFile|FileBacking|FileBacked|FileLike|open\(|write\(|fs::write|create_new|create\(\)|append\(|flush\(|close\(" pyre/pyre-interpreter/src/builtins.rs | sed -n '1,220p'

Repository: youknowone/pyre

Length of output: 10996


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file wrapper outline =="
ast-grep outline pyre/pyre-interpreter/src/builtins.rs --view expanded 2>/dev/null | rg -A 40 -B 5 "file_wrapper|FileWrapper|__file_data__|__file_pos__|flush|close|write" || true

echo "== file wrapper definitions/usages around 9700-10350 =="
sed -n '9730,10350p' pyre/pyre-interpreter/src/builtins.rs | cat -n -v

echo "== fileio write/flush/close definitions =="
sed -n '11000,11215p' pyre/pyre-interpreter/src/builtins.rs | cat -n -v

echo "== open path fd-backed definitions =="
sed -n '11620,11695p' pyre/pyre-interpreter/src/builtins.rs | cat -n -v

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant tests =="
sed -n '12605,12625p' pyre/pyre-interpreter/src/builtins.rs | cat -n -v

echo "== parse and inspect file-wrapper state storage for writing branch =="
python3 - <<'PY'
from pathlib import Path
p=Path('pyre/pyre-interpreter/src/builtins.rs')
s=p.read_text()
start=s.find('builtin_open(args: &[PyObjectRef])')
end=s.find('\n/// Compatibility entry point used by the builtin `open()` pipeline.', start)
chunk=s[start:end]
for attr in ['__file_fd__', '__file_data__', '__file_pos__', '__file_name__', '__file_mode__', '__file_binary__', 'write(true)', 'create_new(true)', 'create(true)', 'fs::write']:
    print(attr, bool(chunk.find(attr) != -1))
print('contains_write_only_open_call_mapping_to_unit=', 'OpenOptions::open(&path).map(|_| ())' in chunk or '.open(&path).\n                    .map(|_| ())' in chunk or '.open(&path)' in chunk and '.map(|_| ())' in chunk)
print('contains_writing_attr=', 'let _ = crate::baseobjspace::setattr_str(wrapper,\n            "__file_fd__",' in chunk)
PY

Repository: youknowone/pyre

Length of output: 1791


Keep the opened file handle on the returned wrapper.

OpenOptions::open(...).map(|_| ()) closes the file before open() returns, while this branch stores only __file_data__ and not __file_fd__. A later write()/flush() resolves the pathname again, so "x" can create/write a different file and "a" loses its O_APPEND descriptor semantics. Retain an owned descriptor/handle or use the fd-backed implementation.

🤖 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 `@pyre/pyre-interpreter/src/builtins.rs` around lines 11740 - 11754, Update the
file-opening logic in the builtins implementation so the handle returned by
OpenOptions::open for the “x” and “a” modes is retained by the returned file
wrapper instead of discarded via map(|_| ()). Populate the wrapper’s fd-backed
field, such as __file_fd__, or reuse the existing fd-backed implementation,
while preserving the requested create-new and append semantics for later write
and flush operations.

Comment on lines +318 to +342
pub fn _getfullpathname(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let (path, as_bytes) = arg_path(args, "_getfullpathname")?;
let wide = to_wide(&path);
unsafe {
let needed = GetFullPathNameW(
wide.as_ptr(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if needed == 0 {
return Err(last_error(&path));
}
let mut buf = vec![0u16; needed as usize];
let written =
GetFullPathNameW(wide.as_ptr(), needed, buf.as_mut_ptr(), std::ptr::null_mut());
if written == 0 || written >= needed {
return Err(last_error(&path));
}
Ok(wrap_path(
&String::from_utf16_lossy(&buf[..written as usize]),
as_bytes,
))
}
}

Copy link
Copy Markdown

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

_getfullpathname lacks the embedded-NUL check its siblings have.

_getfinalpathname and _getdiskusage both reject '\0' before to_wide, but here a path containing NUL is silently truncated at the NUL by the Win32 wide-string convention, so ntpath.abspath("evil\0/../x") resolves a different path than the caller asked for. Reject it like the others (CPython's path converter raises ValueError: embedded null character).

🛡️ Proposed fix
         let (path, as_bytes) = arg_path(args, "_getfullpathname")?;
+        if path.contains('\0') {
+            return Err(crate::PyError::value_error("embedded null character"));
+        }
         let wide = to_wide(&path);
📝 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
pub fn _getfullpathname(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let (path, as_bytes) = arg_path(args, "_getfullpathname")?;
let wide = to_wide(&path);
unsafe {
let needed = GetFullPathNameW(
wide.as_ptr(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if needed == 0 {
return Err(last_error(&path));
}
let mut buf = vec![0u16; needed as usize];
let written =
GetFullPathNameW(wide.as_ptr(), needed, buf.as_mut_ptr(), std::ptr::null_mut());
if written == 0 || written >= needed {
return Err(last_error(&path));
}
Ok(wrap_path(
&String::from_utf16_lossy(&buf[..written as usize]),
as_bytes,
))
}
}
pub fn _getfullpathname(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let (path, as_bytes) = arg_path(args, "_getfullpathname")?;
if path.contains('\0') {
return Err(crate::PyError::value_error("embedded null character"));
}
let wide = to_wide(&path);
unsafe {
let needed = GetFullPathNameW(
wide.as_ptr(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
);
if needed == 0 {
return Err(last_error(&path));
}
let mut buf = vec![0u16; needed as usize];
let written =
GetFullPathNameW(wide.as_ptr(), needed, buf.as_mut_ptr(), std::ptr::null_mut());
if written == 0 || written >= needed {
return Err(last_error(&path));
}
Ok(wrap_path(
&String::from_utf16_lossy(&buf[..written as usize]),
as_bytes,
))
}
}
🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 318 -
342, Update _getfullpathname to reject paths containing an embedded NUL
character before calling to_wide, matching the validation in _getfinalpathname
and _getdiskusage. Return the same ValueError behavior and message used by those
sibling functions, while preserving the existing Win32 path-resolution flow for
valid paths.

Comment on lines +365 to +376
let needed = GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, VOLUME_NAME_DOS);
if needed == 0 {
CloseHandle(handle);
return Err(last_error(&path));
}
let mut buf = vec![0u16; needed as usize + 1];
let written =
GetFinalPathNameByHandleW(handle, buf.as_mut_ptr(), needed, VOLUME_NAME_DOS);
CloseHandle(handle);
if written == 0 {
return Err(last_error(&path));
}

Copy link
Copy Markdown

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

CloseHandle clobbers GetLastError before last_error reads it.

Both failure paths call CloseHandle(handle) and then build the error from the live GetLastError value — a successful CloseHandle overwrites it, so the OSError carries a bogus errno (typically 0/success). The same hazard is already handled deliberately in disk_free; capture the error before closing.

🐛 Proposed fix
             let needed = GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, VOLUME_NAME_DOS);
             if needed == 0 {
+                let err = last_error(&path);
                 CloseHandle(handle);
-                return Err(last_error(&path));
+                return Err(err);
             }
             let mut buf = vec![0u16; needed as usize + 1];
             let written =
                 GetFinalPathNameByHandleW(handle, buf.as_mut_ptr(), needed, VOLUME_NAME_DOS);
+            let err = if written == 0 { Some(last_error(&path)) } else { None };
             CloseHandle(handle);
-            if written == 0 {
-                return Err(last_error(&path));
+            if let Some(err) = err {
+                return Err(err);
             }
📝 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
let needed = GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, VOLUME_NAME_DOS);
if needed == 0 {
CloseHandle(handle);
return Err(last_error(&path));
}
let mut buf = vec![0u16; needed as usize + 1];
let written =
GetFinalPathNameByHandleW(handle, buf.as_mut_ptr(), needed, VOLUME_NAME_DOS);
CloseHandle(handle);
if written == 0 {
return Err(last_error(&path));
}
let needed = GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, VOLUME_NAME_DOS);
if needed == 0 {
let err = last_error(&path);
CloseHandle(handle);
return Err(err);
}
let mut buf = vec![0u16; needed as usize + 1];
let written =
GetFinalPathNameByHandleW(handle, buf.as_mut_ptr(), needed, VOLUME_NAME_DOS);
let err = if written == 0 { Some(last_error(&path)) } else { None };
CloseHandle(handle);
if let Some(err) = err {
return Err(err);
}
🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 365 -
376, Update both failure paths around GetFinalPathNameByHandleW to capture the
Windows last-error value before calling CloseHandle, then close the handle and
construct the error from the captured value. Preserve the existing
successful-path cleanup and use the same error-preservation pattern as
disk_free.

Comment on lines +386 to +405
pub fn _getfileinformation(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
let Some(&arg) = args.first() else {
return Err(crate::PyError::type_error(
"_getfileinformation() missing required argument 'fd'",
));
};
let fd = unsafe { pyre_object::w_int_get_value(arg) } as i32;
unsafe {
let handle = _get_osfhandle(fd) as Handle;
let mut info: ByHandleFileInformation = std::mem::zeroed();
if GetFileInformationByHandle(handle, &mut info) == 0 {
return Err(last_error(""));
}
Ok(pyre_object::w_tuple_new(vec![
pyre_object::w_int_new(info.dw_volume_serial_number as i64),
pyre_object::w_int_new(info.n_file_index_high as i64),
pyre_object::w_int_new(info.n_file_index_low as i64),
]))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unvalidated w_int_get_value on the fd/handle/cookie arguments.

_getfileinformation, get_handle_inheritable, set_handle_inheritable and _remove_dll_directory all read argument 0 with pyre_object::w_int_get_value without an is_int check, so nt._remove_dll_directory("x") (or any non-int) reinterprets an unrelated object's payload as a raw handle/pointer and hands it to Win32 — RemoveDllDirectory/SetHandleInformation on a fabricated pointer, instead of a TypeError. Route these through the checked int conversion (crate::baseobjspace::int_w or an is_int guard) as the rest of the module does. Also worth rejecting _get_osfhandle(fd) == -1/-2 up front so a bad fd raises EBADF rather than ERROR_INVALID_HANDLE.

🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 386 -
405, Validate argument 0 as an integer before calling
pyre_object::w_int_get_value in _getfileinformation, get_handle_inheritable,
set_handle_inheritable, and _remove_dll_directory, returning the standard
TypeError for non-integer inputs via crate::baseobjspace::int_w or the module’s
established is_int pattern. In _getfileinformation, also reject
_get_osfhandle(fd) results of -1 or -2 before invoking
GetFileInformationByHandle and return EBADF for those invalid descriptors.

Comment on lines +438 to +442
let cut = path.rfind('\\').or_else(|| path.rfind('/'));
let parent = match cut {
Some(index) => &path[..index],
None => path.as_str(),
};

Copy link
Copy Markdown

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

Parent-directory fallback drops the root separator.

rfind cuts before the separator, so C:\file.txtC: (a drive-relative path resolved against that drive's current directory, not its root) and \file.txt"", which GetDiskFreeSpaceExW rejects. PyPy's interp_nt._getdiskusage retries against dirname, which retains the trailing separator for root-level paths.

🐛 Proposed fix
-            let cut = path.rfind('\\').or_else(|| path.rfind('/'));
+            let cut = path.rfind(['\\', '/']);
             let parent = match cut {
-                Some(index) => &path[..index],
+                // Keep the separator so a root-level path stays absolute
+                // (`\file` → `\`, `C:\file` → `C:\`).
+                Some(index) => &path[..=index],
                 None => path.as_str(),
             };
📝 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
let cut = path.rfind('\\').or_else(|| path.rfind('/'));
let parent = match cut {
Some(index) => &path[..index],
None => path.as_str(),
};
let cut = path.rfind(['\\', '/']);
let parent = match cut {
// Keep the separator so a root-level path stays absolute
// (`\file` → `\`, `C:\file` → `C:\`).
Some(index) => &path[..=index],
None => path.as_str(),
};
🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 438 -
442, Update the parent-path computation around the cut match to retain the
separator when the matched separator is the root boundary, so drive-root paths
such as C:\file.txt produce C:\ and rooted paths such as \file.txt produce \.
Preserve the existing parent extraction for non-root nested paths and ensure the
resulting path remains valid for GetDiskFreeSpaceExW.

moduledef.py `applevel_name = os.name` installs the one posix module under
`os.name`, which is `nt` on Windows where no module named `posix` exists.
Register it under `nt` there, so os.py reports `os.name == "nt"`, selects
ntpath, and runs its `os.name == "nt"` branches.

Port the Windows-only entries from moduledef.py's `if os.name == 'nt'` block
and interp_nt.py through direct Win32 FFI: _getfullpathname, _getfinalpathname,
_getfileinformation, _getdiskusage, get_handle_inheritable/set_handle_inheritable,
_add_dll_directory/_remove_dll_directory, _supports_virtual_terminal.

Populate nt.environ as str -> str (the nt branch of _create_environ_mapping
requires str keys, matching _convertenviron's newtext) and bind the Windows
open() mode flags (O_BINARY, O_TEXT, O_NOINHERIT, O_TEMPORARY, O_SHORT_LIVED,
O_RANDOM, O_SEQUENTIAL).
importlib._bootstrap_external eagerly `import winreg`s on every
sys.platform == "win32" build, so the module must exist for the import
machinery — and `import site` — to start. Its one bootstrap consumer,
WindowsRegistryFinder, is deprecated, never installed onto sys.meta_path, and
reaches winreg only inside unreached methods. Expose the integer constants
(HKEY_*, KEY_*, REG_*) a caller reads; the RegOpenKeyEx family is left for a
follow-up.
site.getusersitepackages -> _get_path reads sys.winver to form the per-user
site-packages directory on nt; without it `import site` raises AttributeError
at startup. Bind it to the "major.minor" tag ("3.14").
The not(unix) open() path buffers writes in memory and writes to disk
only on a dirty flush, so open(p, "w").close() with no write never
created the file and "x" did not enforce exclusivity. Create the file
when open() returns: "x" via create_new, "a" via append+create,
"w"/"w+" via a truncating write.
…ed FFI

The Windows nt functions declared raw `unsafe extern "system"` Win32
bindings. Delegate to rustpython_host_env::nt (getfullpathname,
getfinalpathname, getdiskusage, get/set_handle_inheritable,
supports_virtual_terminal, handle_from_fd) instead, and fall back to
windows-sys only for AddDllDirectory/RemoveDllDirectory and
GetFileInformationByHandle, which host_env does not wrap. Gate the win_nt
module and its registration on the host_env feature.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc710ff41b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +7 to +10
//! `sys.meta_path`, and touches `winreg.OpenKey`/`QueryValue` only inside its
//! (unreached) methods, so the registry-access functions are not ported here;
//! this exposes the integer constants a caller reads, leaving the RegOpenKeyEx
//! family for a follow-up.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Implement registry functions before advertising winreg

On Windows, the first call to mimetypes.guess_type() now fails with AttributeError because lib-python/3/mimetypes.py:31-34 treats the successful winreg import as registry support, init() invokes read_windows_registry(), and line 306 unconditionally calls the missing winreg.OpenKey (the _winapi module also lacks its alternative registry accelerator). Thus adding a constants-only module breaks common MIME-type lookup; port at least the registry API used by the checked-in stdlib rather than exposing a deliberately incomplete winreg.

AGENTS.md reference: AGENTS.md:L194-L196

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
pyre/pyre-interpreter/src/module/posix/interp_posix.rs (2)

252-258: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

_getfullpathname and _add_dll_directory still don't reject embedded NUL in the path.

Both build a Win32 wide string directly from path without checking for '\0' first, unlike _getfinalpathname/_getdiskusage which do. A NUL-containing path silently truncates at the embedded NUL for GetFullPathNameW/AddDllDirectory, resolving/registering a different, shorter path than the caller asked for. The _getfullpathname instance was already flagged in a prior review round.

🛡️ Proposed fix
     pub fn _getfullpathname(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
         let (path, as_bytes) = arg_path(args, "_getfullpathname")?;
+        if path.contains('\0') {
+            return Err(crate::PyError::value_error("embedded null character"));
+        }
         match host_nt::getfullpathname(Path::new(&path)) {
     pub fn _add_dll_directory(args: &[PyObjectRef]) -> Result<PyObjectRef, crate::PyError> {
         use windows_sys::Win32::System::LibraryLoader::AddDllDirectory;
         let (path, _) = arg_path(args, "_add_dll_directory")?;
+        if path.contains('\0') {
+            return Err(crate::PyError::value_error("embedded null character"));
+        }
         let wide: Vec<u16> = path.encode_utf16().chain(std::iter::once(0)).collect();

Also applies to: 349-358

🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 252 -
258, Update _getfullpathname and _add_dll_directory to reject paths containing
an embedded '\0' before calling host_nt::getfullpathname or AddDllDirectory.
Match the existing validation and error behavior used by _getfinalpathname and
_getdiskusage, ensuring NUL-containing paths fail rather than being truncated.

276-296: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unvalidated w_int_get_value on fd/handle/cookie args (still present).

_getfileinformation, get_handle_inheritable, set_handle_inheritable, and _remove_dll_directory all reinterpret argument 0 (and, for the latter, argument 1) via pyre_object::w_int_get_value without an is_int check, so a non-int argument reinterprets an unrelated object's payload as a raw handle/pointer before it reaches Win32. This mirrors a previously flagged concern on the same functions.

Also applies to: 317-328, 331-343, 361-372

🤖 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 `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs` around lines 276 -
296, The Windows handle argument paths in _getfileinformation,
get_handle_inheritable, set_handle_inheritable, and _remove_dll_directory must
validate integer arguments before conversion. Add the existing is_int/type-check
behavior for argument 0, and argument 1 where applicable, before calling
w_int_get_value; return the appropriate type error for non-integers while
preserving the current handle operations for valid integers.
🤖 Prompt for all review comments with 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.

Duplicate comments:
In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 252-258: Update _getfullpathname and _add_dll_directory to reject
paths containing an embedded '\0' before calling host_nt::getfullpathname or
AddDllDirectory. Match the existing validation and error behavior used by
_getfinalpathname and _getdiskusage, ensuring NUL-containing paths fail rather
than being truncated.
- Around line 276-296: The Windows handle argument paths in _getfileinformation,
get_handle_inheritable, set_handle_inheritable, and _remove_dll_directory must
validate integer arguments before conversion. Add the existing is_int/type-check
behavior for argument 0, and argument 1 where applicable, before calling
w_int_get_value; return the appropriate type error for non-integers while
preserving the current handle operations for valid integers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 324348a9-7a85-465e-945a-12cdd92b30b3

📥 Commits

Reviewing files that changed from the base of the PR and between e9086cf and bc710ff.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • pyre/pyre-interpreter/Cargo.toml
  • pyre/pyre-interpreter/src/builtins.rs
  • pyre/pyre-interpreter/src/importing.rs
  • pyre/pyre-interpreter/src/module/_winapi/mod.rs
  • pyre/pyre-interpreter/src/module/mod.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs
  • pyre/pyre-interpreter/src/module/sys/vm.rs
  • pyre/pyre-interpreter/src/module/winreg/mod.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant