Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 6, 2025

Summary by CodeRabbit

  • New Features

    • Support using IP addresses as server hostnames (SNI will not be sent for IPs).
    • Windows: enumerate system certificates and CRLs from OS stores.
  • Improvements

    • Improved Windows system certificate loading and load-order when locating trusted roots.
    • Windows-specific default certificate path behavior to avoid file-path checks.

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

@youknowone youknowone marked this pull request as ready for review December 6, 2025 00:38
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 6, 2025

Walkthrough

Relaxed hostname validation to allow IP addresses (no SNI for IPs); added Windows schannel-based loading of system certificates (ROOT and CA) and Windows-specific defaults; introduced Windows-only cert and CRL enumeration functions; exposed those functions in the public _ssl exports.

Changes

Cohort / File(s) Summary
Windows SSL certificate additions & hostname behavior
crates/stdlib/src/ssl.rs
Allow IP addresses as server hostnames (SNI not sent for IPs); add Windows schannel-based loading of system certs from ROOT and CA stores and Windows-specific default verify paths; add Windows-only enum_certificates and enum_crls and export them under _ssl (cfg(windows)).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30–45 minutes

  • Areas needing extra attention:
    • schannel integration and correctness of certificate/CRL enumeration implementations
    • ordering and fallback behavior for loading verify locations and default certs on Windows vs non-Windows
    • public API surface and return-value contracts for enum_certificates / enum_crls
    • hostname validation change for IP addresses and SNI implications

Suggested reviewers

  • coolreader18
  • ShaharNaveh
  • arihant2math

Poem

🐰 I hopped through schannel, sniffed ROOT and CA,

No SNI for an IP — that’s how hops go today.
I list certs and CRLs with a twitch of my nose,
Windows stores now sparkle where the rabbit goes. ✨

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 accurately describes the main objective of the pull request, which is to add Windows-specific SSL module functionality.
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

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.

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 (2)
crates/stdlib/src/ssl.rs (2)

1532-1541: Inconsistent error handling between certificate loading calls.

load_system_certificates errors are silently ignored with let _ = ..., while try_load_from_python_environ propagates errors with ?. This inconsistency could mask legitimate failures.

Consider either:

  1. Using ? for both to propagate errors consistently
  2. Using let _ = ... for both if best-effort loading is intended
  3. Adding a comment explaining why one can fail silently but not the other
-            // Load system certificates first (before creating loader to avoid borrow conflicts)
-            let _ = self.load_system_certificates(&mut store, vm);
+            // Load system certificates first (before creating loader to avoid borrow conflicts)
+            // Silently ignore errors - fallback to webpki-roots below if no certs loaded
+            let _ = self.load_system_certificates(&mut store, vm);

4504-4556: enum_crls uses unsafe Windows API - review carefully.

The implementation correctly:

  1. Opens the store and checks for null
  2. Iterates CRLs with proper null termination check
  3. Closes the store at the end

However, there's a potential issue: if store_name.as_str().encode_utf16() produces an invalid store name that opens successfully but causes CertEnumCRLsInStore to behave unexpectedly, the loop would still safely terminate on null.

One concern: the CertCloseStore at line 4553 is always called, but if early returns were added in the future (e.g., for error handling in the loop), the store could leak. Consider using a RAII wrapper or defer! pattern.

For improved safety, consider wrapping the store handle in a struct with Drop:

struct CertStoreHandle(windows_sys::Win32::Security::Cryptography::HCERTSTORE);
impl Drop for CertStoreHandle {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe { CertCloseStore(self.0, 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 b3141d1 and 2092aad.

📒 Files selected for processing (2)
  • crates/stdlib/src/ssl.rs (7 hunks)
  • crates/vm/src/stdlib/builtins.rs (1 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/stdlib/src/ssl.rs
  • crates/vm/src/stdlib/builtins.rs
🧬 Code graph analysis (2)
crates/stdlib/src/ssl.rs (2)
crates/stdlib/src/ssl/cert.rs (1)
  • is_ca_certificate (270-286)
crates/stdlib/src/ssl/compat.rs (1)
  • writer (318-323)
crates/vm/src/stdlib/builtins.rs (1)
crates/vm/src/stdlib/os.rs (1)
  • extend_module (1573-1597)
⏰ 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 tests under miri
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (7)
crates/vm/src/stdlib/builtins.rs (1)

1190-1195: LGTM!

The WindowsError alias for OSError follows the same pattern as existing aliases (IOError, EnvironmentError at lines 1138-1139) and correctly uses #[cfg(windows)] for platform-specific compilation. This matches CPython's behavior where WindowsError is available as an OSError alias on Windows.

crates/stdlib/src/ssl.rs (6)

481-483: LGTM!

Allowing IP addresses as server_hostname while disabling SNI for them is the correct behavior per RFC 6066. SNI should only contain DNS hostnames, not IP addresses. This aligns with CPython 3.7+ behavior.


1931-1932: LGTM!

The comment correctly documents that IP addresses are permitted as server_hostname, consistent with the validate_hostname function changes.


3333-3341: LGTM!

Returning empty strings for Windows is appropriate since Windows uses the system certificate store rather than file paths. The comment correctly explains this prevents None being passed to os.path.isfile() in Python's ssl.py.


3430-3484: Chunked write implementation looks correct but consider edge case.

The 16KB chunking aligns with typical TLS record sizes and prevents buffer overflow issues. However, if a timeout or error occurs mid-write (e.g., at line 3468-3469), the function returns an error without indicating how many bytes were successfully sent. This differs from typical socket semantics where partial writes return the count.

This is acceptable for SSL semantics (all-or-nothing writes are common), but worth documenting if this behavior diverges from CPython's SSLSocket.write().


4453-4502: enum_certificates implementation looks solid.

Good use of the schannel crate's safe abstractions. The conversion of ValidUses to either a boolean or frozenset correctly matches CPython's expected return format.


1453-1522: The Windows certificate loading implementation follows schannel best practices by opening both ROOT and CA stores from both current user and local machine contexts, properly converting certificates to DER format, and tracking CA vs. non-CA certificates. Using rustls-native-certs for non-Windows platforms is appropriate. Verification of the schannel dependency in Cargo.toml and confirmation that the code passes cargo clippy checks could not be completed due to repository access limitations.

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: 2

🧹 Nitpick comments (4)
crates/stdlib/src/ssl.rs (4)

1532-1554: Document platform-specific certificate loading order.

The implementation has different certificate loading strategies for Windows vs. non-Windows:

  • Windows: System certs first, then environment variables (additively)
  • Non-Windows: Environment variables first, system certs only as fallback

This could be confusing for users expecting consistent behavior. Additionally, the Windows path silently ignores errors from load_system_certificates (line 1536), while the non-Windows path checks success (line 1549).

Consider adding a module-level doc comment explaining this platform difference, or align the behavior if there's no strong reason for the difference. If the difference is intentional (e.g., to match CPython's test expectations), document it clearly:

// Platform-specific certificate loading strategies:
// - Windows: Always loads system store + env vars (matches test_load_default_certs_env_windows)
// - Unix: Prefers env vars over system (matches test_load_default_certs_env)

3443-3497: Consider implications of chunked write implementation.

The write method now processes data in 16KB chunks to avoid filling rustls's internal buffer. However, there are potential issues:

  1. Partial write semantics: If a chunk fails to flush to the socket (line 3469-3495), the method returns an error, but some chunks may have already been written to the TLS layer. This differs from traditional socket write semantics where the return value indicates how much was written.

  2. Performance impact: The method now makes multiple flush attempts (one per chunk) rather than buffering everything. For small writes, this could increase overhead.

  3. Non-blocking behavior: In non-blocking mode, if a chunk fails with SSLWantWriteError, previously written chunks are lost from the caller's perspective.

Consider either:

  • Tracking written bytes and returning partial success instead of error
  • Documenting that this matches SSL_write_ex behavior (all-or-nothing in SSL layer)
  • Adding a comment explaining why chunking is necessary for rustls
// Write data in chunks to avoid filling rustls's internal buffer.
// rustls has a limited internal buffer (~16KB), so we need to flush periodically.
// Note: This follows SSL_write_ex semantics - either all data is accepted or an error is returned.

4488-4514: Consider error handling consistency for certificate usage.

The error handling for c.valid_uses() (lines 4499-4511) catches all errors and falls back to returning true, which could mask genuine errors. This differs from the similar code in the openssl.rs context snippet (lines 3661-3666 in relevant_code_snippets), which propagates errors:

// openssl.rs pattern:
let usage: PyObjectRef = match c.valid_uses().map_err(|e| e.to_pyexception(vm))? {
    ValidUses::All => vm.ctx.new_bool(true).into(),
    // ...
};

vs. current implementation:

// Current pattern:
let usage: PyObjectRef = match c.valid_uses() {
    Ok(ValidUses::All) => vm.ctx.new_bool(true).into(),
    Ok(ValidUses::Oids(oids)) => { /* ... */ },
    Err(_) => vm.ctx.new_bool(true).into(),  // Masks error
};

Consider propagating errors for better diagnostics, or document why errors should be silently ignored. If this matches CPython's behavior of treating invalid usage as "all uses", add a comment explaining the rationale.


4517-4569: Improve safety and error handling in enum_crls.

The function uses extensive unsafe code with raw Windows APIs. While the implementation appears correct, there are safety and maintenance concerns:

  1. Unsafe pointer operations (lines 4550-4552): Dereferencing raw pointers and creating slices without validation
  2. Resource cleanup (line 4566): Store cleanup happens at the end, but if a panic occurs during iteration, the store won't be closed
  3. Inconsistency: Uses windows_sys FFI directly, while enum_certificates uses the higher-level schannel crate

Consider these improvements:

  1. Use RAII for store cleanup:
// Add a guard struct
struct StoreGuard(windows_sys::Win32::Security::Cryptography::HCERTSTORE);
impl Drop for StoreGuard {
    fn drop(&mut self) {
        unsafe { CertCloseStore(self.0, 0) };
    }
}

let _store_guard = StoreGuard(store);
  1. Add safety comments documenting the invariants for each unsafe block:
// SAFETY: crl_context is valid because CertEnumCRLsInStore returned non-null
let crl = unsafe { &*crl_context };
  1. Consider using schannel for consistency with enum_certificates, if it provides CRL enumeration APIs.
📜 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 2092aad and 42356af.

📒 Files selected for processing (2)
  • crates/stdlib/src/ssl.rs (7 hunks)
  • crates/vm/src/stdlib/builtins.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/vm/src/stdlib/builtins.rs
🧰 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/stdlib/src/ssl.rs
🧬 Code graph analysis (1)
crates/stdlib/src/ssl.rs (2)
crates/stdlib/src/openssl.rs (5)
  • open_fns (3639-3642)
  • new (3689-3699)
  • enum_certificates (3632-3667)
  • certs (1377-1386)
  • certs (3665-3665)
crates/stdlib/src/ssl/cert.rs (2)
  • is_ca_certificate (270-286)
  • new (627-635)
⏰ 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 tests under miri
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run rust tests (windows-latest)
🔇 Additional comments (1)
crates/stdlib/src/ssl.rs (1)

481-482: Verify SNI suppression for IP addresses matches the implementation.

The comments state that SNI will not be sent for IP addresses, but the validate_hostname function doesn't explicitly distinguish between hostnames and IPs. Confirm that ServerName::try_from() (or the rustls library it uses) automatically handles this distinction and suppresses SNI for IP addresses as documented. If SNI suppression relies on rustls internals, add implementation details in code or comments to clarify the mechanism.

Comment on lines +4346 to +4352
#[cfg(windows)]
let (default_cafile, default_capath) = {
// Windows uses certificate store, not file paths
// Return empty strings to avoid None being passed to os.path.isfile()
(Some(""), Some(""))
};

#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
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

🌐 Web query:

CPython ssl.get_default_verify_paths() Windows openssl_cafile openssl_capath return values

💡 Result:

Short answer:

  • ssl.get_default_verify_paths() returns a DefaultVerifyPaths(namedtuple) with fields cafile, capath, openssl_cafile_env, openssl_cafile, openssl_capath_env, openssl_capath. The cafile/capath fields are the resolved paths or None if the file/dir does not exist. [1]

  • On Windows, the OpenSSL hard‑coded defaults that Python exposes are typically:

    • openssl_cafile_env = "SSL_CERT_FILE"
    • openssl_cafile = "C:\Program Files\Common Files\SSL\cert.pem"
    • openssl_capath_env = "SSL_CERT_DIR"
    • openssl_capath = "C:\Program Files\Common Files\SSL\certs"
      The runtime cafile and capath (the cafile/capath fields) will be those resolved paths if they exist, otherwise None. [1][2]

Sources:

  • Python ssl.get_default_verify_paths() docs. [1]
  • Explanation / defaults for OpenSSL on Windows (example used by PyOxidizer and others). [2]

Return None instead of empty strings for Windows certificate paths to match CPython behavior.

CPython's ssl.get_default_verify_paths() returns None for the cafile and capath fields when the hardcoded default paths do not exist on Windows (rather than returning empty strings). The current implementation returning Some("") on Windows (lines 4349-4350) deviates from this behavior. Update to return None or the actual path if it exists, consistent with how macOS and Linux are handled.

🤖 Prompt for AI Agents
In crates/stdlib/src/ssl.rs around lines 4346 to 4353, the Windows branch
currently returns Some("") for (default_cafile, default_capath) which differs
from CPython; change the Windows branch to return (None, None) when no valid
cafile/capath exists (or the actual path wrapped in Some(...) if you detect a
real file/directory), matching the macOS/Linux logic and CPython behavior so
absent/default paths are represented as None rather than empty strings.

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 (2)
crates/vm/src/exceptions.rs (1)

1495-1511: Consider matching CPython’s errno/winerror precedence in PyOSError::slot_init.

On Windows, you always overwrite args[0] with winerror_to_errno(winerror_int) when a positional winerror is present. In CPython, OSError.__init__ only derives errno from winerror when errno is not explicitly provided (or is falsy). This change means callers that pass both an errno and winerror positionally will have their explicit errno silently replaced.

If you want full CPython compatibility, consider only rewriting new_args.args[0] when the original errno is missing/0, mirroring CPython’s behavior. Otherwise, a short comment that this intentionally diverges from CPython would help future readers.

crates/stdlib/src/ssl.rs (1)

462-493: Update validate_hostname docstring to reflect that IP addresses are now allowed.

The doc comment lists "Is not an IP address (SNI requires DNS names)" as a validation requirement, but the function now explicitly allows IP addresses (lines 481–482) and performs only basic syntactic checks. Update the docstring to describe the actual behavior: IP literals are accepted here; SNI suppression happens elsewhere.

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

4346-4353: Windows get_default_verify_paths() still returns "" instead of None (same concern as earlier review).

On Windows you keep returning Some("") for both default_cafile and default_capath, which becomes empty strings in the namedtuple rather than None. CPython’s ssl.get_default_verify_paths() returns None for those fields when there is no actual default path. If downstream Python code or tests rely on is None semantics rather than falsy-string checks, this remains a behavioral divergence.

If you decide to keep the empty-string approach (to avoid None being passed into os.path.isfile, etc.), please document that deviation clearly in a comment here or in the Python-side wrapper.

🧹 Nitpick comments (5)
crates/stdlib/src/ssl.rs (5)

1931-1953: Reuse validate_hostname() in _wrap_socket to keep semantics consistent.

_wrap_socket() currently reimplements hostname checks inline (empty, leading dot, NUL) and comments that IPs are allowed, but it:

  • Doesn’t enforce the 253-character limit enforced by validate_hostname().
  • Duplicates logic now shared by _wrap_bio and set_server_hostname().

Consider replacing the inline checks with a call to validate_hostname(hostname, vm)?; to keep all entry points (wrap_socket, wrap_bio, set_server_hostname) in sync, and so any future hostname rules change in one place.


1450-1521: Windows vs non-Windows system cert loading looks reasonable; consider error reporting consistency.

The new load_system_certificates() splits Windows and non-Windows paths cleanly:

  • Windows: enumerates ROOT and CA via schannel stores for both CurrentUser and LocalMachine, counting total and CA certs.
  • Non-Windows: uses rustls_native_certs::load_native_certs() and similarly updates stats.

On Windows, you convert “no certs loaded” into OSError("Failed to load certificates from Windows store"), but callers (load_default_certs) immediately ignore that error and fall back to env/webpki. On non-Windows, you only raise when there are both 0 certs and non-empty errors. If you want consistent diagnostics, you might either:

  • Make Windows also return details when available (e.g., last OS error), or
  • Treat the “0 certs loaded” case the same way as non-Windows and only raise when there’s an explicit error from the loader.

Not strictly required, but it would make behavior and error reporting more uniform across platforms.


1532-1554: load_default_certs Windows/non-Windows ordering matches intent; consider surfacing failure explicitly in logs/tests.

The new split in load_default_certs():

  • Windows: always attempts system store first (via schannel), then optionally adds env-based certs.
  • Non-Windows: prefers env-based SSL_CERT_FILE/SSL_CERT_DIR, falling back to system store only if env doesn’t yield anything.

This mirrors CPython’s documented/tested behavior. Since both paths eventually fall back to webpki_roots when x509_cert_count == 0, the user never sees a hard failure. If you want easier debugging of “why are my system CAs ignored?”, consider emitting a warning or debug log when system-store loading fails but you silently proceed to the Mozilla bundle.


3443-3499: Chunked writes + explicit TLS flushing look correct; watch for performance overhead.

The new write() path:

  • Writes in 16KB chunks into conn.writer().
  • After each chunk, flushes pending TLS data via write_pending_tls() in BIO mode or write_tls + sock_send in socket mode, honoring timeouts and mapping blocking errors to WANT_WRITE.

Semantically this should avoid overfilling rustls’ internal buffers and better match OpenSSL-style behavior. The main downside is per-chunk allocation of a Vec<u8> in the socket mode flush path; if this proves hot under large writes, consider reusing a single buffer or using a stack-allocated array instead of reallocating each iteration.


4466-4515: enum_certificates implementation looks solid; consider reducing unsafe surface.

The Windows-only enum_certificates():

  • Opens both CurrentUser and LocalMachine stores for the given name.
  • Returns an OSError if neither can be opened.
  • Emits tuples of (der_bytes, enc_type, usage) where enc_type is "x509_asn", "pkcs_7_asn", or the raw encoding integer, and usage is either True or a frozenset of OID strings based on ValidUses.

The unsafe block only dereferences the CERT_CONTEXT pointer to read dwCertEncodingType. If the schannel crate ever exposes the encoding type safely, it would be preferable to use that API to avoid direct struct casting via windows_sys::Win32::Security::Cryptography::CERT_CONTEXT. For now this looks correct but slightly brittle across crate-version changes.

📜 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 42356af and 036ee48.

📒 Files selected for processing (3)
  • crates/stdlib/src/ssl.rs (7 hunks)
  • crates/vm/src/exceptions.rs (2 hunks)
  • crates/vm/src/stdlib/builtins.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/vm/src/stdlib/builtins.rs
🧰 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/stdlib/src/ssl.rs
  • crates/vm/src/exceptions.rs
🧬 Code graph analysis (2)
crates/stdlib/src/ssl.rs (1)
crates/stdlib/src/ssl/cert.rs (1)
  • is_ca_certificate (270-286)
crates/vm/src/exceptions.rs (1)
crates/common/src/os.rs (1)
  • winerror_to_errno (110-208)
⏰ 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). (9)
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (2)
crates/vm/src/exceptions.rs (1)

1523-1555: OSError __str__ formatting with [WinError X] / [Errno X] looks correct.

The new formatting logic correctly prefers winerror when set on Windows, falls back to errno otherwise, and incorporates filename / filename2 into messages in the expected CPython-style forms like "[WinError X] msg: 'file' -> 'file2'" and "[Errno X] msg". This should make OSError messages much closer to CPython on all platforms.

crates/stdlib/src/ssl.rs (1)

4517-4569: Free CRL contexts when enumerating to avoid leaking native resources.

In enum_crls() you loop over CertEnumCRLsInStore(store, crl_context) but never call CertFreeCRLContext() on the returned CRL_CONTEXT pointers. Per the Win32 API contract, CertEnumCRLsInStore does not automatically free the previously returned context—you must call CertFreeCRLContext() explicitly to avoid accumulating unmanaged memory.

The standard pattern is to track the previous context pointer and free it before requesting the next one, then free the final context after the loop exits:

         let mut crl_context: *const CRL_CONTEXT = std::ptr::null();
         loop {
+            let prev = crl_context;
             crl_context = unsafe { CertEnumCRLsInStore(store, crl_context) };
+            if !prev.is_null() {
+                unsafe { CertFreeCRLContext(prev) };
+            }
             if crl_context.is_null() {
                 break;
             }

             let crl = unsafe { &*crl_context };
             // ... process crl ...
         }
+        if !crl_context.is_null() {
+            unsafe { CertFreeCRLContext(crl_context) };
+        }

This ensures that every CRL_CONTEXT pointer obtained during enumeration is properly freed, preventing resource leaks on repeated calls.

Likely an incorrect or invalid review comment.

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

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

4346-4351: Inconsistent with CPython: Should return None instead of empty strings.

This issue was previously raised in review comments. CPython's ssl.get_default_verify_paths() returns None for cafile and capath fields when Windows certificate store is used (rather than empty strings). The current implementation returning Some("") deviates from this behavior.

Apply this diff to match CPython behavior:

 #[cfg(windows)]
 let (default_cafile, default_capath) = {
     // Windows uses certificate store, not file paths
-    // Return empty strings to avoid None being passed to os.path.isfile()
-    (Some(""), Some(""))
+    (None, None)
 };

The Python ssl.py module handles None values correctly in its logic.

🧹 Nitpick comments (4)
crates/stdlib/src/ssl.rs (4)

1459-1491: Consider logging certificate load failures for diagnostics.

The current implementation silently skips certificates when store.add() fails (line 1476). While this approach is pragmatic for continuing the load process, diagnostic information about why specific certificates were rejected could be valuable for troubleshooting.

If rustls provides error details, consider logging them:

                                 let is_ca = cert::is_ca_certificate(cert.as_ref());
-                                if store.add(cert).is_ok() {
+                                match store.add(cert) {
+                                    Ok(_) => {
                                     *self.x509_cert_count.write() += 1;
                                     if is_ca {
                                         *self.ca_cert_count.write() += 1;
                                     }
+                                    }
+                                    Err(e) => {
+                                        // Log certificate rejection for diagnostics
+                                        eprintln!("Warning: Failed to add certificate from {} store: {:?}", store_name, e);
+                                    }
                                 }

3443-3497: Verify chunk flushing frequency for performance.

The implementation chunks writes into 16KB blocks and flushes after each chunk. While this prevents internal buffer overflow, it may introduce unnecessary system calls for large writes.

Consider flushing less frequently or only when the internal buffer indicates it's needed:

const CHUNK_SIZE: usize = 16384;
const FLUSH_THRESHOLD: usize = CHUNK_SIZE * 4; // Flush every 64KB
let mut bytes_since_flush = 0;

while written < data.len() {
    let chunk_end = std::cmp::min(written + CHUNK_SIZE, data.len());
    let chunk = &data[written..chunk_end];
    
    {
        let mut writer = conn.writer();
        use std::io::Write;
        writer.write_all(chunk)
            .map_err(|e| vm.new_os_error(format!("Write failed: {e}")))?;
    }
    
    written = chunk_end;
    bytes_since_flush += chunk.len();
    
    // Flush when threshold reached or on last chunk
    if bytes_since_flush >= FLUSH_THRESHOLD || written >= data.len() {
        if conn.wants_write() {
            // ... existing flush logic ...
        }
        bytes_since_flush = 0;
    }
}

This reduces the number of flushes while still preventing buffer overflow. However, verify with benchmarks whether this optimization is beneficial.


4488-4513: Verify safety of unsafe pointer operations.

The code performs unsafe pointer dereferences on certificate contexts (line 4490-4493) without explicit null checks. While schannel's API should guarantee valid pointers, documenting this assumption would improve safety.

Add a safety comment or assertion:

             let cert = vm.ctx.new_bytes(c.to_der().to_owned());
             let enc_type = unsafe {
+                // Safety: schannel's CertContext::as_ptr() always returns a valid pointer
+                // to an initialized CERT_CONTEXT structure
                 let ptr = c.as_ptr() as *const Cryptography::CERT_CONTEXT;
+                debug_assert!(!ptr.is_null(), "Certificate context pointer should never be null");
                 (*ptr).dwCertEncodingType
             };

Additionally, verify with the schannel crate documentation that cert_context.as_ptr() guarantees non-null pointers.


4517-4569: Consider RAII wrapper to prevent resource leaks.

The function manually manages the certificate store handle with CertOpenSystemStoreW and CertCloseStore. If a panic occurs between opening and closing (e.g., during memory allocation or Python object creation), the store handle will leak.

Implement a RAII guard to ensure cleanup:

struct StoreHandle(windows_sys::Win32::Security::Cryptography::HCERTSTORE);

impl Drop for StoreHandle {
    fn drop(&mut self) {
        if !self.0.is_null() {
            unsafe { CertCloseStore(self.0, 0) };
        }
    }
}

#[cfg(windows)]
#[pyfunction]
fn enum_crls(store_name: PyStrRef, vm: &VirtualMachine) -> PyResult<Vec<PyObjectRef>> {
    use windows_sys::Win32::Security::Cryptography::{
        CRL_CONTEXT, CertCloseStore, CertEnumCRLsInStore, CertOpenSystemStoreW,
        X509_ASN_ENCODING,
    };

    let store_name_wide: Vec<u16> = store_name
        .as_str()
        .encode_utf16()
        .chain(std::iter::once(0))
        .collect();

    let store = unsafe { CertOpenSystemStoreW(0, store_name_wide.as_ptr()) };
    
    if store.is_null() {
        return Err(vm.new_os_error(format!(
            "failed to open certificate store {:?}",
            store_name.as_str()
        )));
    }
    
    let _store_guard = StoreHandle(store);  // Ensures cleanup on panic or early return
    
    // ... rest of implementation ...
}

This ensures the store is always closed, even if an exception is raised during processing.

📜 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 036ee48 and d9e1867.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_ssl.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/stdlib/src/ssl.rs (7 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/stdlib/src/ssl.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: Check the WASM package and demo
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (2)
crates/stdlib/src/ssl.rs (2)

481-482: LGTM: IP address handling is correct and well-documented.

The change to allow IP addresses in server_hostname while documenting that SNI will not be sent aligns with RFC 6066 requirements and CPython behavior. The implementation correctly handles this case elsewhere in the codebase.


1532-1554: LGTM: Platform-specific certificate loading precedence is correct.

The implementation correctly handles Windows vs. non-Windows certificate loading order as documented in the comments. Windows loads system certificates first (via schannel), while non-Windows prioritizes environment variables with system certs as fallback.

The silent error handling (let _ = ...) is consistent with the fallback approach where the final webpki-roots ensures some certificates are always available (line 1558).

'Cannot create a client socket with a PROTOCOL_TLS_SERVER context',
str(e.exception))

@unittest.skip("TODO: RUSTPYTHON; Flaky on windows")
Copy link
Member Author

Choose a reason for hiding this comment

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

no idea why this is failing

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/stdlib/src/ssl.rs (1)

3442-3496: Potential data duplication issue with chunked writes in non-blocking mode.

The chunking logic (writing 16KB at a time, flushing after each chunk) can lead to data duplication or corruption in non-blocking mode:

  1. Chunk 1 writes successfully and flushes to socket (data on wire)
  2. Chunk 2 writes to TLS buffer but flush fails with WANT_WRITE error
  3. Function raises SSLWantWriteError to caller
  4. Caller retries write() with the same full buffer
  5. Retry writes chunk 1 again to TLS layer (duplicate) while chunk 1 is already sent

This differs from CPython's SSL_write(), which maintains internal state to handle retries correctly. The issue stems from implementing chunking at the application level rather than within the TLS layer.

Consider one of these approaches:

  1. Remove chunking and write all data at once (simpler, but may use more memory)
  2. Add internal state to track which chunks were successfully sent
  3. Expose a flush() method and document that callers must handle partial writes

For now, this primarily affects non-blocking socket mode, which is less common than blocking or BIO mode.

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

4345-4352: Unresolved: Return None instead of empty strings for Windows paths.

This was flagged in a previous review but remains unchanged. CPython's ssl.get_default_verify_paths() returns None (not empty strings) when default paths don't exist on Windows.

The comment "Return empty strings to avoid None being passed to os.path.isfile()" is not the correct rationale—os.path.isfile(None) should raise TypeError when no valid path exists, which is the expected behavior.

Apply this diff to match CPython behavior:

         #[cfg(windows)]
         let (default_cafile, default_capath) = {
             // Windows uses certificate store, not file paths
-            // Return empty strings to avoid None being passed to os.path.isfile()
-            (Some(""), Some(""))
+            // Return None to match CPython behavior when no file paths exist
+            (None, None)
         };

Based on past review comments at lines 4345-4352.

🧹 Nitpick comments (1)
crates/stdlib/src/ssl.rs (1)

4465-4514: Consider logging warnings for silently skipped certificates.

The function uses filter_map and flat_map (line 4487) which silently skips certificates that fail to process. While this may be intentional (system stores can contain unsupported formats), silently dropping certificates could hide issues during debugging.

Consider one of these optional improvements:

  • Log a warning when certificates are skipped (if logging is available)
  • Add a comment documenting that invalid/unsupported certificates are intentionally skipped
  • Collect and return error information for troubleshooting

This is a recommended enhancement for observability, not a functional issue.

📜 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 d9e1867 and c1830f4.

⛔ Files ignored due to path filters (1)
  • Lib/test/test_ssl.py is excluded by !Lib/**
📒 Files selected for processing (1)
  • crates/stdlib/src/ssl.rs (7 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/stdlib/src/ssl.rs
🧬 Code graph analysis (1)
crates/stdlib/src/ssl.rs (1)
crates/stdlib/src/ssl/cert.rs (1)
  • is_ca_certificate (270-286)
⏰ 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 snippets and cpython tests on wasm-wasi
  • GitHub Check: Check Rust code with clippy
  • GitHub Check: Run snippets and cpython tests (ubuntu-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 (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
🔇 Additional comments (3)
crates/stdlib/src/ssl.rs (3)

481-482: LGTM: Correctly allows IP addresses for server_hostname.

The relaxed validation is correct per RFC 6066, which specifies that SNI (Server Name Indication) is only sent for DNS hostnames, not IP addresses. Allowing IP addresses while documenting that SNI won't be sent matches the expected TLS behavior.


1531-1553: LGTM: Platform-specific certificate loading order is intentional.

The different loading order (Windows: system first, then env; non-Windows: env first, then system) aligns with platform conventions:

  • Windows: System certificate store is the primary trust anchor
  • Unix/Linux: Environment variables commonly override system defaults (e.g., in containers)

The test references confirm this is deliberate design.


4516-4568: Unsafe Windows API usage appears correct.

The function properly:

  • Checks for null store handle before proceeding
  • Validates CRL context pointer before dereferencing
  • Always closes the store handle (no early returns)
  • Uses slice::from_raw_parts with validated length

The unsafe blocks handle the Windows CRL enumeration API correctly. Standard unsafe code review practices apply: ensure the Windows API contracts are upheld (pointer validity, lifetime, etc.).

Comment on lines +1459 to +1520
#[cfg(windows)]
{
// Windows: Use schannel to load from both ROOT and CA stores
use schannel::cert_store::CertStore;

let store_names = ["ROOT", "CA"];
let open_fns = [CertStore::open_current_user, CertStore::open_local_machine];

for store_name in store_names {
for open_fn in &open_fns {
if let Ok(cert_store) = open_fn(store_name) {
for cert_ctx in cert_store.certs() {
let der_bytes = cert_ctx.to_der();
let cert =
rustls::pki_types::CertificateDer::from(der_bytes.to_vec());
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
}
}
}
}
}
}
}

// If there were errors but some certs loaded, just continue
// If NO certs loaded and there were errors, report the first error
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
return Err(vm.new_os_error(format!(
"Failed to load native certificates: {}",
result.errors[0]
)));
if *self.x509_cert_count.read() == 0 {
return Err(vm.new_os_error(
"Failed to load certificates from Windows store".to_owned(),
));
}

Ok(())
}

Ok(())
#[cfg(not(windows))]
{
let result = rustls_native_certs::load_native_certs();

// Load successfully found certificates
for cert in result.certs {
let is_ca = cert::is_ca_certificate(cert.as_ref());
if store.add(cert).is_ok() {
*self.x509_cert_count.write() += 1;
if is_ca {
*self.ca_cert_count.write() += 1;
}
}
}

// If there were errors but some certs loaded, just continue
// If NO certs loaded and there were errors, report the first error
if *self.x509_cert_count.read() == 0 && !result.errors.is_empty() {
return Err(vm.new_os_error(format!(
"Failed to load native certificates: {}",
result.errors[0]
)));
}

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 | 🟠 Major

Consider improving error handling to match non-Windows behavior.

The Windows implementation returns an error if x509_cert_count is 0, regardless of whether store opening succeeded. The non-Windows code (lines 1512-1517) only returns an error if both x509_cert_count == 0 AND there were errors during loading.

The non-Windows approach is more resilient—it succeeds if any certificates loaded, even if some failed. Consider updating the Windows implementation to allow partial success and provide more specific error messages (e.g., "stores exist but are empty" vs "failed to open stores").

+                let had_errors = store_names.iter().all(|&name| {
+                    open_fns.iter().all(|open_fn| open_fn(name).is_err())
+                });
+
                 if *self.x509_cert_count.read() == 0 {
+                    if !had_errors {
+                        // Stores opened but were empty - not an error
+                        return Ok(());
+                    }
                     return Err(vm.new_os_error(
                         "Failed to load certificates from Windows store".to_owned(),
                     ));
                 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In crates/stdlib/src/ssl.rs around lines 1459-1520, the Windows branch currently
returns an error whenever x509_cert_count == 0; change it to mirror the
non-Windows behavior by collecting errors while attempting to open stores and
iterate certs, and only return an error if no certs were loaded and there were
errors. Specifically: add a Vec<String> to accumulate failures from
open_fn(store_name) and any store iteration failures, continue loading any certs
that succeed and increment counts as before, and after the loops if
x509_cert_count == 0 then if errors is non-empty return a vm.os_error with the
first error, else return a vm.os_error stating the stores were opened but
contain no certificates; otherwise return Ok(()).

@youknowone youknowone merged commit cb7450d into RustPython:main Dec 7, 2025
13 checks passed
@youknowone youknowone deleted the ssl branch December 7, 2025 15:08
@coderabbitai coderabbitai bot mentioned this pull request Dec 9, 2025
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.

2 participants