Skip to content

posix descriptor and dir_fd forms, and the clamped ceiling #1071 left behind - #1078

Merged
youknowone merged 26 commits into
mainfrom
rewrite-tracer
Aug 7, 2026
Merged

posix descriptor and dir_fd forms, and the clamped ceiling #1071 left behind#1078
youknowone merged 26 commits into
mainfrom
rewrite-tracer

Conversation

@youknowone

@youknowone youknowone commented Aug 6, 2026

Copy link
Copy Markdown
Owner

The descriptor and dir_fd forms of the posix entry points that advertise
them, plus the one ratio ceiling #1071 did not reach.

Rebased onto origin/main after #1071 landed. Four commits from the earlier
revision are gone because #1071 supersedes them: spectral_norm's stated
floor (main states it at 0.25 from the runners' 0.5x–4.3x span, against my 0.4
from a partial reading), seven of the eight ceilings this branch had raised,
and a type_immutable_reject N-sizing that contradicts the shape main landed.

posix — the capability sets and what stands behind them

os.supports_fd / supports_dir_fd / supports_follow_symlinks are what
callers like shutil.copystat and shutil.rmtree read to choose an
fd-relative implementation over a path-based one. A bit set where the entry
point rejects the argument sends the caller down a route that cannot work; a
bit clear where the entry point implements it costs the caller a fallback it
did not need.

  • chown already reached fchownat, but with AT_FDCWD hard-coded and
    dir_fd rejected with NotImplementedError one step earlier. The descriptor
    the caller names is now what the name resolves against.
  • HAVE_FCHOWNAT and HAVE_UTIMENSAT join _have_functions. Neither was
    listed, so os.py put chown and utime in neither supports_dir_fd nor
    supports_follow_symlinks while both entry points implemented the modifiers.
    HAVE_LCHOWN / HAVE_LUTIMES stay unlisted for the reason HAVE_FUTIMES
    already carried: os.py reads them as the same capability their *at sibling
    provides, and neither lchown nor lutimes is called here.
  • Earlier in the branch: the seven supports_fd names that rejected an integer,
    os.truncate (advertised and doing nothing), fstat on a Windows descriptor,
    and getcwd decoded with the filesystem handler.

This is not only a missed fallback. Two lib-python tests move from failing to
passing, with no other row moving across 599 tests in the two modules:

  • test_os.UtimeTests.test_utime_invalid_arguments asserted
    NotImplementedError for follow_symlinks=False because utime was absent
    from supports_follow_symlinks — and pyre did not raise.
  • test_shutil.TestMisc.test_chown hit the NotImplementedError from
    shutil.chown(dir_fd=...).

Two parity scripts pin the surface against CPython 3.14. os_supports_fd.py
calls every member of that set with a descriptor and observes the result rather
than trusting the return value; os_supports_dir_fd.py does the same for the
other two sets, with every dir_fd call repeated against a name that does not
exist under the descriptor — which is what separates "resolved against dir_fd"
from "ignored it and reached the same file through the cwd".

bench — the ceiling #1071 left behind

getframe_force_cancel_journal crossed its ceiling of 29 on the ubuntu
runner's cranelift arm in both main CI runs on record (31.5x and 32.1x in the
median-3 fail detail, 30.8x/32.6x in the tables), and #1071 widened the rest of
the clamped family without touching it. It is held to #1071's own rule: ceiling
twice the slowest ratio a runner reported (32.6x), floor half the fastest
(8.0x), both stated because the derived floor would sit above the measurement.

One observation for a future adjustment: pickle_ctor_args's new header states
a span of 18.6x–70.3x, but the windows runner read 101.5x (table 103.0x) at
fcf997da0e0. 145 still clears it, so nothing fails — but that reading is the
windows regression tracked separately, and widening the ceiling on top of a
regression is not done here.

Verification

  • pyre/extra_tests/parity_tests/run.py — 193 scripts, cpython 3.14 / dynasm /
    cranelift all OK, runner exit code 0.
  • python3 pyre/check.py — zero ratio or perf gate failures.
  • The run's remaining red is jit-stats deviations. They are not from this
    branch: a control binary built from origin/main's posix source reproduces six
    representative deviations with identical counts (bridges_compiled 18 -> 8; guard_failures 3610 -> 1605 etc.), so they are this host's known deviation
    family. Nothing was re-recorded — improvements and bridges_compiled
    regressions are mixed, and the cause has to be settled before a baseline
    moves.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Expanded POSIX filesystem support for descriptor-based and directory-relative operations.
    • Added support for confstr, waitid, lockf, device-number helpers, sparse-file seeking, and additional platform constants.
    • Improved symlink handling, path validation, and Windows file status operations.
    • Added nullable path-or-descriptor handling and clearer argument errors.
  • Bug Fixes

    • Improved virtual object field access during optimization.
    • Preserved filesystem path encoding details and strengthened temporary-file cleanup.
  • Tests

    • Added comprehensive cross-platform coverage for filesystem, process, path, and platform-specific behavior.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49ef9ddd-2499-4aba-bb52-f8427bd9cea9

📥 Commits

Reviewing files that changed from the base of the PR and between aaefe84 and ea93329.

📒 Files selected for processing (23)
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • pyre/check.py
  • pyre/extra_tests/parity_tests/os_call_effects.py
  • pyre/extra_tests/parity_tests/os_chflags_mknod.py
  • pyre/extra_tests/parity_tests/os_chmod_modifiers.py
  • pyre/extra_tests/parity_tests/os_confstr.py
  • pyre/extra_tests/parity_tests/os_constants.py
  • pyre/extra_tests/parity_tests/os_dir_fd_modifiers.py
  • pyre/extra_tests/parity_tests/os_lockf_waitid_seek.py
  • pyre/extra_tests/parity_tests/os_module_surface.py
  • pyre/extra_tests/parity_tests/os_non_ascii_path.py
  • pyre/extra_tests/parity_tests/os_path_argument_types.py
  • pyre/extra_tests/parity_tests/os_process_group.py
  • pyre/extra_tests/parity_tests/os_spawn_family.py
  • pyre/extra_tests/parity_tests/os_stat_file_descriptor.py
  • pyre/extra_tests/parity_tests/os_supports_dir_fd.py
  • pyre/extra_tests/parity_tests/os_supports_fd.py
  • pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
  • pyre/extra_tests/parity_tests/oserror_winerror_syscall.py
  • pyre/extra_tests/parity_tests/winreg_key_lifecycle.py
  • pyre/pyre-interpreter/src/gateway.rs
  • pyre/pyre-interpreter/src/host_seam.rs
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs

Walkthrough

The change expands POSIX and Windows os compatibility with descriptor, dir_fd, modifier, constant, process, and path handling. It adds extensive parity tests and cleanup logic. It also folds unwritten virtual fields to typed zero constants and makes wasm_float_tol keyword-only.

Changes

POSIX compatibility

Layer / File(s) Summary
Platform capabilities and path contracts
pyre/pyre-interpreter/src/module/posix/interp_posix.rs, pyre/pyre-interpreter/src/gateway.rs, pyre/pyre-interpreter/src/host_seam.rs
Capability flags, platform constants, sandbox exports, path validation, nullable paths, and Windows modifier handling are updated.
Descriptor-relative filesystem operations
pyre/pyre-interpreter/src/module/posix/interp_posix.rs
Descriptor-aware filesystem, metadata, directory, process, timestamp, configuration, and device APIs are implemented.
POSIX API parity validation
pyre/extra_tests/parity_tests/*
Parity tests cover descriptor and dir_fd operations, modifiers, constants, process APIs, path types, errors, and platform-specific behavior.
Parity test cleanup
pyre/extra_tests/parity_tests/*
Temporary test directories and registry hive directories receive recursive cleanup handling.

Optimizer and API contracts

Layer / File(s) Summary
Unwritten virtual field folding
majit/majit-metainterp/src/optimizeopt/virtualize.rs
Unwritten fields on virtual and virtual-struct objects fold to typed zero constants. Raw reads and header fields retain their existing handling.
Benchmark argument contract
pyre/check.py
wasm_float_tol is now keyword-only in Check.run_bench.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ParityTests
  participant interp_posix_rs
  participant HostFilesystem
  ParityTests->>interp_posix_rs: call descriptor or dir_fd operation
  interp_posix_rs->>interp_posix_rs: validate platform capability and modifiers
  interp_posix_rs->>HostFilesystem: execute filesystem operation
  HostFilesystem-->>interp_posix_rs: return result or error
  interp_posix_rs-->>ParityTests: return value or exception
Loading

Possibly related PRs

Poem

A rabbit checks each descriptor,
And tests each path with care.
Virtual fields fold to zero,
While POSIX APIs grow fair.
“OK,” says the bunny, “everywhere!”

🚥 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 clearly identifies the POSIX descriptor and dir_fd changes and the related performance ceiling adjustment.
Docstring Coverage ✅ Passed Docstring coverage is 93.33% 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 rewrite-tracer

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 Aug 6, 2026

Copy link
Copy Markdown

🤖 Codex parity review

Static analysis of this diff vs the local RPython/PyPy sources (commit ea93329).
Updated: 2026-08-07T03:49:00.804Z

Files in the reviewed diff
majit/majit-metainterp/src/optimizeopt/virtualize.rs
pyre/check.py
pyre/extra_tests/parity_tests/os_call_effects.py
pyre/extra_tests/parity_tests/os_chflags_mknod.py
pyre/extra_tests/parity_tests/os_chmod_modifiers.py
pyre/extra_tests/parity_tests/os_confstr.py
pyre/extra_tests/parity_tests/os_constants.py
pyre/extra_tests/parity_tests/os_dir_fd_modifiers.py
pyre/extra_tests/parity_tests/os_lockf_waitid_seek.py
pyre/extra_tests/parity_tests/os_module_surface.py
pyre/extra_tests/parity_tests/os_non_ascii_path.py
pyre/extra_tests/parity_tests/os_path_argument_types.py
pyre/extra_tests/parity_tests/os_process_group.py
pyre/extra_tests/parity_tests/os_spawn_family.py
pyre/extra_tests/parity_tests/os_stat_file_descriptor.py
pyre/extra_tests/parity_tests/os_supports_dir_fd.py
pyre/extra_tests/parity_tests/os_supports_fd.py
pyre/extra_tests/parity_tests/os_utime_pathconf_truncate.py
pyre/extra_tests/parity_tests/oserror_winerror_syscall.py
pyre/extra_tests/parity_tests/winreg_key_lifecycle.py
pyre/pyre-interpreter/src/gateway.rs
pyre/pyre-interpreter/src/host_seam.rs
pyre/pyre-interpreter/src/module/posix/interp_posix.rs

1. Regressions to PyPy parity introduced by this patch

None.

2. Other mismatches introduced by this patch

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:5595 ↔ pypy/module/posix/interp_posix.py:2697: chflags places follow_symlinks in its positional parameter list, accepting chflags(path, flags, False). PyPy declares chflags(path, flags, *, follow_symlinks=True); the modifier must be keyword-only.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:1335 ↔ pypy/module/posix/interp_posix.py:2555: major/minor convert the argument to libc::dev_t, while PyPy unwraps it as c_uint and then applies intmask. This changes both accepted range and negative/large-value behavior.

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

None.

4. Structural adaptations

  • pyre/pyre-interpreter/src/gateway.rs:1709 ↔ pypy/module/posix/interp_posix.py:188: PyPy accepts readable buffer objects as paths with a deprecation warning; the patch accepts only bytes. This is explicitly a Python 3.14 compatibility adaptation, where that deprecated path form is removed.

  • pyre/pyre-interpreter/src/gateway.rs:1722 ↔ pypy/module/posix/interp_posix.py:201: a raising __index__ propagates in the Rust implementation, while PyPy catches it and continues to __fspath__. This is an intentional Python 3.14 behavior adaptation.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:5458 ↔ pypy/module/posix/interp_posix.py:3016: Rust returns None from successful lockf; PyPy returns the underlying integer result (0). This is an intentional CPython 3.14 surface adaptation.

  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs:5074 ↔ pypy/module/posix/interp_posix.py:1722: Rust adds waitid and waitid_result; PyPy explicitly marks waitid as missing. This is a Python-version surface extension rather than a direct PyPy port.

@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

https://github.com/youknowone/pyre/blob/548796e4de403ff25e98508988c90b1612a951c2/pyre-interpreter/src/module/posix/interp_posix.rs#L4680
P1 Badge Avoid borrowing unchecked user descriptors

When the caller supplies a closed descriptor or an invalid value other than -1 through dir_fd, unwrap_fd accepts it and this unsafe BorrowedFd::borrow_raw asserts that it denotes a live resource for the borrow's lifetime. Safe Python input can therefore violate Rust's unsafe contract instead of reaching fchownat and deterministically raising EBADF; preserve the upstream raw-fd syscall shape rather than constructing a BorrowedFd from unvalidated input. The new descriptor branches for chown and chmod repeat the same issue.

AGENTS.md reference: AGENTS.md:L231-L233


https://github.com/youknowone/pyre/blob/548796e4de403ff25e98508988c90b1612a951c2/pyre-interpreter/src/module/posix/interp_posix.rs#L1910
P2 Badge Preserve signed timestamps in the fd utime path

For the newly accepted descriptor form, valid pre-epoch inputs such as os.utime(fd, ns=(-1, -1)) never reach futimens: the shared converters above represent timestamps as unsigned Duration values and reject every negative value. Both CPython and upstream's signed seconds/nanoseconds representation accept these timestamps, so the advertised supports_fd implementation remains incomplete; retain signed timespec components when dispatching to futimens and utimensat.

AGENTS.md reference: AGENTS.md:L231-L233

ℹ️ 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".

@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/check.py`:
- Line 1982: Update the parameter list of the enclosing function around
wasm_float_tol to insert a keyword-only separator before wasm_float_tol, keeping
the preceding threshold arguments positional and requiring keyword arguments for
wasm_float_tol and min_pypy_ratio. Check all call sites to ensure neither
optional performance argument is passed positionally.

In `@pyre/extra_tests/parity_tests/os_supports_dir_fd.py`:
- Around line 102-105: Update the composed os.utime test around the call using
dir_fd and follow_symlinks=False to write and assert a distinct whole-second
link timestamp instead of LINK_MTIME + 1. Preserve the target timestamp
assertion and ensure the new value differs from the preceding link timestamp
setup.
- Around line 46-51: Register temporary-directory cleanup immediately after each
mkdtemp call using atexit.register(shutil.rmtree, d, ignore_errors=True). In
pyre/extra_tests/parity_tests/os_supports_dir_fd.py (lines 46-51), clean up d
and its contents after the directory is created; in
pyre/extra_tests/parity_tests/os_supports_fd.py (lines 19-22), apply the same
registration so both normal completion, assertion failures, and the Windows
early exit clean up the directory.

In `@pyre/pyre-interpreter/src/module/posix/interp_posix.rs`:
- Around line 2676-2685: Update HAVE_FUTIMENS and HAVE_UTIMENSAT to require the
host_env feature, matching the host_env-dependent capability checks used by the
related fd-relative calls. Keep the existing unix and sandbox conditions, and
ensure these constants do not advertise functionality when rustpython-host_env
is unavailable.
- Around line 4165-4177: Update truncate_length_w to convert the parsed length
with libc::off_t::try_from instead of an unchecked cast, returning an
OverflowError when the value cannot fit off_t. Preserve the helper’s existing
exact “Python int too large to convert to C long” error message and retain
successful conversions unchanged.
🪄 Autofix

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: 845d08a1-ed7e-4eeb-b01b-06662260494f

📥 Commits

Reviewing files that changed from the base of the PR and between 1de95e0 and 548796e.

📒 Files selected for processing (16)
  • pyre/bench/synth/attr_instance_shadows_class.py
  • pyre/bench/synth/getframe_force_cancel_journal.py
  • pyre/bench/synth/getframe_while_subwalk_decline_shapes.py
  • pyre/bench/synth/pickle_ctor_args.py
  • pyre/bench/synth/type_call_inline_init_branch_deopt.py
  • pyre/bench/synth/type_dict_surrogate.py
  • pyre/bench/synth/type_immutable_reject.cranelift.jitstats
  • pyre/bench/synth/type_immutable_reject.dynasm.jitstats
  • pyre/bench/synth/type_immutable_reject.py
  • pyre/bench/synth/type_immutable_reject.wasm.jitstats
  • pyre/bench/synth/type_metatype_data_descr.py
  • pyre/bench/synth/unary_positive_resume.py
  • pyre/check.py
  • pyre/extra_tests/parity_tests/os_supports_dir_fd.py
  • pyre/extra_tests/parity_tests/os_supports_fd.py
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs

Comment thread pyre/check.py Outdated
Comment thread pyre/extra_tests/parity_tests/os_supports_dir_fd.py
Comment thread pyre/extra_tests/parity_tests/os_supports_dir_fd.py
Comment on lines +2676 to 2685
/// `utime` reaches `futimens` and `utimensat` through `libc` rather than
/// `host_env`, so it needs one less condition than the rest.
const HAVE_FUTIMENS: bool = cfg!(all(unix, not(feature = "sandbox")));
/// The name form of `utime` is one `utimensat`, so the same bit carries
/// both of its modifiers: `dir_fd` is the descriptor the name resolves
/// against and `follow_symlinks=False` is `AT_SYMLINK_NOFOLLOW`.
const HAVE_UTIMENSAT: bool = cfg!(all(unix, not(feature = "sandbox")));
/// `rposix.HAVE_FSTATAT` — what `DirFD` is parameterised on
/// (`interp_posix.py:612,660`), and what `_have_functions` advertises.
/// (`interp_posix.py:612,660`). `stat_at` calls `fstatat` through `libc`.
const HAVE_FSTATAT: bool = cfg!(all(unix, not(feature = "sandbox")));

Copy link
Copy Markdown

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether rustpython-host_env and its `posix` module are gated on the `host_env` feature.
set -euo pipefail

# Locate the interpreter manifest and inspect the dependency + feature table.
fd -t f 'Cargo.toml' pyre | while IFS= read -r manifest; do
  if rg -q 'rustpython-host_env|host_env' "$manifest"; then
    echo "=== $manifest ==="
    rg -n -C 4 'rustpython-host_env|^\[features\]|^host_env' "$manifest"
  fi
done

# Check whether the `posix` module itself is feature-gated inside the host_env crate.
fd -t f 'lib.rs' --exec rg -n -C 3 'pub mod posix|cfg\(feature\s*=\s*"host_env"\)' {} \;

# Confirm every call site of set_file_times_at and its surrounding cfg.
rg -nP -C 6 'set_file_times_at\s*\(' --type=rust

Repository: youknowone/pyre

Length of output: 1940


🏁 Script executed:

#!/bin/bash
# Inspect how set_file_times_at is called and whether HAVE_FUTIMENS/HAVE_UTIMENSAT affect fd/dir_fd paths.
set -euo pipefail
echo "=== Candidate files ==="
fd -t f 'interp_posix.rs|Cargo.toml' .
echo "=== set_file_times_at usages with context ==="
rg -n -C 8 'set_file_times_at|utime_fd|utime' --type=rust | sed -n '1,220p'
echo "=== HAVE_FUTIMENS/HAVE_UTIMENSAT usages ==="
rg -n -C 6 'HAVE_FUTIMENS|HAVE_UTIMENSAT' --type=r
echo "=== Relevant Cargo dependency section ==="
sed -n '1,120p' pyre/pyre-interpreter/Cargo.toml || true

Repository: youknowone/pyre

Length of output: 1546


🏁 Script executed:

#!/bin/bash
# Inspect utime implementation and HAVE_UTIMENSAT advertising.
set -euo pipefail
echo "=== interp_posix.rs outline area containing utime/constants ==="
wc -l pyre/pyre-interpreter/src/module/posix/interp_posix.rs
rg -n -C 12 'set_file_times_at|utime_fd|utime|HAVE_FUTIMENS|HAVE_UTIMENSAT' pyre/pyre-interpreter/src/module/posix/interp_posix.rs
echo "=== Feature gating of rustpython-host_env ==="
sed -n '1,120p' pyre/pyre-interpreter/Cargo.toml

Repository: youknowone/pyre

Length of output: 18782


🏁 Script executed:

#!/bin/bash
# Check whether `utime_impl` path has cfg attributes on the `rustpython_host_env` call itself.
python3 - <<'PY'
from pathlib import Path
p = Path("pyre/pyre-interpreter/src/module/posix/interp_posix.rs")
s = p.read_text()
start = s.index("    fn utime_impl(args: &[PyObjectRef])")
end = s.index("    crate::module_ns_store(", start)
func = s[start:end]
for i, line in enumerate(func.splitlines(), 1):
    if "rustpython_host_env::posix::set_file_times_at" in line:
        prev = func.splitlines()[i-2] if i > 1 else ""
        print(f"line {i} from utime_impl: {line.strip()}")
        print(f"previous from utime_impl: {prev.strip()}")
        print("contains cfg before actual call:", "cfg(" in prev)
PY

Repository: youknowone/pyre

Length of output: 362


Keep H*UTIMENS conditional on host_env.

HOST_POSIX includes host_env, and the Rust dependencies make rustpython-host_env optional behind the host_env feature. Since utime calls rustpython_host_env::posix::set_file_times_at when dir_fd/follow_symlinks=False is used, advertising HAVE_FUTIMENS and HAVE_UTIMENSAT without host_env can expose capabilities that are not usable. Use the same host_env-dependent condition here as for the related fd-relative calls, or guard the rustpython_host_env path with its feature.

🤖 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 2676 -
2685, Update HAVE_FUTIMENS and HAVE_UTIMENSAT to require the host_env feature,
matching the host_env-dependent capability checks used by the related
fd-relative calls. Keep the existing unix and sandbox conditions, and ensure
these constants do not advertise functionality when rustpython-host_env is
unavailable.

Comment on lines +4165 to +4177
/// `space.int_w` over the `r_longlong` half of `interp_posix.py:404`.
#[cfg(all(unix, not(feature = "sandbox")))]
fn truncate_length_w(obj: PyObjectRef) -> Result<libc::off_t, crate::PyError> {
let w_length = crate::baseobjspace::space_index(obj)?;
let length = crate::baseobjspace::int_w(w_length).map_err(|err| {
if err.kind == crate::PyErrorKind::OverflowError {
crate::PyError::overflow_error("Python int too large to convert to C long")
} else {
err
}
})?;
Ok(length as libc::off_t)
}

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Determine the declared target set and whether any 32-bit target is built.
set -euo pipefail

# Declared targets in CI and build config.
fd -t f -e yml -e yaml . .github --exec rg -n -C 2 'target|--target' {} \; 2>/dev/null || true
fd -t f 'config.toml' .cargo --exec cat {} \; 2>/dev/null || true

# Other off_t conversions already in the tree, to see the established convention.
rg -nP -C 3 'as\s+libc::off_t|off_t::try_from' --type=rust

Repository: youknowone/pyre

Length of output: 10998


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant Rust helper and nearby call path without running repository code.
sed -n '4130,4225p' pyre/pyre-interpreter/src/module/posix/interp_posix.rs

# Inspect the RPython/PyPy source referenced by the parity check, if present.
sed -n '380,425p' pyre/pyre-interpreter/src/module/posix/interp_posix.py 2>/dev/null || true

# Find integer-size helpers/usages to understand whether usize/off_t conversions are checked.
rg -n -C 2 '#\[cfg\(.*32.*\)|usize::try_from|libc::off_t::try_from|off_t::try_from|as\s+libc::off_t|libc::ssize_t::try_from|Py_ssize_t|c::Py_ssize_t' --type=rust pyre pyre-interpreter 2>/dev/null || true

# Check crate metadata and declared targets without installing/building.
for f in Cargo.toml Cargo.lock pyre-interpreter/Cargo.toml .cargo/config.toml .github/workflows/*.yml; do
  test -f "$f" || continue
  echo "===== $f ====="
  rg -n -C 2 'targets\s*=|target\s*=|--target|target-arch|i686|i386|386|x86_64|wasm32|offset|off_t|os\.truncate|ftruncate' "$f" || true
done

Repository: youknowone/pyre

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the RPython Python source for the length validation semantics if included.
printf '===== interp_posix.py around truncate =====\n'
sed -n '380,425p' pyre/pyre-interpreter/src/module/posix/interp_posix.py

# Check whether os.truncate/ftruncate tests exist and what lengths they use.
printf '\n===== truncate/ftruncate tests =====\n'
rg -n -C 3 'truncate|ftruncate|2\*\*\{40\}|2\*\*40|big int|2\*\*\(' tests pyre/pyre-interpreter pyre/test 2>/dev/null || true

# Find all Rust ftruncate truncation helpers and whether any use try_from/off_t checks.
printf '\n===== precise ftruncate helpers =====\n'
rg -n -C 2 'ftruncate.*as\s+libc::off_t|libc::off_t::try_from|try_from\(.*ftruncate|size as libc::off_t|length as libc::off_t' --type=rust

Repository: youknowone/pyre

Length of output: 295


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '===== locate interp_posix.py =====\n'
fd -t f 'interp_posix\.py$' . || true

python3 - <<'PY'
import pathlib, re
for p in pathlib.Path('.').rglob('interp_posix.py'):
    print(p)
PY

printf '\n===== inspect truncat helpers in posix modules =====\n'
for f in $(fd -t f 'interp_posix\.py$' .); do
  echo "--- $f ---"
  rg -n -C 4 'truncate|ftruncate|O_CLOEXEC|OFF_T|_longlong|r_longlong|OFF64_L' "$f" || true
done

printf '\n===== precise ftruncate helpers in Rust =====\n'
rg -n -C 2 'ftruncate.*as\s+libc::off_t|libc::off_t::try_from|try_from\(.*ftruncate|size as libc::off_t|length as libc::off_t' --type=rust

Repository: youknowone/pyre

Length of output: 5122


🌐 Web query:

Python os.truncate integer length too large for platform off_t raises OverflowError documentation

💡 Result:

The OverflowError raised by Python's os.truncate (and os.ftruncate) when an integer length argument is too large occurs because the length must be converted to the C-level off_t type, which is platform-dependent and has a fixed size (often 32-bit on older or specific 32-bit systems) [1][2][3]. When the Python integer passed as the length exceeds the maximum value representable by the platform's C off_t type, Python raises an OverflowError [3][4]. This behavior is a byproduct of the underlying operating system's system call implementation—specifically, that the C function (e.g., truncate or ftruncate) expects an off_t argument [3]. Key points regarding this behavior: 1. Platform Dependency: The constraint is determined by the C compiler and the platform's architecture. On systems where off_t is 32-bit, you cannot truncate a file to a size larger than approximately 2 GiB [5][6]. On systems with 64-bit off_t (standard on modern 64-bit systems), this limit is significantly higher [1][2]. 2. Historical Context: While Python handles arbitrarily large integers, functions that interface directly with C system calls must often perform a narrow conversion to C-compatible types [5][7]. OverflowError is sometimes used in these instances when the value exceeds what the C-level type can accommodate [4][7]. 3. Current Status: There have been discussions within the Python development community about mitigating these OverflowErrors to provide more consistent behavior (e.g., potentially raising an alternative error or handling values differently), but because the underlying C system call is restricted by the platform, raising an error remains the standard response when the OS cannot support the requested operation [3][4]. If you encounter this error, it typically indicates that you are working on a platform or file system configuration that lacks "large file support" (LFS) or is running on a 32-bit architecture where the OS itself cannot handle files of the requested size [2]. Ensure that your environment is configured for large file support, which usually involves ensuring that _FILE_OFFSET_BITS=64 is set during the compilation of the Python interpreter on affected platforms [2].

Citations:


Reject lengths that do not fit off_t before passing them to ftruncate.

len >= 2**40 passes int_w, but on 32-bit off_t builds length as libc::off_t wraps and ftruncate truncates to the wrong size. os.truncate/os.ftruncate should raise OverflowError for values that do not fit the C argument type; use libc::off_t::try_from(length) and keep this helper’s exact error message for parity.

🐛 Proposed fix
             })?;
-            Ok(length as libc::off_t)
+            libc::off_t::try_from(length).map_err(|_| {
+                crate::PyError::overflow_error("Python int too large to convert to C long")
+            })
         }
📝 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
/// `space.int_w` over the `r_longlong` half of `interp_posix.py:404`.
#[cfg(all(unix, not(feature = "sandbox")))]
fn truncate_length_w(obj: PyObjectRef) -> Result<libc::off_t, crate::PyError> {
let w_length = crate::baseobjspace::space_index(obj)?;
let length = crate::baseobjspace::int_w(w_length).map_err(|err| {
if err.kind == crate::PyErrorKind::OverflowError {
crate::PyError::overflow_error("Python int too large to convert to C long")
} else {
err
}
})?;
Ok(length as libc::off_t)
}
/// `space.int_w` over the `r_longlong` half of `interp_posix.py:404`.
#[cfg(all(unix, not(feature = "sandbox")))]
fn truncate_length_w(obj: PyObjectRef) -> Result<libc::off_t, crate::PyError> {
let w_length = crate::baseobjspace::space_index(obj)?;
let length = crate::baseobjspace::int_w(w_length).map_err(|err| {
if err.kind == crate::PyErrorKind::OverflowError {
crate::PyError::overflow_error("Python int too large to convert to C long")
} else {
err
}
})?;
libc::off_t::try_from(length).map_err(|_| {
crate::PyError::overflow_error("Python int too large to convert to C long")
})
}
🤖 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 4165 -
4177, Update truncate_length_w to convert the parsed length with
libc::off_t::try_from instead of an unchecked cast, returning an OverflowError
when the value cannot fit off_t. Preserve the helper’s existing exact “Python
int too large to convert to C long” error message and retain successful
conversions unchanged.

@youknowone youknowone changed the title posix descriptor and dir_fd forms, and check.py gates that describe a measurement posix descriptor and dir_fd forms, and the clamped ceiling #1071 left behind Aug 6, 2026

@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: d3e70b74bc

ℹ️ 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".

// `inheritable=False`; nothing here forks between the open
// and the close, but the descriptor is still not the
// caller's to inherit.
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };

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 Release the interpreter lock while opening truncate paths

When path names a FIFO with no reader, this O_WRONLY open blocks while retaining the interpreter lock, so another Python thread cannot run to open the FIFO's read end and the process deadlocks. The existing os.open implementation explicitly executes this syscall through call_external_function because FIFO opens may wait; route this through that implementation, as upstream truncate does, or use the same blocking guard and EINTR handling here.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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: 1

🤖 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/module/posix/interp_posix.rs`:
- Around line 4208-4224: Update the truncate path around the direct libc::open
call to invoke open through crate::module::thread::call_external_function,
matching the existing posix.open implementation, so the interpreter is released
during blocking I/O. Add EINTR retry handling for the open operation before
returning an error, while preserving O_WRONLY|O_CLOEXEC and the existing close
and ftruncate_retry flow.
🪄 Autofix

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: 7e97eb36-5a90-4306-a0b0-f17be1a0eb10

📥 Commits

Reviewing files that changed from the base of the PR and between 48fb6c6 and f1e960b.

📒 Files selected for processing (4)
  • majit/majit-metainterp/src/optimizeopt/virtualize.rs
  • pyre/extra_tests/parity_tests/os_supports_dir_fd.py
  • pyre/extra_tests/parity_tests/os_supports_fd.py
  • pyre/pyre-interpreter/src/module/posix/interp_posix.rs

Comment on lines +4208 to +4224
let c_path = std::ffi::CString::new(path.as_bytes.as_slice())
.map_err(|_| crate::PyError::value_error("embedded null in path"))?;
// `open(space, w_path, os.O_WRONLY)` is `rposix.open` with
// `inheritable=False`; nothing here forks between the open
// and the close, but the descriptor is still not the
// caller's to inherit.
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(io_err_with_filename(
std::io::Error::last_os_error(),
path.w_path(),
));
}
let truncated = ftruncate_retry(fd, length);
unsafe { libc::close(fd) };
truncated.map_err(|e| io_err_with_filename(e, path.w_path()))?;
Ok(pyre_object::w_none())

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 | 🔵 Trivial | ⚡ Quick win

Route the open in truncate through call_external_function and retry EINTR.

Line 4214 calls libc::open directly. posix.open (Line 1346) wraps the same syscall in crate::module::thread::call_external_function, which releases the interpreter around a call that can block on a FIFO or a slow device. This site keeps the interpreter blocked for the whole open. The call also has no EINTR handling, while the following ftruncate_retry does, so a signal during the open surfaces as InterruptedError.

♻️ Proposed change
-                    let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
-                    if fd < 0 {
-                        return Err(io_err_with_filename(
-                            std::io::Error::last_os_error(),
-                            path.w_path(),
-                        ));
-                    }
+                    let fd = loop {
+                        let (fd, errno) = crate::module::thread::call_external_function(|| unsafe {
+                            libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC)
+                        });
+                        if fd >= 0 {
+                            break fd;
+                        }
+                        crate::builtins::eintr_retry_with(
+                            std::io::Error::from_raw_os_error(errno),
+                            |e| io_err_with_filename(e, path.w_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
let c_path = std::ffi::CString::new(path.as_bytes.as_slice())
.map_err(|_| crate::PyError::value_error("embedded null in path"))?;
// `open(space, w_path, os.O_WRONLY)` is `rposix.open` with
// `inheritable=False`; nothing here forks between the open
// and the close, but the descriptor is still not the
// caller's to inherit.
let fd = unsafe { libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(io_err_with_filename(
std::io::Error::last_os_error(),
path.w_path(),
));
}
let truncated = ftruncate_retry(fd, length);
unsafe { libc::close(fd) };
truncated.map_err(|e| io_err_with_filename(e, path.w_path()))?;
Ok(pyre_object::w_none())
let c_path = std::ffi::CString::new(path.as_bytes.as_slice())
.map_err(|_| crate::PyError::value_error("embedded null in path"))?;
// `open(space, w_path, os.O_WRONLY)` is `rposix.open` with
// `inheritable=False`; nothing here forks between the open
// and the close, but the descriptor is still not the
// caller's to inherit.
let fd = loop {
let (fd, errno) = crate::module::thread::call_external_function(|| unsafe {
libc::open(c_path.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC)
});
if fd >= 0 {
break fd;
}
crate::builtins::eintr_retry_with(
std::io::Error::from_raw_os_error(errno),
|e| io_err_with_filename(e, path.w_path()),
)?;
};
let truncated = ftruncate_retry(fd, length);
unsafe { libc::close(fd) };
truncated.map_err(|e| io_err_with_filename(e, path.w_path()))?;
Ok(pyre_object::w_none())
🤖 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 4208 -
4224, Update the truncate path around the direct libc::open call to invoke open
through crate::module::thread::call_external_function, matching the existing
posix.open implementation, so the interpreter is released during blocking I/O.
Add EINTR retry handling for the open operation before returning an error, while
preserving O_WRONLY|O_CLOEXEC and the existing close and ftruncate_retry flow.

@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: f1e960b8a9

ℹ️ 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 +1781 to +1782
access: std::time::Duration,
modified: std::time::Duration,

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 Keep descriptor timestamps signed

For the newly enabled descriptor form, representing both timestamps as std::time::Duration makes every pre-epoch value unrepresentable: os.utime(fd, (-1, -2)) and os.utime(fd, ns=(-1, -2)) are valid on POSIX, but the shared conversion rejects them with ValueError before this helper reaches futimens. Upstream preserves signed seconds/nanoseconds through parse_utime_args and writes them directly to timespec; use an equivalent signed representation rather than routing the new fd path through Duration.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

@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: bae92a610d

ℹ️ 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".

));
}
let truncated = ftruncate_retry(fd, length);
unsafe { libc::close(fd) };

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 Propagate close failures from truncate

When the descriptor opened for a path reports a delayed writeback error such as EIO or ENOSPC from close, this unconditional discard makes os.truncate(path, length) report success even though the filesystem reported failure. PyPy's interp_posix.py:429-431 calls close() in the finally block and therefore propagates that error; preserve the close result here rather than ignoring it.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

.map_err(|e| io_err_with_filename(e, path.w_path()))?
{
let limit = if path.as_fd != -1 {
host_posix::fpathconf(path.as_fd, name).map_err(|e| io_err(e, ""))?

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 Return -1 for indeterminate descriptor limits

When the newly accepted descriptor is queried for an indeterminate limit (for example PC_ASYNC_IO or PC_SYMLINK_MAX on Linux), host_posix::fpathconf represents the successful -1/unchanged-errno result as None, and the shared branch below converts that to Python None. os.pathconf(fd, name) must return the integer -1, as PyPy's descriptor branch passes the result directly to space.newint; otherwise callers of this newly advertised fd form receive the wrong type and value.

AGENTS.md reference: AGENTS.md:L231-L233

Useful? React with 👍 / 👎.

youknowone added a commit that referenced this pull request Aug 6, 2026
…rectory cleanup

`run_bench`'s `wasm_float_tol` and `min_pypy_ratio` are now keyword-only. Ruff
reports FBT002 for the first; no call site passed either positionally.

Ten parity scripts called `tempfile.mkdtemp` and never removed the result, so
every run of the suite left a directory behind — three runners over 196 scripts.
`atexit.register(shutil.rmtree, ...)` covers the early `raise SystemExit` on
Windows and every assertion failure without wrapping each script in a
`try`/`finally`. The #1078 review named the two in that diff; the other eight
are the same line.

os_supports_dir_fd.py's composed dir_fd + follow_symlinks call wrote
`LINK_MTIME + 1`, one nanosecond past a whole second, against a file that
states two lines above that every timestamp is a whole second so a coarse
filesystem still reads back what was written. It also could not tell "the call
did nothing" from "the value rounded down", because the step before it had
already written `LINK_MTIME`. It writes a second whole-second pair instead.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 6, 2026
Four answers from the #1078 review that were the wrong value rather than the
wrong call. `pathconf`'s fifth rides with the conf* tables, which it shares a
helper with.

`truncate` opened the name it was given with a bare `libc::open`. The name can
be a FIFO with no reader, and the call waited there holding the interpreter, so
no other thread could reach the other end; it also reported an interrupted open
as `InterruptedError` where the `ftruncate` beside it retried. It now goes
through `call_external_function` under the same retry loop, which is what
`interp_posix.py:418` reaches by opening through the module's own `open`. The
close is no longer discarded: `interp_posix.py:429-431` closes in a `finally`,
through a `close` that raises, so a writeback error the close is first to see is
the caller's. The truncation's own failure still wins when both fail.

`truncate_length_w` narrowed to `off_t` with an `as` cast. A length wider than
`off_t` became a different length rather than an error, and the file was
truncated to that; `off_t::try_from` reports it with the message the helper
already had for a too-wide value.

`utime` carried both timestamps as `std::time::Duration`, which has no second
below the epoch, so every pre-epoch time was refused with "timestamp out of
range". `rposix.futimens` and `rposix.utimensat` (`rposix.py:2634-2671`) keep
the seconds and the nanoseconds apart and signed; this does the same, with the
floor-division `_PyTime_ObjectToTimespec` applies, so `ns=(-1, -1)` is
`(-1, 999999999)` and reads back as `-1`. The name form now calls `utimensat`
directly rather than `rustpython_host_env::posix::set_file_times_at`, whose
signature cannot carry a negative second — which is also what
`interp_posix.rs:3136` already said it did. The Windows host call still counts
upwards from the epoch, and turns a pre-epoch time away rather than writing a
different one.

`times` is accepted as a keyword. `interp_posix.py:1862` puts `__kwonly__`
after `w_times`, so it is the one argument here a caller may spell either way,
and it was positional-only.

extra_tests/parity_tests/os_utime_pathconf_truncate.py pins these against
CPython 3.14, together with pathconf's answer.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 6, 2026
`BorrowedFd::borrow_raw` documents one value it may not be given: `-1`, which
the standard library reserves as the niche that makes `Option<BorrowedFd>` cost
nothing. Ten call sites built one straight out of an `fd` argument the caller
supplied, and `os.fchmod(-1, 0o644)`, `os.chown(p, -1, -1, dir_fd=-1)` and
`os.sendfile(-1, ...)` are all reachable from Python.

`fd_borrow` answers those with the `EBADF` the syscall would have answered
with, so the observable behaviour is unchanged and the one integer that may not
become a handle no longer does. The sites reading a descriptor the module
itself just produced — `dup2`'s result, and `path.as_fd` past its own `!= -1`
guard — are left alone; those are not caller values.

Reported in the #1078 review.

Assisted-by: Claude
youknowone added a commit that referenced this pull request Aug 6, 2026
Both were noop stubs: `confstr_names` answered `None` rather than a dict, and
`confstr` answered `None` whatever it was asked.

`posixmodule.c posix_constants_confstr` and `rposix.py:2248-2300` name the same
candidate set, every entry `#ifdef`-guarded, so a host publishes exactly the
names its own `<unistd.h>` defines. `libc` carries `_CS_PATH` and nothing else,
and the two numberings disagree from that first entry on — 1 on the Apple
targets, 0 in glibc's `bits/confname.h`, whose enum also restarts twice, at
1000 and at 1100. Both tables are written out for that reason: 17 names on
Darwin, 27 on glibc. The ten the candidate set carries for the System V hosts
are defined by neither header, so neither table has them.

The tables were derived by crossing `posix_constants_confstr` with each host's
header rather than written from memory. The same derivation over the macOS SDK
reproduces CPython 3.14's own `os.confstr_names` on this host entry for entry,
which is what says the method is right. The glibc half comes from that project's
`bits/confname.h`, and every value in it is confirmed a second time by `libc`:
it carries `_CS_PATH` 0, `_CS_GNU_LIBC_VERSION` 2 and `_CS_GNU_LIBPTHREAD_VERSION`
3 outright, and it puts `_CS_POSIX_V6_ILP32_OFF32_CFLAGS` at 1116 — one past the
end of the XBS5 run, which is what fixes that run at 1100..1115. No Linux
compile was reached; this branch has none available.

The call is `rposix.confstr` (`rposix.py:2129-2143`): ask for the length, fill a
buffer of exactly that size. A zero length is either a name the host has no
string for, which is `None`, or one it does not know, which is the errno it
set — so errno is cleared before the question is put. The length counts the
terminator and the string does not, so `len - 1` bytes are decoded, the way
`os_confstr_impl` does; `rffi.charp2strn(buf, n)` keeps it. The value can be a
search path, so it is decoded through the filesystem handler.

`confname_arg` now takes the table to resolve against, and the dict-building it
shares with `pathconf_names` is one function.

`pathconf` and `fpathconf` come with it, because the fix lands in the lines that
refactor touched. Both answered `None` where the host has no determinate limit;
`interp_posix.py:2433` hands whatever `pathconf` returned to `space.newint`, so
the answer is the number `-1`. `PC_ASYNC_IO` and `PC_SYMLINK_MAX` are the names
that reach it on hosts that do not implement them, and `None` is neither that
value nor a type a caller can compare against a limit. Reported in the #1078
review; pinned by extra_tests/parity_tests/os_utime_pathconf_truncate.py.

The sandbox build refuses `confstr`: the answer is the host's own search path
among other strings, and `pathconf`, `fpathconf` and `sysconf` are refused
there beside it.

extra_tests/parity_tests/os_confstr.py checks what holds on any host that has
the call — the names resolve, the values are distinct, CS_PATH is a real search
path, and an unknown name is refused.

Assisted-by: Claude

@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: 460580b343

ℹ️ 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".

let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
let name = name.to_bytes();
if name != b"." && name != b".." {
names.push(name.to_vec());

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 readdir metadata for fd scandir entries

For the newly supported os.scandir(fd) form, retaining only each entry's name discards the d_type and inode that readdir already returned. Consequently, if an entry is removed or replaced after the scan, DirEntry.is_file()/is_dir() and inode() perform a fresh fstatat and return false, raise, or describe the replacement, whereas PyPy's W_DirEntry stores known_type and inode from the directory entry. Carry those fields into the created DirEntry objects instead of reducing entries to names.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

crate::PyError::overflow_error(
"Python int too large to convert to C long",
)
})? as libc::id_t;

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 Reject waitid IDs that do not fit id_t

On platforms such as 64-bit Linux where id_t is 32-bit, this cast silently truncates any Python integer that fits i64 but not id_t. For example, os.waitid(os.P_PID, 2**32 + child_pid, os.WEXITED) can operate on and reap child_pid, while CPython rejects the argument with OverflowError; use a checked conversion before issuing the syscall.

Useful? React with 👍 / 👎.

/// modifier. `os.py:118` reads it as `chmod` honouring `dir_fd`; `os.py:179`
/// deliberately does *not* read it for `follow_symlinks`, because a host can
/// have `fchmodat` and still not honour `AT_SYMLINK_NOFOLLOW`.
const HAVE_FCHMODAT: bool = cfg!(all(unix, not(feature = "sandbox")));

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 Gate HAVE_FCHMODAT on the host_env implementation

In a Unix --no-default-features build, this evaluates to true even though the real chmod override is compiled only inside the #[cfg(all(unix, feature = "host_env"))] block; the initial no-op chmod placeholder therefore remains installed. os.py will put chmod in supports_dir_fd, but os.chmod(path, mode, dir_fd=fd) then silently returns without changing the file. Either compile the libc implementation outside host_env or include host_env in this capability condition.

Useful? React with 👍 / 👎.

/// apart. `os.py:189` reads it a second time as `stat` honouring
/// `follow_symlinks`; where this bit is false, `MS_WINDOWS` carries that
/// second claim instead (`os.py:192`).
const HAVE_LSTAT: bool = HAVE_FSTATAT;

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 Keep HAVE_LSTAT advertised in sandbox builds

In the sandbox configuration, HAVE_FSTATAT is false, but the sandbox branch of stat_path still implements follow_symlinks=False through host_seam::ops::lstat, and stat is not overwritten by the sandbox stubs. Tying HAVE_LSTAT to HAVE_FSTATAT therefore removes os.stat from supports_follow_symlinks despite the modifier working, causing callers to choose unnecessary or incorrect fallbacks; preserve the independently supported HAVE_LSTAT capability.

AGENTS.md reference: AGENTS.md:L231-L232

Useful? React with 👍 / 👎.

…hat rejected one

os.chdir, os.chmod, os.chown, os.pathconf, os.statvfs and os.utime each
appeared in os.supports_fd and answered a descriptor with

    TypeError: expected str, bytes or os.PathLike object, not int

They now unwrap through `gateway::fsencode_path_or_fd_w` and branch on
`Path.as_fd` where `interp_posix.py` does: chdir to fchdir (:910-918),
chmod to fchmod (:1228-1243), chown to fchown (:2475-2500), pathconf to
fpathconf (:2420-2433), statvfs to fstatvfs (:704-719) and utime to
futimens (:1860-1904). lchown keeps `allow_fd=0` and so keeps the
narrower allowed-type list in its own type error.

chown and utime reject follow_symlinks=False beside a descriptor, and
utime rejects dir_fd, with the ValueErrors 3.14 words; `interp_posix.py`
spells the first of them "cannnot" and words utime's dir_fd conflict
"can't specify both dir_fd and fd", where 3.14 says "can't specify dir_fd
without matching path". The parity suite reads CPython as the oracle.

_have_functions is now built from the same constants the entry points
branch on, so the advertisement cannot drift from the behaviour: the
family drops out under sandbox and on the hosts carrying no
host_env::posix, rather than each being spelled twice. HAVE_FUTIMES
leaves the list because nothing here calls futimes, and os.py:150-151
reads either bit as the same utime capability.

Assisted-by: Claude
os.truncate was one of the noop placeholders registered so os.py's
_exists probes find a name, so it returned None and left the file
untouched — on a path and on a descriptor alike — while HAVE_FTRUNCATE
advertised it in os.supports_fd. os.ftruncate did the work all along:

    with open(p, "wb") as f: f.write(b"0123456789")
    os.truncate(p, 4);   os.stat(p).st_size   # 10
    os.ftruncate(fd, 1); os.stat(fd).st_size  # 1

interp_posix.py:414-431 takes a descriptor as it stands and opens a name
write-only, truncates whichever it ended up with, and closes only the one
it opened. The EINTR retry loop and the length conversion move out of the
os.ftruncate closure so both callers share them; the path form reports
the name it opened, which the descriptor form has none of.

Assisted-by: Claude
`os.getcwd()` built its str with `to_string_lossy`, so a directory whose
name carries a byte with no UTF-8 reading came back with U+FFFD in place
of it and no longer named the directory it came from.
interp_posix.py:906 is `space.fsdecode(getcwdb(space))`, whose
surrogateescape round-trips.

`gateway::fsdecode_os_str` and `fsdecode_filename_bytes` are the same
decoders sys.argv goes through, so the two boundaries agree.

Not exercised end to end here: APFS refuses to create a directory whose
name is not valid UTF-8 (OSError errno 92), so the case needs a Linux
host.

Reported by the Codex parity review on #1066 (section 3).

Assisted-by: Claude
The set is what callers read to choose an fd-relative implementation over
a path-based one, so a member that rejects an integer — or accepts one
and does nothing — sends the caller down a route that cannot work.

The script asserts the eight-name floor before the per-name blocks, so a
capability that is dropped rather than fixed fails here instead of
quietly shrinking the coverage of the guarded blocks below it. Each name
is then called with a descriptor and the result observed rather than the
return value trusted: chdir moves the process, chmod and utime are read
back through stat, truncate shrinks the file.

It also pins the two type errors path_or_fd emits — the caller-named list
widens with the descriptor form, so lchown answers "string, bytes or
os.PathLike" where chown answers "string, bytes, os.PathLike or integer"
— the two ValueErrors a descriptor beside dir_fd or follow_symlinks
raises, and that utime's times/ns conflict outranks both.

listdir, scandir and execve are skipped: the first two take a nullable
path and word their type error differently, and execve takes an argv.

Assisted-by: Claude
…MENSAT

chown already reached `fchownat`, but with `AT_FDCWD` hard-coded and the
dir_fd keyword rejected with NotImplementedError one step earlier; the
descriptor the caller names is now what the name resolves against.
`_DirFD_Unavailable.unwrap` (interp_posix.py:285-292) converts before it
reports the platform, so the value is unwrapped first and the availability
bit consulted second. A descriptor path plus dir_fd is the ValueError
interp_posix.py:2481-2483 raises, checked ahead of the follow_symlinks one
it already had.

Neither macro was in `_have_functions`, so os.py put chown and utime in
neither supports_dir_fd (os.py:119,133) nor supports_follow_symlinks
(os.py:180,191) while both entry points implemented the modifiers —
utime through one `utimensat`, chown through one `fchownat`. HAVE_LCHOWN
and HAVE_LUTIMES stay unlisted for the reason HAVE_FUTIMES already carried:
os.py reads them as the same capability and neither `lchown` nor `lutimes`
is called here.

Two lib-python tests move from failing to passing against the same corpus
otherwise unchanged (dynasm, 599 tests across the two modules, no other
row moves):

  test_os.UtimeTests.test_utime_invalid_arguments — asserted
    NotImplementedError for `follow_symlinks=False` because utime was
    absent from supports_follow_symlinks, and pyre did not raise
  test_shutil.TestMisc.test_chown — shutil.chown(dir_fd=...) hit the
    NotImplementedError

Assisted-by: Claude
…laims them

The companion to os_supports_fd.py for the other two capability sets. It
asserts the chown/stat/utime floor in both supports_dir_fd and
supports_follow_symlinks, then calls each with the modifier and reads the
result back rather than trusting the return value.

Every dir_fd call is repeated against a name that does not exist under the
descriptor, which is what separates "resolved the name against dir_fd" from
"ignored it and reached the same file through the cwd" — a fixture whose
relative name also resolves from the process cwd cannot tell the two apart.
follow_symlinks=False is read back through lstat with the target asserted
unmoved, the two modifiers are then used together, and the three ValueErrors
a descriptor path produces are pinned by message.

The last block is what the second set buys a caller: shutil.copystat with
follow_symlinks=False substitutes `_nop` (shutil.py:435-439) for any name
the set does not carry, so before this the call silently copied nothing.

Assisted-by: Claude
optimize_getfield_gc answered GETFIELD_GC on a virtual only when the trace
had already stored that field; an unset field fell through to PassOn and the
load was emitted. virtualize.py:184-193 substitutes
optimizer.new_const(fielddescr) when opinfo.getfield returns None.

Port that fallback. typeptr, w_class and the GETFIELD_RAW_* opcodes stay out
of it: the first two are header fields the same function already resolves
from class identity, and upstream defines this handler for
GETFIELD_GC_{I,R,F} only.

pytraceback.rs:462 reads an exception's traceback slot before writing it, so
every raise emitted that load and the arg-forcing pass materialized the
exception behind it. type_immutable_reject's compiled loop body goes from 62
ops with 8 allocations to 19 ops with none.

wasm's exception_value_op_caught baseline drops to guard_failures=1, the
value dynasm and cranelift already record.

Assisted-by: Claude
… MS_WINDOWS makes

`("HAVE_LSTAT", true)` was the one hardcoded entry in a table whose every
other bit is the condition its entry point branches on. os.py reads that
bit twice: :118 `_add("HAVE_LSTAT", "lstat")` builds supports_dir_fd, and
:189 `_add("HAVE_LSTAT", "stat")` builds supports_follow_symlinks. On
Windows HAVE_FSTATAT is false, so `os.lstat(name, dir_fd=fd)` raises while
the set still listed it:

    os.supports_dir_fd  # {<built-in function lstat>} on windows

HAVE_LSTAT now follows HAVE_FSTATAT, which is the call both spellings
resolve a dir_fd with. That drops os.stat from supports_follow_symlinks
wherever the bit goes false, and os.py offers MS_WINDOWS (:192) as the
other vehicle for that claim — which also claims chmod takes a descriptor
(:143) and honours follow_symlinks (:184).

So os.chmod on Windows grows the two forms the bit advertises. The
descriptor form is `host_nt::fchmod` on the handle the CRT descriptor
wraps, and dispatches with no follow_symlinks test, as
interp_posix.py:1233-1241 does; follow_symlinks=False is
`host_nt::win32_lchmod`, the name's own attributes rather than the file
the link resolves to. dir_fd stays refused, and refuses the way
`_DirFD_Unavailable.unwrap` (interp_posix.py:285-292) does — converting
the value first, so a wrongly typed one is a TypeError.

MS_WINDOWS is appended after the HAVE_* rows, the position
interp_posix.py:2854-2855 gives it.

os_supports_fd.py's win32 branch was asserting os.stat alone and exiting;
it now exercises every name the platform advertises — the round-trip
through a descriptor for chmod and truncate, follow_symlinks read back
through the attribute bit, the dir_fd rejection, and the widened
allowed-type message — and fails on any advertised name it has no probe
for.

Assisted-by: Claude
fdlistdir reads the names a descriptor holds: fdopendir on an
F_DUPFD_CLOEXEC duplicate, a readdir loop that clears errno per call, then
rewinddir before closedir so the caller's descriptor keeps its offset.
listdir returns those names as str.

scandir builds its entries from the same call when it is given a descriptor.
Each entry's path is the bare name and carries the descriptor, and stat,
inode, is_dir, is_file and is_symlink resolve that name with fstatat against
it; a failed stat names the entry. The three is_* predicates now share one
dir_entry_kind that reads S_IFMT.

gateway gains fsencode_path_or_fd_nullable_w, the nullable half of
_unwrap_path: None resolves to "." in the unwrapper rather than at each
boundary, and the allowed-type list becomes the four-way matrix, which the
DeprecationWarning now words the same way as the TypeError.

_have_functions lists HAVE_FDOPENDIR, in the position the upstream name list
gives it. The constant is HOST_POSIX, the condition fdlistdir compiles under.

os_supports_fd.py covers both descriptor forms, the None forms, and the
nullable allowed-type message.

Assisted-by: Claude
path_or_fd_w takes bytes and no longer any readable buffer. _unwrap_path's
buffer arm (interp_posix.py:188-198) accepts one with a DeprecationWarning;
3.14 completed that deprecation, so a bytearray now gets the same TypeError
every other rejected type gets.

posix.fspath carried a second copy of that arm, which is what let os.fsencode
and os.fsdecode hand a bytearray back unconverted. It also never checked what
__fspath__ answered with; both are now the gateway's rules.

The type in these messages is named by _PyType_Name — its own name without the
module that qualifies it elsewhere, so array.array is reported as array.

os_path_argument_types.py covers the rejection at every path boundary, the two
shapes of the message, and that bytes, a bytes subclass and os.PathLike still
work.

Assisted-by: Claude
The unix chmod was registered with a fixed arity of 2, so it took no keyword at
all — spelling either modifier's own default was a TypeError. It now binds its
arguments the way chown_entry does and dispatches like _chmod_path
(interp_posix.py:1254-1258): fchmodat where a name has to be resolved against a
directory descriptor or the final symlink must not be followed, plain chmod
otherwise.

A descriptor still answers before either modifier is consulted
(interp_posix.py:1233-1242). chown turns both away in that case and chmod does
not, so the two entry points differ here on purpose.

os.lchmod stops being one of the no-op stubs at the top of the module and
becomes chmod's follow_symlinks=False arm, registered only on the hosts that
carry a working lchmod.

_have_functions gains HAVE_FCHMODAT, which os.py:118 reads as chmod honouring
dir_fd, and HAVE_LCHMOD, which os.py:183 reads as chmod honouring
follow_symlinks. The second is the narrower bit for the reason os.py:159-177
gives: fchmodat can be present and still not honour AT_SYMLINK_NOFOLLOW.

os_chmod_modifiers.py exercises each claim rather than asserting it — the
dir_fd arm resolves a name that does not exist in the working directory, and
the follow_symlinks arm reads the link's mode and the target's separately.

Assisted-by: Claude
…'s to see

DirEntry's is_dir/is_file/is_symlink answered False for every stat failure.
check_mode (interp_scandir.py:319-330) answers "not this type" for ENOENT
alone — a vanished entry is better reported as not being of the asked-for kind
than as an error — and propagates the rest, named by the entry. Both arms of
dir_entry_kind now do that.

chmod reads ENOTSUP and EOPNOTSUPP on the follow_symlinks=False path as the
modifier being unavailable rather than as an OS error
(interp_posix.py:1247-1251): a host can accept AT_SYMLINK_NOFOLLOW and not
implement it, which is the same fact that makes HAVE_LCHMOD a narrower bit
than HAVE_FCHMODAT.

listdir and scandir given a descriptor report it as the failure's filename,
since it is what named the directory.

Found by the Codex parity review of this branch.

Assisted-by: Claude
None of the five resolved a name against a directory descriptor, and none
accepted the keyword at all: `open`, `mkdir` and `mkfifo` read their
arguments positionally, so a call carrying any keyword reached them as a
trailing dict and failed converting it to an integer; `rmdir`, `unlink`
and `remove` were registered with a declared arity, which turns every
keyword away.

Each now binds its positional-or-keyword prefix through `bind_path_args`
and reads `*, dir_fd=None` through `dir_fd_kwarg`, which spells
`DirFD(available)` (`interp_posix.py:274-292`): `None` and an absent
argument are the same default, and the value is converted before the
platform is reported. The name form dispatches to `openat`, `mkdirat`,
`mkfifoat` and `unlinkat` — the last with `AT_REMOVEDIR` for `rmdir`
(`rposix.py:2717-2720`).

`_have_functions` gains HAVE_MKDIRAT, HAVE_MKFIFOAT, HAVE_OPENAT and
HAVE_UNLINKAT, which `os.py:124-132` reads into `supports_dir_fd`.
HAVE_MKNODAT is not among them: `mknod` is still a placeholder that
creates nothing.

`chmod` and `chown` carried the same `dir_fd` block written out twice;
both now call `dir_fd_kwarg`.

The five path boundaries also name themselves when the argument is not a
path — `unlink: path should be string, bytes or os.PathLike, not int`.

extra_tests/parity_tests/os_dir_fd_modifiers.py exercises the modifier
resolving against a descriptor rather than the working directory, the
advertisement matching the behaviour, and the argument-list messages.

Assisted-by: Claude
The three sat in the block of noop stubs that expects a real implementation
further down and had none, so each took any argument, made no syscall and
reported success. Their callers probe for presence and believe the answer:
`shutil.copystat` (`shutil.py:467`) reaches chflags through
`lookup("chflags")`, `tempfile._resetperms` through a `try: _os.chflags`, and
`tarfile.makedev` through `hasattr(os, "mknod")` — so the flags were never
copied and no fallback ran. The stub list is also not `#[cfg]`-gated, so
`hasattr(os, 'chflags')` was true on Linux, where the interface does not exist.

`mknod(path, mode=0o600, device=0, *, dir_fd=None)` is now `mknod`/`mknodat`,
registered beside `mkfifo` in the POSIX `host_env` block.

`chflags(path, flags, follow_symlinks=True)` and `lchflags(path, flags)` are
one call whose `follow_symlinks=False` arm is the second name, registered only
on the BSD-flavoured hosts that carry the pair. `<sys/stat.h>` declares
`lchflags` on the Apple targets, where `libc` carries only `chflags` and
`fchflags`, so it is named in an `extern` block.

The three names are dropped from the noop list, so a host without the call no
longer answers `hasattr`. `os.py:112-114` guards `_add` with `fn in _globals`,
and all three consumers above probe before calling.

`_have_functions` gains HAVE_LCHFLAGS (`os.py:182` → chflags in
supports_follow_symlinks) and HAVE_MKNODAT (`os.py:126` → mknod in
supports_dir_fd).

`bind_path_args` takes the keyword-only names rather than assuming `dir_fd`,
because a signature without a keyword-only tail counts a surplus argument
differently: every argument counts against the one limit and it is always
"at most", which is why `os.lchflags(p, 0, follow_symlinks=False)` is a count
error and not an unknown keyword.

HAVE_LCHMOD was spelled without the `host_env` half of the condition its
registration carries. Both it and the new HAVE_LCHFLAGS now read
`HOST_POSIX && BSD_FLAVOURED`, which is the invariant the table's own comment
states: each bit is the condition the entry point itself is compiled under.

`mknod` reports the failing name in `filename`, following
`interp_posix.py:1360-1372` `wrap_oserror2(space, e, w_path)`; CPython's
`os_mknod_impl` uses the pathless `posix_error()` there, as it does for
`mkfifo`, which pyre already named.

Assisted-by: Claude
The three sat in the noop-stub block and answered `None`. `tarfile` reads a
node's pair out of `st_rdev` to write a header (`tarfile.py:2275-2276`) and
puts one back together to recreate the node (`:2735`), so the header field it
wrote was not a number.

They are `libc::major`/`minor`/`makedev` (`interp_posix.py:2551-2563`), which
is the host's own encoding rather than arithmetic that can be spelled
portably — macOS splits a `dev_t` 8/24 and Linux 12/20 with the minor bits in
two pieces. The device argument is narrowed to `dev_t` rather than to a C int,
because that type is wider than an int where the pair is, and a value that does
not fit reports the overflow instead of wrapping.

`libc` is a shim under `feature = "sandbox"` and carries no `dev_t`, so the
names are absent there rather than answering with another host's arithmetic.

The remaining 40 lying names in that stub list are measured and filed
separately.

Assisted-by: Claude
`os.py:881` writes the spawn family in Python over fork+exec+waitpid, guarded
by `if _exists("fork") and not _exists("spawnv") and _exists("execv")`. A
`spawnv` bound here is therefore not a placeholder waiting to be overwritten
further down — it is what stops that definition from ever running, and the
noop stub won. `fork`, `execv`, `_exit`, `waitpid`, `WIFSTOPPED` and
`waitstatus_to_exitcode` are all real, so dropping the four names is the whole
implementation.

The same block defines P_WAIT and P_NOWAIT, and the constant list above bound
both to 0 — so the two modes were equal and `os.spawnv(os.P_NOWAIT, …)` waited
instead of returning a pid. POSIX has no spawn call and no such constants;
`<process.h>` does, so they are now bound on Windows only, with the values
`_spawnv` reads.

The sandbox build drops the four names too: the spawn family is app-level code
in os.py rather than an external, and binding a name there would take P_WAIT
and P_NOWAIT with it. `fork` is a raising stub in that build, so the definition
os.py provides refuses at the fork.

extra_tests/parity_tests/os_spawn_family.py spawns a child both ways and checks
that P_WAIT hands back the exit code where P_NOWAIT hands back a pid — which is
the assertion the two modes being equal fails.

Assisted-by: Claude
The four families sat in a list that bound every name to 0, under a comment
saying zero stubs were fine for os.py init. os.py names none of them, so the
zero was serving nothing; the readers are the callers. `os._exit` takes an EX_*
straight to the exit status, `statvfs(...).f_flag` is masked with the ST_* bits,
and the RTLD_* set is handed back to `dlopen`, where a zero asks for
`RTLD_LOCAL | RTLD_LAZY` whatever the caller named. Within each family every
member also compared equal to every other.

EX_* are `<sysexits.h>` literals: `libc` binds none of them, and the header is a
verbatim descendant of the 4.3BSD one wherever it is carried, so the values are
the same on every host that has it. The rest come from `libc`, except the Apple
scheduling policies — `<pthread/pthread_impl.h>` declares those and the crate
does not mirror them.

They are bound on the POSIX builds, which is where the headers are; SCHED_BATCH
and SCHED_IDLE narrow to Linux and RTLD_DEEPBIND to glibc, as `rposix.py:296-300`
and `rdynload.py:50-82` read them. Before this they were bound unconditionally,
so the Windows and wasm builds carried names whose header their host does not
have.

`host_seam::sys` names each one, because the sandbox build reaches `libc`
through that facade rather than directly.

WNOHANG, WCONTINUED, WUNTRACED and the PRIO_* trio stay in the zero list: the
POSIX blocks further down overwrite those with the real values, and the comment
above them now says so.

extra_tests/parity_tests/os_constants.py checks each family for the shape it has
to have — the members distinct, the flag members single bits — which is what
binding a whole family to one value destroys.

Assisted-by: Claude
Seventeen names in the noop-stub list answered `None` and were never given a
body. They fall into four kinds, and none of the four is a name the module
should carry.

`fstatat`, `faccessat`, `futimens`, `futimes` and `fdopendir` are the C entry
points the calls above them are served with — `openat` and its family are how
`dir_fd` and a descriptor path are honoured, not calls of their own — and
`setenv` is the C spelling of `putenv`. `moduledef.py` publishes none of them.
A name bound for one is a capability a caller probes for and believes.

`pipe2`, `dup3` and the four scheduling-policy calls are Linux's own additions,
and were bound on every host. Nothing serves them on any host here, so they are
dropped rather than kept as a stub that reports success; the Linux bodies —
`sched_getparam` and `sched_setparam` need a `sched_param` type that does not
exist here yet — are filed as their own task.

`WEXITED`, `WNOWAIT` and `WSTOPPED` are `waitid`'s option flags, which are
numbers rather than calls. They are bound with the other wait options, from
libc.

The fourth kind is the one `spawnv` was: names os.py writes in Python and lists
in its own `__all__`. `popen` (os.py:1020-1067) and `get_exec_path`
(os.py:649) were both in the stub list; `getenv` (os.py:818-825) had a real
body here, and the SEEK_SET/SEEK_CUR/SEEK_END trio came off the constants table
that os.py fixes at 0/1/2 itself (os.py:203-206). os.py's definitions win —
unlike the spawn family, none of
these four is guarded on the name being free — so what the bindings changed was
`os.__all__`, where each arrived twice: once through `_get_exports_list` and
once through os.py's own list. `os.__all__` held six duplicates against
CPython's none; it now holds none.

`popen` leaves the sandbox build's raising-stub list for the same reason the
spawn family did: os.py builds it over `subprocess`, whose fork the stubs
beside it already refuse.

extra_tests/parity_tests/os_module_surface.py checks `os.__all__` for a name
listed twice, which is what catches this whole class at once, and checks that
each C entry point is absent and each option flag is a number.

Assisted-by: Claude
The three answered `None` from the stub list, which is a number a caller cannot
tell from a group id and a name it cannot tell from a terminal.

`getpgrp` (`interp_posix.py:2167-2172`) cannot fail and so is not checked;
`getpgid` (`:2201-2210`) can be asked about a process that is not there and
reports it. `ctermid` (`:2603-2608`) is read the way `rposix.py:1724-1728`
reads it — the call is handed a null pointer and answers the static buffer it
keeps — and the result is a filename, so it is decoded through
`fsdecode_filename_bytes` rather than assumed to be text.

`<stdio.h>` declares `ctermid` on every POSIX host, and the `libc` crate
carries it for a handful, so the declaration is spelled out where the crate has
none — the same shape `lchflags` already uses here.

The sandbox build refuses all three instead: a process group and the
controlling terminal's name are host facts, and the neighbouring reads
(`getpid`, `getppid`, `ttyname`, `tcgetpgrp`) are refused there for the same
reason.

`confstr` and `confstr_names` stay stubs. They need the `_CS_*` table, which
the `libc` crate carries one entry of per host while the module publishes 17 on
Darwin and about fifty on glibc; spelling the rest out is what `pathconf_names`
already does here, and it is filed as its own task rather than written from
values this host cannot check.

extra_tests/parity_tests/os_process_group.py checks the two group calls against
each other and the terminal name for being a path, which is what a `None` fails.

Assisted-by: Claude
…rectory cleanup

`run_bench`'s `wasm_float_tol` and `min_pypy_ratio` are now keyword-only. Ruff
reports FBT002 for the first; no call site passed either positionally.

Ten parity scripts called `tempfile.mkdtemp` and never removed the result, so
every run of the suite left a directory behind — three runners over 196 scripts.
`atexit.register(shutil.rmtree, ...)` covers the early `raise SystemExit` on
Windows and every assertion failure without wrapping each script in a
`try`/`finally`. The #1078 review named the two in that diff; the other eight
are the same line.

os_supports_dir_fd.py's composed dir_fd + follow_symlinks call wrote
`LINK_MTIME + 1`, one nanosecond past a whole second, against a file that
states two lines above that every timestamp is a whole second so a coarse
filesystem still reads back what was written. It also could not tell "the call
did nothing" from "the value rounded down", because the step before it had
already written `LINK_MTIME`. It writes a second whole-second pair instead.

Assisted-by: Claude
Four answers from the #1078 review that were the wrong value rather than the
wrong call. `pathconf`'s fifth rides with the conf* tables, which it shares a
helper with.

`truncate` opened the name it was given with a bare `libc::open`. The name can
be a FIFO with no reader, and the call waited there holding the interpreter, so
no other thread could reach the other end; it also reported an interrupted open
as `InterruptedError` where the `ftruncate` beside it retried. It now goes
through `call_external_function` under the same retry loop, which is what
`interp_posix.py:418` reaches by opening through the module's own `open`. The
close is no longer discarded: `interp_posix.py:429-431` closes in a `finally`,
through a `close` that raises, so a writeback error the close is first to see is
the caller's. The truncation's own failure still wins when both fail.

`truncate_length_w` narrowed to `off_t` with an `as` cast. A length wider than
`off_t` became a different length rather than an error, and the file was
truncated to that; `off_t::try_from` reports it with the message the helper
already had for a too-wide value.

`utime` carried both timestamps as `std::time::Duration`, which has no second
below the epoch, so every pre-epoch time was refused with "timestamp out of
range". `rposix.futimens` and `rposix.utimensat` (`rposix.py:2634-2671`) keep
the seconds and the nanoseconds apart and signed; this does the same, with the
floor-division `_PyTime_ObjectToTimespec` applies, so `ns=(-1, -1)` is
`(-1, 999999999)` and reads back as `-1`. The name form now calls `utimensat`
directly rather than `rustpython_host_env::posix::set_file_times_at`, whose
signature cannot carry a negative second — which is also what
`interp_posix.rs:3136` already said it did. The Windows host call still counts
upwards from the epoch, and turns a pre-epoch time away rather than writing a
different one.

`times` is accepted as a keyword. `interp_posix.py:1862` puts `__kwonly__`
after `w_times`, so it is the one argument here a caller may spell either way,
and it was positional-only.

extra_tests/parity_tests/os_utime_pathconf_truncate.py pins these against
CPython 3.14, together with pathconf's answer.

Assisted-by: Claude
`BorrowedFd::borrow_raw` documents one value it may not be given: `-1`, which
the standard library reserves as the niche that makes `Option<BorrowedFd>` cost
nothing. Ten call sites built one straight out of an `fd` argument the caller
supplied, and `os.fchmod(-1, 0o644)`, `os.chown(p, -1, -1, dir_fd=-1)` and
`os.sendfile(-1, ...)` are all reachable from Python.

`fd_borrow` answers those with the `EBADF` the syscall would have answered
with, so the observable behaviour is unchanged and the one integer that may not
become a handle no longer does. The sites reading a descriptor the module
itself just produced — `dup2`'s result, and `path.as_fd` past its own `!= -1`
guard — are left alone; those are not caller values.

Reported in the #1078 review.

Assisted-by: Claude
Both were noop stubs: `confstr_names` answered `None` rather than a dict, and
`confstr` answered `None` whatever it was asked.

`posixmodule.c posix_constants_confstr` and `rposix.py:2248-2300` name the same
candidate set, every entry `#ifdef`-guarded, so a host publishes exactly the
names its own `<unistd.h>` defines. `libc` carries `_CS_PATH` and nothing else,
and the two numberings disagree from that first entry on — 1 on the Apple
targets, 0 in glibc's `bits/confname.h`, whose enum also restarts twice, at
1000 and at 1100. Both tables are written out for that reason: 17 names on
Darwin, 27 on glibc. The ten the candidate set carries for the System V hosts
are defined by neither header, so neither table has them.

The tables were derived by crossing `posix_constants_confstr` with each host's
header rather than written from memory. The same derivation over the macOS SDK
reproduces CPython 3.14's own `os.confstr_names` on this host entry for entry,
which is what says the method is right. The glibc half comes from that project's
`bits/confname.h`, and every value in it is confirmed a second time by `libc`:
it carries `_CS_PATH` 0, `_CS_GNU_LIBC_VERSION` 2 and `_CS_GNU_LIBPTHREAD_VERSION`
3 outright, and it puts `_CS_POSIX_V6_ILP32_OFF32_CFLAGS` at 1116 — one past the
end of the XBS5 run, which is what fixes that run at 1100..1115. No Linux
compile was reached; this branch has none available.

The call is `rposix.confstr` (`rposix.py:2129-2143`): ask for the length, fill a
buffer of exactly that size. A zero length is either a name the host has no
string for, which is `None`, or one it does not know, which is the errno it
set — so errno is cleared before the question is put. The length counts the
terminator and the string does not, so `len - 1` bytes are decoded, the way
`os_confstr_impl` does; `rffi.charp2strn(buf, n)` keeps it. The value can be a
search path, so it is decoded through the filesystem handler.

`confname_arg` now takes the table to resolve against, and the dict-building it
shares with `pathconf_names` is one function.

`pathconf` and `fpathconf` come with it, because the fix lands in the lines that
refactor touched. Both answered `None` where the host has no determinate limit;
`interp_posix.py:2433` hands whatever `pathconf` returned to `space.newint`, so
the answer is the number `-1`. `PC_ASYNC_IO` and `PC_SYMLINK_MAX` are the names
that reach it on hosts that do not implement them, and `None` is neither that
value nor a type a caller can compare against a limit. Reported in the #1078
review; pinned by extra_tests/parity_tests/os_utime_pathconf_truncate.py.

The sandbox build refuses `confstr`: the answer is the host's own search path
among other strings, and `pathconf`, `fpathconf` and `sysconf` are refused
there beside it.

extra_tests/parity_tests/os_confstr.py checks what holds on any host that has
the call — the names resolve, the values are distinct, CS_PATH is a real search
path, and an unknown name is refused.

Assisted-by: Claude
None of the three existed. All are POSIX rather than Linux-only — the Apple
targets carry every one of them — so they were absent on a host that has them.

`lockf` is `interp_posix.py:3006-3012`: one call under the `eintr_retry` loop,
put through the call gate because `F_LOCK` waits. Its four commands are
published beside it. It answers `None`, which is what `os_lockf_impl` does;
`interp_posix.py:3012` answers the `0` the call returns on success, and 3.14 —
the oracle the parity suite reads — does not carry it.

`waitid` is one `interp_posix.py:1722` names and does not have, so the shape is
CPython 3.14's: a five-field `waitid_result` structseq, the three `P_*` id
types and the six `CLD_*` codes. A zero `si_pid` is the "nothing to report"
answer and is `None` rather than a result of zeroes. `WEXITED`, `WSTOPPED` and
`WNOWAIT` were already published.

`SEEK_HOLE` and `SEEK_DATA` are the two `whence` values beyond the three os.py
fixes itself. The hosts whose headers define them are named rather than
excluded, so a host left out is one short of a name rather than one carrying a
wrong value.

The sandbox build refuses `lockf` and `waitid`; their constants are numbers and
stay.

extra_tests/parity_tests/os_lockf_waitid_seek.py locks a region, walks a file
with both new whence values, and reports a child with WNOWAIT before reaping
it — which is the difference between waitid and waitpid.

Assisted-by: Claude
…table

The constant block that binds EX_*, ST_*, RTLD_* and SCHED_* was registered
twice: once before the `nt` constants and once after, the second copy carrying
the F_* and SEEK_HOLE/SEEK_DATA rows the first did not. Both bound the same
values, so nothing observed the duplication. The first copy is removed.

os.EX_OK is not one of the names Windows has not got — CPython answers it there
with the same 0 — so it is bound with the `nt` constants as well. Only that one
member of the family; the rest of `<sysexits.h>` stays where the header is.

os.spawnv and os.spawnve go back to being registered on Windows. The reading
that unbinds them is the POSIX one: os.py:881 writes the spawn family over
fork+exec+waitpid and does so only `if not _exists("spawnv")`, so a binding
there prevents the definition. That block is behind `_exists("fork")`, which
Windows does not have, and `nt` carries `_spawnv` itself — so on Windows the
name is the module's or it is nowhere.

host_seam::sys orders the two scheduling `pub use` lines as rustfmt does.

Assisted-by: Claude
Each of these asserted on Windows something only a POSIX host answers, and the
Windows runners are what reported it.

- os_constants.py expected the whole `<sysexits.h>` family to be absent there.
  EX_OK is not: CPython binds it on Windows too, with the same 0.
- os_spawn_family.py checked `P_NOWAIT == P_NOWAITO` before the Windows exit.
  That equality is os.py's, written in the branch that has fork; the C runtime
  numbers P_NOWAITO 3.
- os_path_argument_types.py pinned listdir's TypeError with `integer` in the
  list of types it takes. The word is there for the descriptor form, which the
  Windows build has no fdopendir to serve.
- os_utime_pathconf_truncate.py wrote a timestamp before the epoch. A FILETIME
  counts 100ns ticks, so CPython reads -1ns back as -100, and this build's
  Windows path carries the timestamp as an unsigned duration and refuses the
  range outright.

winreg_key_lifecycle.py removed the saved hive by name and then rmdir'd its
directory. The registry writes its transaction log beside the hive under names
of its own, so the directory was not empty; it is emptied instead.

Assisted-by: Claude
@youknowone
youknowone merged commit 367681f into main Aug 7, 2026
@youknowone
youknowone deleted the rewrite-tracer branch August 7, 2026 03:45
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@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

https://github.com/youknowone/pyre/blob/ea933298adeb7f6a1758188d44a3d68c32b1ba2d/pyre-interpreter/src/module/posix/interp_posix.rs#L2512
P2 Badge Preserve UTIME_NOW for descriptor defaults

When the newly supported descriptor form is called as os.utime(fd) without times or ns, this passes a userspace snapshot of the current time to futimens instead of preserving the “now” state as UTIME_NOW. On POSIX, setting both timestamps to now is permitted when the caller has write access, while supplying explicit timestamps generally requires ownership, so a writable descriptor for a file owned by another user can now fail with EPERM; carry the omitted-time state through to utime_fd and emit UTIME_NOW, as upstream do_utimens does.

AGENTS.md reference: AGENTS.md:L231-L233


https://github.com/youknowone/pyre/blob/ea933298adeb7f6a1758188d44a3d68c32b1ba2d/pyre-interpreter/src/module/posix/interp_posix.rs#L5282-L5285
P2 Badge Run signal handlers between ftruncate retries

When ftruncate is interrupted by a signal, this loop retries immediately without invoking checksignals_now; crt_call! is only a raw libc-call wrapper and does not provide the call gate claimed above. Consequently, a periodic signal whose Python handler raises or disables the timer can repeatedly interrupt the newly implemented os.truncate(path, ...) while its handler never gets a turn, delaying the exception or livelocking the call. Use the same eintr_retry_with handling used by open below before retrying.

AGENTS.md reference: AGENTS.md:L231-L233

ℹ️ 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".

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