Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 9, 2025

Summary by CodeRabbit

  • Tests

    • Windows test suite now runs previously skipped IO and OS tests.
  • Bug Fixes

    • Windows no longer exposes hidden environment entries that begin with "=" when reading the environment.
    • Improved Windows environment set/unset behavior with stricter name/value validation and length checks for more reliable updates.
    • Adjusted environment-key validation for spawning processes to better handle keys that start with "=".

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 9, 2025

Walkthrough

Removed Windows CI skips for test_io and test_os; allowed environment variable names beginning with = in nt.rs; added Windows-specific putenv/unsetenv using CRT _wputenv with wide-string length checks and adjusted non-Windows putenv/unsetenv signatures and ffi/OsStr handling in os.rs.

Changes

Cohort / File(s) Summary
CI test skip removal
\.github/workflows/ci.yaml
Removed test_io and test_os from the Windows-specific test skip list and deleted related commented guidance.
NT env-var validation
crates/vm/src/stdlib/nt.rs
Relaxed environment-variable key validation for spawnve/execve to permit a leading = by only checking for '=' after the first character; preserved embedded-null checks and other invariants; skipped names starting with = when building the Python env dict.
OS env APIs (Windows & non-Windows adjustments)
crates/vm/src/stdlib/os.rs
Added Windows-only putenv(key: PyStrRef, value: PyStrRef) and unsetenv(key: PyStrRef) using CRT _wputenv with wide-string "key=value" and check_env_var_len validation; introduced Windows key/value validation (non-empty, no embedded nulls, no '=' after first char). Updated non-Windows putenv/unsetenv signatures to use crate::function::Either<PyStrRef, PyBytesRef>, moved several ffi::CStr/ffi::CString uses to std::ffi, and annotated bytes_as_os_str as non-Windows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Inspect wide-string allocation, null-termination, and ownership around _wputenv calls in crates/vm/src/stdlib/os.rs.
  • Verify check_env_var_len / _MAX_ENV wide-character length semantics and off-by-one behavior.
  • Confirm nt.rs environment construction and the decision to skip =-prefixed vars when building the Python env dict.
  • Audit call sites for the updated non-Windows putenv/unsetenv signatures and Either<...> usage.

Possibly related PRs

Poem

🐇 I hop through envs both wide and thin,
A leading = now finds its way in.
I whisper to CRT with a wide-string cheer,
Tests wake and stretch — the morning is near.
🥕 Small changes, big hops — a rabbit’s cheer!

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and directly references the main changes (test_os and test_io on Windows), matching the key modifications in the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a3b77c and df899ea.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_ntpath.py is excluded by !Lib/**
  • Lib/test/test_unittest/testmock/testpatch.py is excluded by !Lib/**
📒 Files selected for processing (3)
  • .github/workflows/ci.yaml (0 hunks)
  • crates/vm/src/stdlib/nt.rs (3 hunks)
  • crates/vm/src/stdlib/os.rs (10 hunks)
💤 Files with no reviewable changes (1)
  • .github/workflows/ci.yaml
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/stdlib/nt.rs
  • crates/vm/src/stdlib/os.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: arihant2math
Repo: RustPython/RustPython PR: 5790
File: build.rs:2-2
Timestamp: 2025-06-28T16:31:03.991Z
Learning: In Cargo build scripts (build.rs), the environment variable CARGO_CFG_TARGET_OS is guaranteed to exist and is automatically set by Cargo during the build process, making unwrap() safe to use when accessing this variable.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
🔇 Additional comments (7)
crates/vm/src/stdlib/nt.rs (1)

127-135: Filtering out internal =‑prefixed environment entries in environ looks reasonable

Skipping keys starting with '=' (e.g., =C:, =ExitCode) avoids exposing Windows internal bookkeeping variables that cannot be manipulated via _wputenv anyway. This keeps nt.environ focused on user‑visible variables; just ensure this matches the behavior you want relative to CPython’s os.environ semantics for tests on Windows.

crates/vm/src/stdlib/os.rs (6)

115-119: Non‑Windows bytes_as_os_str helper looks correct and safely scoped

Limiting bytes_as_os_str to #[cfg(not(windows))] and delegating to rustpython_common::os::bytes_as_os_str with a clear decode error message keeps the helper aligned with existing path semantics and matches its usage sites (non‑Windows putenv/unsetenv and related helpers).


395-408: Using OsStr::from_bytes in listdir FD path is appropriate

The change to OutputMode::String.process_path(std::ffi::OsStr::from_bytes(fname), vm) for the nix::dir/FD branch cleanly round‑trips the raw fname bytes back into an OsStr via the OsStrExt trait, while still skipping "."/"..". This keeps behavior consistent with other path handling code paths on Unix.


419-491: Non‑Windows putenv/unsetenv refactor to Either<PyStrRef, PyBytesRef> looks good

The env_bytes_as_bytes helper plus the updated signatures:

  • Cleanly accept both str and bytes keys/values, matching CPython’s cross‑type behavior on POSIX.
  • Preserve existing validation (embedded null byte, empty or '='‑containing names) before converting via bytes_as_os_str.
  • Continue to call env::set_var/env::remove_var with OsStr inputs, so OS‑level behavior is unchanged.

This is a solid API cleanup and should satisfy tests that exercise byte/str combos for environment manipulation.


1038-1046: Switch to std::ffi::CString in non‑Windows stat_inner is sound

Using std::ffi::CString::new(path) and returning Ok(None) on interior‑NUL failure is a safe way to signal an invalid C pathname to the caller, which is then turned into cstring_error higher up. The rest of the fstatat/stat/lstat logic is unchanged, so behavior for valid paths remains the same.


1542-1545: strerror now using std::ffi::CStr is correct

Constructing the error string with CStr::from_ptr(libc::strerror(e)) and to_string_lossy() is the standard, safe pattern for turning strerror’s static C buffer into a Rust String. No behavioral changes beyond using the canonical type.


1641-1647: Non‑Windows device_encoding CStr usage is idiomatic

The updated branch that checks encoding.is_null() / encoding.read() == '\0' and otherwise wraps the pointer in std::ffi::CStr::from_ptr before to_string_lossy() is a correct and idiomatic way to obtain the current codeset string from nl_langinfo(CODESET). This keeps behavior consistent while using standard FFI types.


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 and usage tips.

@youknowone youknowone force-pushed the test-os-on-windows branch 3 times, most recently from 81b279c to ef41136 Compare December 10, 2025 00:44
@youknowone youknowone changed the title Test os on windows test_os, test_io on windows Dec 10, 2025
@youknowone youknowone marked this pull request as ready for review December 10, 2025 00:44
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/vm/src/stdlib/os.rs (1)

470-490: Fix error type consistency in non-Windows implementations.

The non-Windows putenv function incorrectly raises ValueError for invalid keys, but CPython's os.putenv raises OSError (like unsetenv does). Change "illegal environment variable name" error on line 483 to match unsetenv's behavior of raising an OSError with errno.

The platform-specific separation with #[cfg(not(windows))] is correctly implemented; only the error type needs alignment with CPython.

🧹 Nitpick comments (2)
crates/vm/src/stdlib/os.rs (2)

428-437: Unused helper function check_env_var_len.

This helper function is defined but never called. The new Windows putenv and unsetenv implementations (lines 455, 507) perform inline length checks instead. Consider either:

  1. Removing this dead code, or
  2. Refactoring the new functions to use this helper for consistency

Note: The inline check wide.len() > _MAX_ENV + 1 differs from what this helper would check (size > _MAX_ENV), which could lead to off-by-one discrepancies.


492-520: Duplicated key validation logic with putenv.

The key validation (lines 498-503) is identical to putenv (lines 446-452). Consider extracting this into a helper function to reduce duplication and ensure consistent validation across both functions.

+#[cfg(windows)]
+fn validate_env_key(key_str: &str, vm: &VirtualMachine) -> PyResult<()> {
+    if key_str.is_empty()
+        || key_str.get(1..).is_some_and(|s| s.contains('='))
+        || key_str.contains('\0')
+    {
+        return Err(vm.new_value_error("illegal environment variable name"));
+    }
+    Ok(())
+}
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 194e2a6 and bff45e1.

📒 Files selected for processing (1)
  • crates/vm/src/stdlib/os.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/stdlib/os.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run tests under miri
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (1)
crates/vm/src/stdlib/os.rs (1)

439-468: Verify _MAX_ENV is in scope for this function.

The function references _MAX_ENV at lines 455 and 458, but the only visible import is inside check_env_var_len (line 429), which is scoped to that function. Ensure there's a module-level import for _MAX_ENV or this won't compile.

#!/bin/bash
# Description: Verify _MAX_ENV is imported at module level within the _os module
# Looking for use statements that import _MAX_ENV outside of function scope

rg -n "_MAX_ENV" crates/vm/src/stdlib/os.rs -B5 -A2

@youknowone youknowone force-pushed the test-os-on-windows branch 2 times, most recently from e3dc792 to 016c575 Compare December 10, 2025 03:01
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
crates/vm/src/stdlib/os.rs (3)

425-437: Unused check_env_var_len helper and stale doc comment

check_env_var_len is no longer called, and its comment about key.len() + value.len() + 2 doesn’t match the current wide-string-based _MAX_ENV checks in putenv/unsetenv. This is likely to trigger an unused-function lint on Windows and is a bit misleading.

Consider either:

  • Wiring this helper into the Windows putenv/unsetenv paths (taking wide.len() or similar), and updating the comment accordingly; or
  • Dropping the helper entirely and keeping the length checks inline.

495-522: Windows unsetenv: mirrors putenv correctly; consider sharing logic

This Windows-specific unsetenv mirrors putenv’s validation rules and uses _wputenv("KEY=") to remove the variable, which matches typical CRT/CPython behavior. Error handling via new_value_error and new_last_errno_error also looks consistent.

Given how similar it is to putenv, you could:

  • Share the name validation logic between the two functions, and
  • Optionally share the _MAX_ENV length check via a small helper,

to avoid future drift between the two paths.

If you want to be sure this matches your target CRT’s behavior, please double-check the _wputenv + _MAX_ENV semantics for your MSVC toolchain version and that KEY= indeed removes the variable rather than leaving an empty one.


439-469: Windows putenv: validation logic is duplicated; confirm bytes API change was intentional

The Windows putenv implementation looks correct: it enforces non-empty names, allows a leading =, rejects additional = and embedded NULs, checks the _MAX_ENV limit using the wide string length, and uses _wputenv to keep the CRT environment in sync.

One follow-up worth addressing:

  • The name validity check (empty, = position, NUL chars) and _MAX_ENV-based length check are duplicated between putenv (lines 445–454, 456–460) and unsetenv (lines 477–487, 489–494). Pulling them into small helpers (e.g. validate_env_name(&str, vm)? and validate_env_length(&[u16], vm)?) would reduce duplication and keep Windows-specific rules in one place.

Regarding the signature change to PyStrRef only: the current code already accepts only str, not bytes. If this was a recent change from Either<PyStrRef, PyBytesRef>, confirm that the behavioral change is intentional and acceptable (it aligns with CPython on Windows, which accepts text only).

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3dc792 and 016c575.

📒 Files selected for processing (1)
  • crates/vm/src/stdlib/os.rs (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/stdlib/os.rs
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run tests under miri
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Ensure compilation on various targets

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/vm/src/stdlib/os.rs (1)

429-439: Remove unused function.

The check_env_var_len function is never called. The length validation is performed inline at lines 463 and 516. Either use this function or remove it to avoid dead code.

If you intend to use this function, apply this pattern at lines 463 and 516:

-        if wide.len() > _MAX_ENV + 1 {
-            return Err(vm.new_value_error(format!(
-                "the environment variable is longer than {} characters",
-                _MAX_ENV
-            )));
-        }
+        check_env_var_len(wide.len(), vm)?;

However, note that the inline checks use wide.len() which includes the null terminator, while check_env_var_len expects size to be key.len() + value.len() + 2. The function signature would need adjustment to match the actual usage pattern.

📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 016c575 and 7013528.

📒 Files selected for processing (1)
  • crates/vm/src/stdlib/os.rs (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/stdlib/os.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: arihant2math
Repo: RustPython/RustPython PR: 5790
File: build.rs:2-2
Timestamp: 2025-06-28T16:31:03.991Z
Learning: In Cargo build scripts (build.rs), the environment variable CARGO_CFG_TARGET_OS is guaranteed to exist and is automatically set by Cargo during the build process, making unwrap() safe to use when accessing this variable.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run tests under miri
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (6)
crates/vm/src/stdlib/os.rs (6)

115-119: LGTM!

Correct platform gating. Windows uses wide strings (PyStrRef) directly, while non-Windows platforms need byte-to-OsStr conversion.


419-425: LGTM!

Correct platform gating. This helper is only needed for non-Windows implementations that accept Either<PyStrRef, PyBytesRef>.


441-444: LGTM!

Correct Windows CRT function declaration for _wputenv.


478-498: LGTM!

Correct non-Windows implementation. The stricter validation (rejecting '=' anywhere in the key) is appropriate for Unix systems, unlike Windows which allows keys starting with '=' for hidden variables.


500-529: LGTM! (Same edge case as putenv)

The implementation correctly uses _wputenv with an empty value ("key=") to unset environment variables on Windows, matching CPython's approach. The validation logic is consistent with the Windows putenv implementation, including the same edge case where a key of just "=" would be allowed.


531-551: LGTM!

Correct non-Windows implementation with appropriate errno-based error handling for invalid key names.

Comment on lines 446 to 468
#[cfg(windows)]
#[pyfunction]
fn putenv(key: PyStrRef, value: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
use crate::common::windows::_MAX_ENV;
let key_str = key.as_str();
let value_str = value.as_str();
// Search from index 1 because on Windows starting '=' is allowed for
// defining hidden environment variables.
if key_str.is_empty()
|| key_str.get(1..).is_some_and(|s| s.contains('='))
|| key_str.contains('\0')
|| value_str.contains('\0')
{
return Err(vm.new_value_error("illegal environment variable name"));
}
let env_str = format!("{}={}", key_str, value_str);
let wide = env_str.to_wide_with_nul();
if wide.len() > _MAX_ENV + 1 {
return Err(vm.new_value_error(format!(
"the environment variable is longer than {} characters",
_MAX_ENV
)));
}

// Use _wputenv like CPython (not SetEnvironmentVariableW) to update CRT environ
let result = unsafe { suppress_iph!(_wputenv(wide.as_ptr())) };
if result != 0 {
return Err(vm.new_last_errno_error());
}
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Search for test cases related to putenv
rg -n "putenv|os.putenv" --type=py crates/

Repository: RustPython/RustPython

Length of output: 186


🏁 Script executed:

# Check if there are any test files for the os module
fd -e py -p test | rg -i "test.*os|os.*test" | head -20

Repository: RustPython/RustPython

Length of output: 186


🏁 Script executed:

# Look at the git context to see if there are comments or commits related to this
rg -n "hidden.*environment\|environment.*variable.*name" --type=rs

Repository: RustPython/RustPython

Length of output: 186


🌐 Web query:

CPython putenv Windows environment variable validation

💡 Result:

Short answer: CPython validates environment-variable names for os.putenv / os.environ on Windows — names may not contain '=' except possibly as the first character (Windows "hidden" variables like "=C:..."); CPython rejects names with '=' elsewhere and keeps behavior in sync with the C runtime/Windows APIs. See Python issue tracker discussions and the related CPython changes. [1][2][3]

Sources

  • Reject '=' in env var names (issue discussion & PRs). [1]
  • Windows parsing/leading '=' (hidden) behavior and tests. [2]
  • Changes to os.putenv/os.unsetenv implementation and Windows notes (CRT vs SetEnvironmentVariable). [3]

🌐 Web query:

Windows environment variable naming rules "=" character

💡 Result:

Short answer: Windows environment variable names cannot contain the equals sign ("="). Environment strings are stored as "Name=Value\0" so the '=' is the separator and is not allowed in the name. [1][2]

References:

  • Microsoft: Environment Variables (Win32) — "The name of an environment variable cannot include an equal sign (=)." [1]
  • POSIX / Open Group: environment strings have the form name=value and "names shall not contain the character '='." (useful for general background). [2]

🌐 Web query:

CPython source code putenv Windows implementation validation

💡 Result:

Short answer: CPython validates Windows putenv names and values in Modules/posixmodule.c (win32_putenv). On Windows it:

  • rejects empty names and any name containing '=' at or after index 1 (leading '=' is allowed for hidden vars).
  • builds a "NAME=VALUE" wide string and uses PyUnicode_AsWideCharString (which rejects embedded NULs).
  • checks the combined length against _MAX_ENV and raises ValueError if too long.
  • calls the CRT _wputenv (so CRT updates _wenviron/_environ).

Sources: CPython win32_putenv implementation in Modules/posixmodule.c and the related tracker/commit discussion. [1][2]

References

  • win32_putenv implementation (Modules/posixmodule.c). Shows the '=' check, NUL rejection via PyUnicode_AsWideCharString, length check against _MAX_ENV, and use of _wputenv. [1]
  • bpo-39406 discussion / commits about using CRT putenv on Windows (context why CPython prefers _wputenv). [2]

[1] Modules/posixmodule.c — win32_putenv implementation.
[2] bpo-39406 / CPython commit discussion (os.putenv() behavior on Windows).


Fix edge case: key consisting of only "=" should be rejected

The validation logic allows a key consisting of just "=", which is invalid. The check key_str.get(1..).is_some_and(|s| s.contains('=')) returns false for this case because the substring after index 1 is empty.

Per Windows environment variable rules and CPython's implementation, "=" alone is not a valid environment variable name (it is the separator character in "NAME=VALUE" format). While the code correctly allows leading "=" for Windows hidden variables (e.g., "=C:"), it should reject a key that is only "=".

Add an additional check: || key_str == "=" to explicitly reject this edge case.

🤖 Prompt for AI Agents
In crates/vm/src/stdlib/os.rs around lines 446 to 476, the current validation
for Windows environment variable keys mistakenly allows a key equal to "=";
update the existing conditional that rejects empty, null-containing, or
'='-containing keys to also explicitly reject the single-character key "="
(e.g., add a check like key_str == "=" to the OR chain) so that a lone "=" is
treated as an invalid environment variable name.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
crates/vm/src/stdlib/os.rs (1)

444-468: Critical issue remains unresolved: key consisting of only "=" is still accepted.

The validation at lines 451-456 still allows a key of "=" to pass, despite the previous review comment flagging this exact issue. When key_str = "=", the check key_str.get(1..).is_some_and(|s| s.contains('=')) evaluates to false because the substring after index 1 is empty and does not contain '='.

Per Windows environment variable naming rules and CPython's implementation, a key consisting solely of "=" is invalid (it serves as the separator in "NAME=VALUE" format). Add an explicit check to reject this edge case.

Apply this diff to fix the validation:

         if key_str.is_empty()
             || key_str.get(1..).is_some_and(|s| s.contains('='))
+            || key_str == "="
             || key_str.contains('\0')
             || value_str.contains('\0')
         {
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7013528 and 431a043.

⛔ Files ignored due to path filters (2)
  • Lib/test/test_ntpath.py is excluded by !Lib/**
  • Lib/test/test_unittest/testmock/testpatch.py is excluded by !Lib/**
📒 Files selected for processing (3)
  • .github/workflows/ci.yaml (0 hunks)
  • crates/vm/src/stdlib/nt.rs (2 hunks)
  • crates/vm/src/stdlib/os.rs (10 hunks)
💤 Files with no reviewable changes (1)
  • .github/workflows/ci.yaml
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.rs: Follow the default rustfmt code style by running cargo fmt to format Rust code
Always run clippy to lint Rust code (cargo clippy) before completing tasks and fix any warnings or lints introduced by changes
Follow Rust best practices for error handling and memory management
Use the macro system (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • crates/vm/src/stdlib/nt.rs
  • crates/vm/src/stdlib/os.rs
🧠 Learnings (1)
📓 Common learnings
Learnt from: arihant2math
Repo: RustPython/RustPython PR: 5790
File: build.rs:2-2
Timestamp: 2025-06-28T16:31:03.991Z
Learning: In Cargo build scripts (build.rs), the environment variable CARGO_CFG_TARGET_OS is guaranteed to exist and is automatically set by Cargo during the build process, making unwrap() safe to use when accessing this variable.
🧬 Code graph analysis (1)
crates/vm/src/stdlib/os.rs (2)
crates/common/src/os.rs (2)
  • bytes_as_os_str (66-69)
  • bytes_as_os_str (72-74)
crates/vm/src/function/fspath.rs (1)
  • bytes_as_os_str (123-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (11)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run tests under miri
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
🔇 Additional comments (4)
crates/vm/src/stdlib/os.rs (4)

470-490: Non-Windows implementation looks correct.

The signature change to accept Either<PyStrRef, PyBytesRef> for both key and value aligns with CPython's behavior on Unix-like systems. The validation at line 482 correctly rejects any key containing '=', including the edge case of a key consisting solely of "=".


517-540: Non-Windows implementation looks correct.

The signature change and validation logic are appropriate. The check at line 527 correctly rejects any key containing '=', including a key consisting only of "=".


115-119: Helper function changes look correct.

The addition of #[cfg(not(windows))] to bytes_as_os_str is appropriate since Windows uses wide strings for environment variables. The update to env_bytes_as_bytes to handle Either<PyStrRef, PyBytesRef> correctly supports the new function signatures for non-Windows platforms.

Also applies to: 419-425


428-442: Windows helper functions look correct.

The unsafe extern "C" binding for _wputenv correctly follows CPython's approach of using the CRT function to update the environment. The check_env_var_len function appropriately validates against Windows' _MAX_ENV limit, accounting for the null terminator.

Comment on lines +367 to 377
// Validate: no '=' in key (search from index 1 because on Windows
// starting '=' is allowed for defining hidden environment variables)
if key_str.get(1..).is_some_and(|s| s.contains('=')) {
return Err(vm.new_value_error("illegal environment variable name"));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical issue: key consisting of only "=" is incorrectly accepted.

The validation at line 369 has the same flaw as the corresponding code in os.rs. When key_str = "=", the check key_str.get(1..).is_some_and(|s| s.contains('=')) returns false because the substring after index 1 is empty and does not contain '='.

A key consisting solely of "=" is not a valid environment variable name and should be explicitly rejected.

Apply this diff to fix the validation:

             // Validate: no '=' in key (search from index 1 because on Windows
             // starting '=' is allowed for defining hidden environment variables)
-            if key_str.get(1..).is_some_and(|s| s.contains('=')) {
+            if key_str == "=" || key_str.get(1..).is_some_and(|s| s.contains('=')) {
                 return Err(vm.new_value_error("illegal environment variable name"));
             }
🤖 Prompt for AI Agents
In crates/vm/src/stdlib/nt.rs around lines 367 to 370, the current validation
misses the case where key_str == "=" so a single "=" is incorrectly accepted;
update the validation to explicitly reject a key that is exactly "=" (return the
same vm.new_value_error) before the existing substring check (or alter the
condition to treat an empty substring after index 1 as invalid), ensuring any
key consisting solely of "=" is treated as illegal.

Comment on lines +484 to 494
// Validate: no '=' in key (search from index 1 because on Windows
// starting '=' is allowed for defining hidden environment variables)
if key_str.get(1..).is_some_and(|s| s.contains('=')) {
return Err(vm.new_value_error("illegal environment variable name"));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical issue: key consisting of only "=" is incorrectly accepted.

The validation at line 486 has the same flaw as spawnve above. A key of "=" will incorrectly pass validation because key_str.get(1..).is_some_and(|s| s.contains('=')) returns false when the substring after index 1 is empty.

Apply this diff to fix the validation:

             // Validate: no '=' in key (search from index 1 because on Windows
             // starting '=' is allowed for defining hidden environment variables)
-            if key_str.get(1..).is_some_and(|s| s.contains('=')) {
+            if key_str == "=" || key_str.get(1..).is_some_and(|s| s.contains('=')) {
                 return Err(vm.new_value_error("illegal environment variable name"));
             }
🤖 Prompt for AI Agents
crates/vm/src/stdlib/nt.rs around lines 484 to 487: the current check allows a
key of "=" because get(1..) is None and the condition is false; change the
validation to also reject a lone "=" by updating the conditional to fail when
key_str == "=" or when key_str.get(1..).is_some_and(|s| s.contains('=')).
Replace the existing if condition with a compound check that returns Err for
either case.

Comment on lines +492 to +515
#[cfg(windows)]
#[pyfunction]
fn unsetenv(key: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
let key_str = key.as_str();
// Search from index 1 because on Windows starting '=' is allowed for
// defining hidden environment variables.
if key_str.is_empty()
|| key_str.get(1..).is_some_and(|s| s.contains('='))
|| key_str.contains('\0')
{
return Err(vm.new_value_error("illegal environment variable name"));
}
// "key=" to unset (empty value removes the variable)
let env_str = format!("{}=", key_str);
let wide = env_str.to_wide_with_nul();
check_env_var_len(wide.len(), vm)?;

// Use _wputenv like CPython (not SetEnvironmentVariableW) to update CRT environ
let result = unsafe { suppress_iph!(_wputenv(wide.as_ptr())) };
if result != 0 {
return Err(vm.new_last_errno_error());
}
Ok(())
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical issue: key consisting of only "=" is incorrectly accepted.

The validation at lines 498-502 has the same flaw as putenv above. When key_str = "=", the check key_str.get(1..).is_some_and(|s| s.contains('=')) returns false because the substring after index 1 is empty.

A key of just "=" is not a valid environment variable name and should be rejected.

Apply this diff to fix the validation:

         if key_str.is_empty()
             || key_str.get(1..).is_some_and(|s| s.contains('='))
+            || key_str == "="
             || key_str.contains('\0')
         {
🤖 Prompt for AI Agents
In crates/vm/src/stdlib/os.rs around lines 492 to 515, the validation allows a
key consisting only of "=" because checking key_str.get(1..) misses the case
where the first character is '='; update the guard to also reject keys that
start with '=' (e.g., key_str.starts_with('=') or key_str.chars().next() ==
Some('=')) so that a lone "=" or any name beginning with '=' is treated as
invalid, while keeping the existing empty and NUL checks and the subsequent
logic unchanged.

@youknowone youknowone force-pushed the test-os-on-windows branch 2 times, most recently from a8f1e26 to df899ea Compare December 11, 2025 03:22
@youknowone youknowone merged commit 4828fb3 into RustPython:main Dec 11, 2025
13 checks passed
@youknowone youknowone deleted the test-os-on-windows branch December 11, 2025 04:11
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