Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Oct 29, 2025

Summary by CodeRabbit

  • New Features

    • Cross‑platform file-open API returning FILE* with encoded path handling and non‑inheritable descriptors.
    • Socket.recv_into accepts an optional byte-count for precise buffer sizing and validation.
    • Major SSL enhancements: SNI callbacks, BIO/memory‑BIO wrapping, post‑handshake auth, cipher descriptions, DH param loading, session/cert introspection, and richer SSL error reporting.
  • Chores

    • Added OpenSSL data generator and generated PHF-backed error/library maps; added phf dependency.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Adds a cross-platform fopen (with Windows handle-inheritance control), a script to generate PHF-backed OpenSSL error maps and generated ssl_data modules for multiple OpenSSL versions, extends recv_into with an optional nbytes, and performs a large SSL refactor introducing SNI, BIO support, unified connection abstraction, and richer OpenSSL error mapping.

Changes

Cohort / File(s) Summary
Cross-platform file handling
\common/src/fileutils.rs``
Adds pub fn fopen(path: &std::path::Path, mode: &str) -> std::io::Result<*mut libc::FILE> with EINTR retry and platform branches; adds Windows-only fn set_inheritable(fd: libc::c_int, inheritable: bool) -> std::io::Result<()>.
SSL data generation script
\scripts/make_ssl_data_rs.py``
New CLI script parsing OpenSSL sources (err.h, openssl.txt, openssl.ec) and emitting Rust PHF maps (LIBRARY_CODES, ERROR_CODES) and encode_error_key; writes output and prints a summary.
Dependency addition
\stdlib/Cargo.toml``
Adds dependency phf = "0.11" with features = ["macros"].
Socket API extension
\stdlib/src/socket.rs``
Extends PySocket.recv_into signature with nbytes: OptionalArg<isize>; computes/clamps read_len, slices the buffer accordingly, and errors on negative nbytes.
SSL module refactor & features
\stdlib/src/ssl.rs``
Major refactor: OpenSSL-version conditional modules; adds SNI ex_data and callbacks, PySslMemoryBio/BioStream, SslConnection enum (Socket
Generated SSL error maps (1.1.1)
\stdlib/src/ssl/ssl_data_111.rs``
Adds PHF LIBRARY_CODES and ERROR_CODES maps and encode_error_key(lib, reason) -> u64 helper generated from OpenSSL 1.1.1 data.
Generated SSL error maps (3.0)
\stdlib/src/ssl/ssl_data_300.rs``
Adds PHF LIBRARY_CODES and ERROR_CODES maps and encode_error_key(lib, reason) -> u64 helper generated from OpenSSL 3.0 data.
Generated SSL error maps (3.1)
\stdlib/src/ssl/ssl_data_31.rs``
Adds PHF LIBRARY_CODES and ERROR_CODES maps and encode_error_key(lib, reason) -> u64 helper generated from OpenSSL 3.1 data.

Sequence Diagram(s)

sequenceDiagram
    participant Py as Python
    participant Ctx as PySslContext
    participant SSL as OpenSSL
    participant CB as SNI callback

    Py->>Ctx: set_sni_callback(callback)
    activate Ctx
    Ctx->>Ctx: store callback in sni_callback (ex_data)
    deactivate Ctx

    Py->>SSL: start TLS handshake
    activate SSL
    SSL->>CB: invoke servername callback
    activate CB
    CB->>Ctx: lookup Python callback via ex_data
    CB->>Py: call(callback, servername, ctx)
    Note right of CB: log Python exception if raised
    deactivate CB
    SSL->>SSL: continue handshake
    deactivate SSL
Loading
sequenceDiagram
    participant App as Application
    participant fopen as fileutils::fopen()
    participant OS as libc/WinAPI

    App->>fopen: fopen(path, mode)
    activate fopen
    loop retry on EINTR
        fopen->>OS: open file (File::open)
        alt Windows
            fopen->>OS: _open_osfhandle / _fdopen -> FILE*
            fopen->>OS: SetHandleInformation to make non-inheritable
        else Unix
            fopen->>OS: into_raw_fd / fdopen -> FILE*
            Note right of fopen: relies on O_CLOEXEC set by File::open
        end
        alt Success
            fopen->>App: return FILE*
        else EINTR
            fopen->>fopen: retry
        else Failure
            fopen->>App: return io::Error
        end
    end
    deactivate fopen
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Areas needing careful review:
    • stdlib/src/ssl.rs: thread-safety, ex_data lifecycle, BIO vs socket I/O semantics, and verify/error mapping.
    • common/src/fileutils.rs: Windows _open_osfhandle / _fdopen usage and inheritance flag handling.
    • scripts/make_ssl_data_rs.py and generated ssl_data_*.rs: parsing correctness, duplicate handling, and key encoding.
    • stdlib/src/socket.rs: buffer-slicing semantics and interaction with existing memory model.

Possibly related PRs

Suggested reviewers

  • coolreader18
  • ShaharNaveh
  • arihant2math

Poem

🐇
I hopped through bytes beneath the build-night sky,
Opened files and stitched their handles shy,
I listened for SNI whispers on the wire,
Mapped errors like stars to guide the fire,
Carrots and commits — a cozy, coded sigh. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "more ssl impl" is partially related to the changeset. The PR's dominant focus is SSL implementation work in stdlib/src/ssl.rs, which includes major enhancements such as SNI callback support, PySslMemoryBio wrapping, extended context methods, and error handling improvements. This work is supported by the Python script for generating OpenSSL error maps, the generated data files (ssl_data_111.rs, ssl_data_300.rs, ssl_data_31.rs), and the phf dependency addition. However, the title omits secondary but notable changes to stdlib/src/socket.rs (recv_into parameter) and common/src/fileutils.rs (file opening utilities), so it doesn't capture the complete changeset scope.
✨ 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.

@youknowone youknowone marked this pull request as ready for review October 29, 2025 10:02
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: 6

Caution

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

⚠️ Outside diff range comments (1)
stdlib/src/ssl.rs (1)

193-205: HAS_SSLv2/HAS_SSLv3 should reflect actual support.

Modern OpenSSL builds disable SSLv2/v3. Advertising true misleads Python code.

Apply this diff:

-    const HAS_SSLv2: bool = true;
-    const HAS_SSLv3: bool = true;
+    // Reflect actual build support (v2/v3 are almost always disabled)
+    const HAS_SSLv2: bool = false;
+    const HAS_SSLv3: bool = false;
🧹 Nitpick comments (13)
stdlib/Cargo.toml (1)

43-43: phf addition looks fine; consider gating under ssl feature to avoid extra deps for non‑SSL builds.

Move phf under an optional dependency and include it in the existing ssl feature to cut compile time for builds without SSL.

Example:

-[dependencies]
+[dependencies]
@@
-phf = { version = "0.11", features = ["macros"] }
+phf = { version = "0.11", features = ["macros"], optional = true }
@@
 [features]
 ssl = ["openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"]
+ssl = ["dep:phf", "openssl", "openssl-sys", "foreign-types-shared", "openssl-probe"]
common/src/fileutils.rs (2)

460-471: EINTR retry on Windows is unnecessary.

_wfopen on Windows doesn’t meaningfully return EINTR; looping adds complexity without benefit. Optional cleanup: drop the retry loop in the Windows branch while keeping it on Unix.


489-494: Prefer OsStrExt::as_bytes() within Unix-gated code for idiomatic platform-specific handling.

The original MSRV concern is incorrect—as_encoded_bytes() was stabilized in Rust 1.74.0, well before the repository's MSRV of 1.89.0. However, since this code is already guarded with #[cfg(not(windows))], using the Unix-idiomatic OsStrExt::as_bytes() is preferred for direct OS-level byte access within Unix-specific contexts.

    #[cfg(not(windows))]
    {
        // Convert path to CString
+       use std::os::unix::ffi::OsStrExt;
-       let path_cstring = CString::new(path.as_os_str().as_encoded_bytes())
+       let path_cstring = CString::new(path.as_os_str().as_bytes())
            .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "invalid path"))?;
stdlib/src/ssl/ssl_data_31.rs (3)

1-6: Add provenance details in header for reproducibility.

Include OpenSSL version/commit and the generator script version to make diffs auditable.


69-1763: Large PHF map: consider feature‑gating and/or splitting to reduce compile time for non‑SSL builds.

Keeping data generated and static is fine; gating the module behind ssl keeps builds lean.


1766-1769: Deduplicate encode_error_key across versions.

Move this helper to a shared ssl module and re‑export to avoid repetition.

stdlib/src/ssl/ssl_data_111.rs (3)

1-6: Add provenance details in header for reproducibility.

Note OpenSSL 1.1.1 exact tag/commit and generator script version.


61-1340: Same suggestion as 3.1: gate and/or split to reduce compile overhead when SSL is disabled.


1342-1346: Deduplicate encode_error_key across versions.

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

7-14: Add a tiny sanity-test in-tree (optional).

Consider a unit test that spot-checks a few encode_error_key lookups across libs (e.g., SSL 20/134) to catch generator drift.

Also applies to: 66-74

scripts/make_ssl_data_rs.py (2)

71-87: Clarify dedup behavior for lib codes.

Comment says “keep the last one,” but iteration is by sorted(libname), so “alphabetically last name wins,” not “last in source file.” Either adjust the comment or switch to source-order tracking if that was intended.


130-137: Fix generator header path (nit).

Header says tools/make_ssl_data_rs.py; file resides under scripts/. Update for accuracy.

stdlib/src/ssl.rs (1)

1408-1417: SNI callback setter: ensure arg cleared when disabling.

When clearing the callback, also clear the tlsext servername arg to avoid dangling context pointers.

Apply this diff:

                 } else {
                     // Clear callback
                     *callback_guard = None;
                     unsafe {
                         sys::SSL_CTX_set_tlsext_servername_callback__fixed_rust(
                             self.ctx().as_ptr(),
                             None,
                         );
+                        sys::SSL_CTX_set_tlsext_servername_arg(self.ctx().as_ptr(), std::ptr::null_mut());
                     }
                 }
📜 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 fda9cee and 516df51.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • Lib/test/test_httplib.py is excluded by !Lib/**
  • Lib/test/test_ssl.py is excluded by !Lib/**
  • Lib/test/test_urllib2_localnet.py is excluded by !Lib/**
📒 Files selected for processing (8)
  • common/src/fileutils.rs (1 hunks)
  • scripts/make_ssl_data_rs.py (1 hunks)
  • stdlib/Cargo.toml (1 hunks)
  • stdlib/src/socket.rs (1 hunks)
  • stdlib/src/ssl.rs (45 hunks)
  • stdlib/src/ssl/ssl_data_111.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_300.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_31.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

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

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl/ssl_data_111.rs
  • common/src/fileutils.rs
  • stdlib/src/socket.rs
  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/ssl.rs
{vm,stdlib}/**/*.rs

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

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/socket.rs
  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/ssl.rs
🧬 Code graph analysis (6)
stdlib/src/ssl/ssl_data_111.rs (2)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1756-1758)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1767-1769)
common/src/fileutils.rs (3)
common/src/os.rs (2)
  • last_os_error (23-34)
  • last_os_error (40-42)
vm/src/stdlib/nt.rs (1)
  • set_inheritable (93-100)
vm/src/stdlib/posix.rs (2)
  • set_inheritable (6-15)
  • set_inheritable (956-958)
stdlib/src/socket.rs (1)
vm/src/builtins/memory.rs (1)
  • nbytes (575-577)
stdlib/src/ssl/ssl_data_31.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1344-1346)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1756-1758)
stdlib/src/ssl/ssl_data_300.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1344-1346)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1767-1769)
stdlib/src/ssl.rs (4)
common/src/fileutils.rs (4)
  • path (77-79)
  • std (318-326)
  • std (336-336)
  • fopen (445-522)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1344-1346)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1756-1758)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1767-1769)
🪛 Flake8 (7.3.0)
scripts/make_ssl_data_rs.py

[error] 20-20: 'sys' imported but unused

(F401)

🪛 GitHub Check: Check Rust code with rustfmt and clippy
stdlib/src/ssl/ssl_data_111.rs

[warning] 257-257:
Unknown word (RECIPIENTINFO)


[warning] 195-195:
Unknown word (NBIO)


[warning] 109-109:
Unknown word (BMPSTRING)


[warning] 67-67:
Unknown word (BMPSTRING)


[warning] 49-49:
Unknown word (JPAKE)


[warning] 44-44:
Unknown word (OSSL)

common/src/fileutils.rs

[warning] 462-462:
Unknown word (wmode)


[warning] 462-462:
Unknown word (wpath)


[warning] 457-457:
Unknown word (wmode)


[warning] 454-454:
Unknown word (wpath)

⏰ 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). (6)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Run rust tests (macos-latest)
  • GitHub Check: Run rust tests (windows-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
🔇 Additional comments (5)
stdlib/src/socket.rs (1)

1198-1224: recv_into nbytes semantics match CPython; good change.

  • Negative nbytes -> ValueError.
  • nbytes clamped to buffer length.
  • Slice narrowed before recv. All correct.

Add a quick test to lock behavior (clamp vs. error) and negative case.

Example test outline (Python):

import socket, array
s1, s2 = socket.socketpair()
try:
    s1.send(b"abcd")
    buf = array.array('b', b'\x00'*2)
    assert s2.recv_into(buf, 10) == 2  # clamped
    try:
        s2.recvfrom_into(buf, 10)  # should error
    except ValueError:
        pass
    else:
        raise AssertionError("expected ValueError")
finally:
    s1.close(); s2.close()
stdlib/src/ssl/ssl_data_31.rs (1)

9-64: LIBRARY_CODES map: LGTM.

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

9-55: LIBRARY_CODES map: LGTM.

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

69-74: LGTM: PHF maps and key encoding helper are consistent with other versions.

The example key, encoding, and helper match ssl_data_111.rs/ssl_data_31.rs. No issues.

Also applies to: 1754-1758

stdlib/src/ssl.rs (1)

219-222: PHA configuration: minor robustness.

When setting POST_HANDSHAKE on servers, ensure SSL_VERIFY_PEER mask uses libc::c_int consistently; on clients, guard SSL_set_post_handshake_auth under ossl111 as you do. Looks good overall.

Also applies to: 1511-1533, 1650-1672

@youknowone youknowone force-pushed the more-ssl branch 2 times, most recently from ea0d242 to 56b9ede Compare October 29, 2025 11:05
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 (3)
stdlib/src/ssl.rs (2)

619-619: Critical: Still using hardcoded ex_data index 0 for PySslSocket pointer.

This was flagged in a previous review but remains unaddressed. Using index 0 without allocation risks collisions with other OpenSSL users. While you correctly allocate a dedicated index for SNI callback data at line 540-552, the SSL socket pointer storage at lines 619, 1563, and 1696 still uses hardcoded 0.

Allocate a dedicated ex_data index for storing the PySslSocket pointer, similar to how get_sni_ex_data_index() works:

+    // Get or create an ex_data index for storing PySslSocket*
+    fn get_ssl_socket_ex_data_index() -> libc::c_int {
+        use std::sync::LazyLock;
+        static SSL_SOCKET_EX_DATA_IDX: LazyLock<libc::c_int> = LazyLock::new(|| unsafe {
+            sys::SSL_get_ex_new_index(0, std::ptr::null_mut(), None, None, None)
+        });
+        *SSL_SOCKET_EX_DATA_IDX
+    }

Then replace all three occurrences:

-            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, 0); // Index 0 for SSL socket
+            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, get_ssl_socket_ex_data_index());
-                    sys::SSL_set_ex_data(ssl_ptr, 0, &*py_ref as *const _ as *mut _);
+                    sys::SSL_set_ex_data(ssl_ptr, get_ssl_socket_ex_data_index(), &*py_ref as *const _ as *mut _);

Also applies to: 1563-1563, 1696-1696


1058-1072: cfg gate still incorrect for num_tickets methods.

Despite being marked as "✅ Addressed" in the past review, lines 1061, 1067, 1087, and 1096 still use #[cfg(ossl110)] instead of #[cfg(ossl111)]. The functions SSL_CTX_get_num_tickets and SSL_CTX_set_num_tickets were introduced in OpenSSL 1.1.1, not 1.1.0.

Apply this diff:

-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
             {
                 let ctx = self.ctx();
                 let num = unsafe { sys::SSL_CTX_get_num_tickets(ctx.as_ptr()) };
                 Ok(num)
             }
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]
             {
                 let _ = vm;
                 Ok(0)
             }
-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
             {
                 let ctx = self.builder();
                 let result = unsafe { sys::SSL_CTX_set_num_tickets(ctx.as_ptr(), value as usize) };
                 if result != 1 {
                     return Err(vm.new_value_error("failed to set num tickets."));
                 }
                 Ok(())
             }
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]
             {
                 let _ = (value, vm);
                 Ok(())
             }

Also applies to: 1073-1101

common/src/fileutils.rs (1)

475-476: Use crate::os::last_os_error() for reliable Windows error mapping (CRT errno vs GetLastError).

Replace std::io::Error::last_os_error() with crate::os::last_os_error() at these sites to avoid code 0 from CRT-only failures.

-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
@@
-            unsafe { libc::close(fd) };
-            return Err(std::io::Error::last_os_error());
+            unsafe { libc::close(fd) };
+            return Err(crate::os::last_os_error());
@@
-        return Err(std::io::Error::last_os_error());
+        return Err(crate::os::last_os_error());
@@
-        return Err(std::io::Error::last_os_error());
+        return Err(crate::os::last_os_error());

Run to catch any remaining instances:

#!/bin/bash
rg -nP --type=rust -C1 '\bstd::io::Error::last_os_error\s*\(' common/src/fileutils.rs

Also applies to: 481-484, 526-527, 531-533

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

1-4: Stabilize generator header for reproducible builds.

Drop the wall-clock timestamp and avoid embedding ephemeral source paths like “/tmp/openssl-3.1”. Make the header deterministic (e.g., version/commit only). Also, the path says tools/make_ssl_data_rs.py; please verify the actual script location and make it consistent across files.


8-10: Use doc comments on public items.

Convert these explanatory comments to /// so they appear in generated docs for LIBRARY_CODES and ERROR_CODES.

Also applies to: 67-70


1768-1770: Make encode_error_key const, take u32, and mask both halves.

Prevents accidental sign issues and allows compile-time evaluation.

-#[inline]
-pub fn encode_error_key(lib: i32, reason: i32) -> u64 {
-    ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)
-}
+#[inline]
+pub const fn encode_error_key(lib: u32, reason: u32) -> u64 {
+    ((lib as u64 & 0xFFFF_FFFF) << 32) | (reason as u64 & 0xFFFF_FFFF)
+}

If call sites pass signed ints, adjust them to u32 (or cast once at the call site).


70-1754: Consider cfg-gating to compile only one version’s table.

These maps are large and repeated across versions; compiling only the active OpenSSL mapping can cut compile time and binary size.

Confirm that the top-level selection already gates this; if not, add a cargo feature or cfg guard to pick exactly one ssl_data_* at build time.

stdlib/src/ssl/ssl_data_111.rs (4)

1-4: Deterministic header, and path consistency.

Remove the timestamp, avoid /tmp/… paths, and ensure the generator path string matches the actual script location.


8-10: Promote comments to rustdoc.

Switch // to /// above the exported statics so users get inline docs.

Also applies to: 58-61


1345-1347: Harden and const-ify encode_error_key.

Mirror the change proposed in the 3.1 file for type safety and const evaluation.

-#[inline]
-pub fn encode_error_key(lib: i32, reason: i32) -> u64 {
-    ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)
-}
+#[inline]
+pub const fn encode_error_key(lib: u32, reason: u32) -> u64 {
+    ((lib as u64 & 0xFFFF_FFFF) << 32) | (reason as u64 & 0xFFFF_FFFF)
+}

61-1341: Optional: compile-time gating to avoid duplicate tables.

If only one OpenSSL flavor is used per build, gate the others with cfg/features to save compile time and binary size.

stdlib/src/ssl/ssl_data_300.rs (4)

1-4: Make generation metadata reproducible.

Strip timestamps and non-reproducible source paths; keep only OpenSSL version/commit and generator name.


8-10: Prefer rustdoc for public statics.

Change these descriptive // comments to /// above LIBRARY_CODES and ERROR_CODES.

Also applies to: 67-70


1757-1759: Const + u32 params for encode_error_key.

Align with the other modules; safer types and const evaluable.

-#[inline]
-pub fn encode_error_key(lib: i32, reason: i32) -> u64 {
-    ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)
-}
+#[inline]
+pub const fn encode_error_key(lib: u32, reason: u32) -> u64 {
+    ((lib as u64 & 0xFFFF_FFFF) << 32) | (reason as u64 & 0xFFFF_FFFF)
+}

70-1753: Feature/cfg gating to limit compiled datasets.

If runtime doesn’t need all versions, add cfg selection to compile exactly one dataset.

common/src/fileutils.rs (2)

479-481: Avoid CString allocation for constant "rb".

Since only "rb" is accepted, pass a static C string literal.

-        let mode_cstr = CString::new(mode).unwrap();
-        let fp = unsafe { _fdopen(fd, mode_cstr.as_ptr()) };
+        let fp = unsafe { _fdopen(fd, b"rb\0".as_ptr() as *const libc::c_char) };
@@
-        let mode_cstr = CString::new(mode).unwrap();
-        let fp = unsafe { libc::fdopen(fd, mode_cstr.as_ptr()) };
+        let fp = unsafe { libc::fdopen(fd, b"rb\0".as_ptr() as *const libc::c_char) };

Also applies to: 503-505


506-508: Consistent error helper across platforms (optional).

For consistency with this file’s other paths, consider using crate::os::last_os_error() here too.

-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
📜 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 fe8a849 and 56b9ede.

📒 Files selected for processing (6)
  • common/src/fileutils.rs (1 hunks)
  • scripts/make_ssl_data_rs.py (1 hunks)
  • stdlib/src/ssl.rs (44 hunks)
  • stdlib/src/ssl/ssl_data_111.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_300.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_31.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/make_ssl_data_rs.py
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

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

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • common/src/fileutils.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl.rs
  • stdlib/src/ssl/ssl_data_31.rs
{vm,stdlib}/**/*.rs

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

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl.rs
  • stdlib/src/ssl/ssl_data_31.rs
🔇 Additional comments (7)
stdlib/src/ssl.rs (7)

1981-1996: Verify SSL_set_SSL_CTX usage and consider error handling.

The set_context method calls SSL_set_SSL_CTX which is a non-trivial operation that changes the SSL context of an active SSL connection. This can fail or have side effects depending on the SSL connection state.

Concerns:

  1. Handshake state: Can this be called mid-handshake or after handshake? CPython may restrict when this can be called.
  2. NULL return: Line 1989 checks for NULL but only returns a generic error. Consider capturing and converting the OpenSSL error stack.
  3. Session compatibility: Should this check if a session is in progress and whether the new context is compatible?

Apply this improvement:

             // Set the new SSL_CTX on the SSL object
             unsafe {
                 let result = SSL_set_SSL_CTX(stream.ssl().as_ptr(), value.ctx().as_ptr());
                 if result.is_null() {
-                    return Err(vm.new_runtime_error("Failed to set SSL context".to_owned()));
+                    return Err(convert_openssl_error(vm, openssl::error::ErrorStack::get()));
                 }
             }

Verify CPython's _ssl.c to see if there are additional checks on when context can be reassigned (e.g., not after handshake, only for TLS, etc.).


1323-1338: File handle leaks on error path after PEM_read_DHparams.

If PEM_read_DHparams returns NULL (line 1336), the code calls convert_openssl_error which returns early at line 1337, but fclose(fp) was already called at line 1333. However, if PEM_read_DHparams fails, the early return at line 1337 means we correctly don't proceed to use the null dh pointer.

Wait, re-reading: the fclose at line 1333 is AFTER the PEM_read_DHparams call (lines 1325-1330), so the file is always closed. But if dh.is_null() at line 1336, we return at line 1337. This is correct—no leak.

Actually the flow is correct. The file is closed at line 1333, then we check if dh is null and return error if so. No issues here.


527-650: Verify lifetime safety of vm_ptr in SniCallbackData.

The SniCallbackData struct stores a raw pointer to VirtualMachine at line 530, which is dereferenced in the SNI callback at line 604. While the comment at lines 602-604 claims this is safe because "the handshake happens synchronously in the same thread," this depends on the SSL connection lifecycle.

Potential issues:

  1. Lifetime guarantee: Is the VM guaranteed to outlive the SSL connection? If the SSL object is stored and used after the original wrapping call returns, the VM pointer could be dangling.
  2. Thread safety: The comment assumes synchronous handshake, but what if the socket is passed to another thread?
  3. Drop order: When PySslSocket is dropped, is the VM still valid?

Recommendations:

  • Document the lifetime contract clearly: "vm_ptr must remain valid for the entire SSL connection lifetime"
  • Consider storing a reference-counted VM handle if available, rather than a raw pointer
  • Add debug assertions in development builds to detect use-after-free

The current implementation may be safe if:

  • The SSL socket holds a reference to the Python socket which keeps the VM alive
  • Handshakes only happen synchronously during method calls from Python

Please verify these assumptions hold in all code paths.


3275-3291: LGTM: cipher_description implementation is safe.

The function allocates a 256-byte buffer for SSL_CIPHER_description, which is more than the 128 bytes mentioned in the comment (line 3277) but provides a safety margin. The null-terminator search at line 3288 ensures safe string conversion.

Good defensive programming with the oversized buffer and proper null-terminator handling.


1490-1492: Good: null byte check in server_hostname.

Adding the null byte check for server_hostname at lines 1490-1492 (in _wrap_socket) and 1614-1616 (in _wrap_bio) prevents potential security issues with embedded null bytes in hostnames. This is a good defensive practice.

Also applies to: 1614-1616


2501-2508: Good: special-case handling for zero-byte reads.

The early return for n == 0 at lines 2501-2508 is a good optimization that avoids unnecessary SSL and socket operations when no data is requested. This matches expected behavior where reading 0 bytes should immediately return 0 or an empty bytes object.


782-782: Verify BASETYPE flag is appropriate for PySslContext.

Line 782 marks PySslContext with both BASETYPE and IMMUTABLETYPE flags, while other SSL types (PySslSocket, PySslMemoryBio, PySslSession) use only IMMUTABLETYPE. The BASETYPE flag allows Python subclassing.

Questions:

  1. Does CPython's _ssl.SSLContext allow subclassing? If not, remove BASETYPE.
  2. Is it safe to subclass SSLContext given it holds SslContextBuilder and uses unsafe FFI?
  3. Do any internal implementations rely on the type being final/non-subclassable?

Check CPython's implementation to confirm whether SSLContext should be subclassable. If it should not be, apply this diff:

-    #[pyclass(flags(BASETYPE, IMMUTABLETYPE), with(Constructor))]
+    #[pyclass(flags(IMMUTABLETYPE), with(Constructor))]
     impl PySslContext {

Also applies to: 1960-1960, 2892-2892, 2963-2963

Comment on lines +471 to +477
// Convert File handle to CRT file descriptor
let handle = file.into_raw_handle();
let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY) };
if fd == -1 {
return Err(std::io::Error::last_os_error());
}

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

Fix Windows HANDLE leak on _open_osfhandle failure; pass O_BINARY for "rb".

  • If _open_osfhandle returns -1, the raw HANDLE from into_raw_handle() is leaked. Close it with CloseHandle.
  • For binary read mode, pass _O_BINARY to prevent text-mode translation.

Apply:

-        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY) };
-        if fd == -1 {
-            return Err(std::io::Error::last_os_error());
-        }
+        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY | libc::O_BINARY) };
+        if fd == -1 {
+            // _open_osfhandle did not take ownership; close the raw HANDLE to avoid a leak.
+            unsafe { windows_sys::Win32::Foundation::CloseHandle(handle as _) };
+            return Err(crate::os::last_os_error());
+        }

(Optional: also add _O_NOINHERIT if available; not required since you call SetHandleInformation later.)

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

🤖 Prompt for AI Agents
In common/src/fileutils.rs around lines 471 to 477, if _open_osfhandle returns
-1 the raw HANDLE obtained from file.into_raw_handle() is leaked and the file is
opened in text mode; fix by calling CloseHandle(handle) before returning the
error to avoid the HANDLE leak, and pass _O_BINARY (and optionally _O_NOINHERIT
if desired) combined with libc::O_RDONLY to _open_osfhandle so the descriptor is
opened in binary read mode (and non-inheritable if used).

Comment on lines 3030 to 3054
// Check if this is a system library error (file not found, etc.)
let lib = sys::ERR_GET_LIB(e.code());
let library_str = e.library().unwrap_or("");

// Check both lib code and library string for system errors
if lib == sys::ERR_LIB_SYS || library_str == "SSL routines" {
let reason = sys::ERR_GET_REASON(e.code());
let reason_str = e.reason().unwrap_or("");

// Check if it's a system lib error by reason string
if reason_str == "system lib" || lib == sys::ERR_LIB_SYS {
// errno 2 = ENOENT = FileNotFoundError
let exc_type = if reason == 2 {
vm.ctx.exceptions.file_not_found_error.to_owned()
} else {
vm.ctx.exceptions.os_error.to_owned()
};
let exc = vm.new_exception(exc_type, vec![vm.ctx.new_int(reason).into()]);
// Set errno attribute explicitly
let _ = exc
.as_object()
.set_attr("errno", vm.ctx.new_int(reason), vm);
return exc;
}
}
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

System error detection logic is overly complex and potentially incorrect.

The condition at line 3035 attempts to detect system library errors by checking if lib == ERR_LIB_SYS OR library_str == "SSL routines", but "SSL routines" is not a system library—it's the SSL library. Then it checks if reason_str == "system lib" OR lib == ERR_LIB_SYS again (line 3040). This double-checks ERR_LIB_SYS and conflates SSL library errors with system errors.

Simplify the logic to clearly distinguish system errors:

-                // Check both lib code and library string for system errors
-                if lib == sys::ERR_LIB_SYS || library_str == "SSL routines" {
-                    let reason = sys::ERR_GET_REASON(e.code());
-                    let reason_str = e.reason().unwrap_or("");
-
-                    // Check if it's a system lib error by reason string
-                    if reason_str == "system lib" || lib == sys::ERR_LIB_SYS {
+                // Check if this is a system library error (errno-based)
+                if lib == sys::ERR_LIB_SYS {
+                    let reason = sys::ERR_GET_REASON(e.code());
+                    {
                         // errno 2 = ENOENT = FileNotFoundError
                         let exc_type = if reason == 2 {
                             vm.ctx.exceptions.file_not_found_error.to_owned()

This removes the confusing intermediate checks and directly detects system library errors by ERR_LIB_SYS.

🤖 Prompt for AI Agents
In stdlib/src/ssl.rs around lines 3030 to 3054, the system error detection
conflates SSL library errors with system errors by checking library_str == "SSL
routines" and redundantly testing ERR_LIB_SYS twice; update the logic to only
treat an error as a system error when the lib returned by
sys::ERR_GET_LIB(e.code()) == sys::ERR_LIB_SYS or when reason_str == "system
lib", remove the library_str == "SSL routines" check and the duplicate lib
check, and keep the existing errno-to-exception mapping and errno attribute
setting otherwise.

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

♻️ Duplicate comments (9)
common/src/fileutils.rs (4)

504-508: Use crate::os::last_os_error() consistently across the codebase.

While less critical on Unix than Windows, the previous review comment requested consistent use of crate::os::last_os_error() throughout fopen() for uniformity.

Apply this diff:

         if fp.is_null() {
             unsafe { libc::close(fd) };
-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
         }

471-477: Fix Windows HANDLE leak on _open_osfhandle failure and pass O_BINARY for binary mode.

This issue was previously flagged: If _open_osfhandle returns -1, the raw HANDLE from into_raw_handle() is leaked because ownership was transferred but CloseHandle is not called. Additionally, for binary read mode, O_BINARY should be passed to prevent text-mode translation.

Apply this diff:

-        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY) };
+        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY | libc::O_BINARY) };
         if fd == -1 {
+            // _open_osfhandle did not take ownership; close the raw HANDLE to avoid a leak.
+            unsafe { windows_sys::Win32::Foundation::CloseHandle(handle as _) };
-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
         }

481-484: Use crate::os::last_os_error() instead of std::io::Error::last_os_error().

This issue was previously flagged: On Windows, _fdopen sets errno but not GetLastError, making std::io::Error::last_os_error() unreliable. Use crate::os::last_os_error() which bridges this gap.

Apply this diff:

         if fp.is_null() {
             unsafe { libc::close(fd) };
-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
         }

519-536: Use crate::os::last_os_error() in set_inheritable for Windows CRT/API calls.

This issue was previously flagged: Both get_osfhandle (CRT) and SetHandleInformation (Windows API) error handling should use crate::os::last_os_error() instead of std::io::Error::last_os_error() for reliable errno/GetLastError bridging.

Apply this diff:

         let handle = unsafe { libc::get_osfhandle(fd) };
         if handle == INVALID_HANDLE_VALUE as isize {
-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
         }
     
         let flags = if inheritable { HANDLE_FLAG_INHERIT } else { 0 };
         let result = unsafe { SetHandleInformation(handle as HANDLE, HANDLE_FLAG_INHERIT, flags) };
         if result == 0 {
-            return Err(std::io::Error::last_os_error());
+            return Err(crate::os::last_os_error());
         }
scripts/make_ssl_data_rs.py (1)

20-20: Remove unused import.

Apply:

-import sys
stdlib/src/ssl.rs (4)

618-633: Don’t use ex_data index 0; allocate a dedicated index for PySslSocket.

Index 0 isn’t reserved and can collide. Create and use a dedicated index everywhere you store/fetch the PySslSocket pointer.

Apply:

+    // Get or create an ex_data index for storing PySslSocket*
+    fn get_ssl_socket_ex_data_index() -> libc::c_int {
+        use std::sync::LazyLock;
+        static IDX: LazyLock<libc::c_int> = LazyLock::new(|| unsafe {
+            sys::SSL_get_ex_new_index(0, std::ptr::null_mut(), None, None, None)
+        });
+        *IDX
+    }
@@
-            // Get SSL socket from SSL ex_data (stored as PySslSocket pointer)
-            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, 0); // Index 0 for SSL socket
+            // Get SSL socket from SSL ex_data (stored as PySslSocket pointer)
+            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, get_ssl_socket_ex_data_index());
@@
-                    // Store PyRef pointer (heap-allocated) in ex_data index 0
-                    sys::SSL_set_ex_data(ssl_ptr, 0, &*py_ref as *const _ as *mut _);
+                    // Store PySslSocket pointer in its dedicated ex_data index
+                    sys::SSL_set_ex_data(
+                        ssl_ptr,
+                        get_ssl_socket_ex_data_index(),
+                        &*py_ref as *const _ as *mut _,
+                    );
@@
-                    // Store PyRef pointer (heap-allocated) in ex_data index 0
-                    sys::SSL_set_ex_data(ssl_ptr, 0, &*py_ref as *const _ as *mut _);
+                    // Store PySslSocket pointer in its dedicated ex_data index
+                    sys::SSL_set_ex_data(
+                        ssl_ptr,
+                        get_ssl_socket_ex_data_index(),
+                        &*py_ref as *const _ as *mut _,
+                    );
#!/bin/bash
# Find any remaining uses of ex_data index 0 under ssl.rs
rg -n 'SSL_(get|set)_ex_data\([^,]+,\s*0\b' stdlib/src/ssl.rs -n -C2

Also applies to: 1550-1565, 1695-1697


1003-1015: Fix sentinel mapping for minimum_version/maximum_version (map -2/-1 to 0).

Both setters must translate Python sentinels to OpenSSL’s “unset/default” value (0). Current code forces concrete TLS versions.

Apply:

-            let proto_version = match value {
-                -2 => { sys::TLS1_2_VERSION }
-                -1 => { sys::TLS1_3_VERSION }
-                _ => value,
-            };
+            let proto_version = match value {
+                -2 | -1 => 0, // clear limit; let OpenSSL choose min/max
+                _ => value,
+            };
-            let proto_version = match value {
-                -1 => { 0 }
-                -2 => { sys::TLS1_2_VERSION }
-                _ => value,
-            };
+            let proto_version = match value {
+                -1 | -2 => 0, // clear limit; let OpenSSL choose min/max
+                _ => value,
+            };

Also applies to: 1036-1048


1058-1072: Gate num_tickets on OpenSSL 1.1.1+, not 1.1.0.

SSL_CTX_{get,set}_num_tickets were added in 1.1.1. Using ossl110 will break on 1.1.0.

Apply:

-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
@@
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]

and:

-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
@@
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]
#!/bin/bash
# Check cfg gates around num_tickets use
rg -n 'SSL_CTX_(get|set)_num_tickets|num_tickets\(' stdlib/src/ssl.rs -n -C2

Also applies to: 1074-1101


3030-3054: Simplify and correct system error detection to only match ERR_LIB_SYS.

Current logic conflates SSL library errors with system errors and double-checks ERR_LIB_SYS. Restrict to lib == ERR_LIB_SYS.

Apply:

-                // Check both lib code and library string for system errors
-                if lib == sys::ERR_LIB_SYS || library_str == "SSL routines" {
+                // System library error (errno-based)
+                if lib == sys::ERR_LIB_SYS {
                     let reason = sys::ERR_GET_REASON(e.code());
-                    let reason_str = e.reason().unwrap_or("");
-
-                    // Check if it's a system lib error by reason string
-                    if reason_str == "system lib" || lib == sys::ERR_LIB_SYS {
+                    {
                         // errno 2 = ENOENT = FileNotFoundError
                         let exc_type = if reason == 2 {
                             vm.ctx.exceptions.file_not_found_error.to_owned()
                         } else {
                             vm.ctx.exceptions.os_error.to_owned()
                         };
                         let exc = vm.new_exception(exc_type, vec![vm.ctx.new_int(reason).into()]);
                         // Set errno attribute explicitly
                         let _ = exc
                             .as_object()
                             .set_attr("errno", vm.ctx.new_int(reason), vm);
                         return exc;
-                    }
+                    }
                 }
🧹 Nitpick comments (9)
stdlib/src/ssl/ssl_data_31.rs (2)

1-8: Mark generated file to avoid rustfmt/clippy churn.

Add file-scoped attributes so CI isn’t noisy on unreadable numerals and we skip formatting of generated tables.

Apply:

-// File generated by tools/make_ssl_data_rs.py
+// File generated by tools/make_ssl_data_rs.py
+#![allow(clippy::unreadable_literal)]
+#![rustfmt::skip]
 // Generated on 2025-10-29T07:17:16.621749+00:00
 // Source: OpenSSL from /tmp/openssl-3.1
 // spell-checker: disable

1766-1770: Avoid helper duplication across ssl_data_ modules.*

encode_error_key is identical in each versioned file. Consider moving it to a shared ssl module and reusing it from here to reduce duplication.

stdlib/src/ssl/ssl_data_111.rs (2)

1-8: Silence clippy on large numerals and skip formatting for generated tables.

Same rationale as ssl_data_31.rs.

Apply:

-// File generated by tools/make_ssl_data_rs.py
+// File generated by tools/make_ssl_data_rs.py
+#![allow(clippy::unreadable_literal)]
+#![rustfmt::skip]
 // Generated on 2025-10-29T07:17:23.692784+00:00
 // Source: OpenSSL from /tmp/openssl-1.1.1
 // spell-checker: disable

1343-1347: Deduplicate encode_error_key to cert.rs or a shared utilities module.

The function is identically defined in stdlib/src/ssl/ssl_data_31.rs:1768, ssl_data_111.rs:1345, and ssl_data_300.rs:1757. Move to a shared location and re-export from versioned files. Note: stdlib/src/ssl/mod.rs does not exist; consider cert.rs as the shared location or create a dedicated utilities file. After deduplication, manually verify clippy runs without warnings.

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

1-5: Optional: make generated file more stable and lint-quiet.

To reduce churn and lints:

  • Drop or gate the timestamp line.
  • Add allow attributes at top:
+#![allow(clippy::all)]
+#![allow(dead_code)]
scripts/make_ssl_data_rs.py (3)

127-129: Sorting doc vs behavior mismatch — pick one.

Comment says “sort by libname, numeric error code” but code sorts by reason then number. Prefer lib then number for stable grouping.

Apply one of:

  • Fix code:
-    args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
+    # sort by libname then numeric error code
+    args.reasons = sorted(reasons, key=operator.itemgetter(1, 3))
  • Or update the comment to reflect actual sort by reason.

90-104: Protect against duplicate (lib, reason) keys when merging openssl.txt and openssl.ec.

Duplicate keys produce duplicate entries in the phf_map literal. Dedup by last-wins for deterministic output.

Apply:

 def gen_error_codes_rust(args):
     """Generate Rust phf map for error codes"""
     yield "// Maps encoded (lib, reason) -> error mnemonic"
     yield '// Example: encode_error_key(20, 134) -> "CERTIFICATE_VERIFY_FAILED"'
     yield "// Key encoding: (lib << 32) | reason"
     yield "pub static ERROR_CODES: phf::Map<u64, &'static str> = phf_map! {"
-    for reason, libname, errname, num in args.reasons:
-        if libname not in args.lib2errnum:
-            continue
-        lib_num = args.lib2errnum[libname]
-        # Encode (lib, reason) as single u64
-        key = (lib_num << 32) | num
-        yield f'    {key}u64 => "{errname}",'
+    entries = {}
+    for reason, libname, errname, num in args.reasons:
+        if libname not in args.lib2errnum:
+            continue
+        lib_num = args.lib2errnum[libname]
+        key = (lib_num << 32) | num
+        entries[key] = errname  # last wins
+    for key in sorted(entries.keys()):
+        yield f'    {key}u64 => "{entries[key]}",'
     yield "};"
     yield ""

131-138: Fix header path and consider dropping timestamp for reproducibility.

  • Change “tools/make_ssl_data_rs.py” to “scripts/make_ssl_data_rs.py”.
  • Optionally gate timestamp with an env var to reduce VCS noise.

Apply:

-        "// File generated by tools/make_ssl_data_rs.py",
+        "// File generated by scripts/make_ssl_data_rs.py",
-        f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
+        f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",

Or:

-        f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
+        f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
+        // Set NO_TIMESTAMP=1 to omit the previous line when reproducibility matters.
stdlib/src/ssl.rs (1)

1511-1532: BIO mode: good change to drive I/O through Read/Write; minor nit on error kinds.

Using WouldBlock for empty BIO is correct. For write errors, prefer a specific ErrorKind.

Apply:

-                if nbytes < 0 {
-                    return Err(std::io::Error::other("BIO write failed"));
-                }
+                if nbytes < 0 {
+                    return Err(std::io::Error::new(std::io::ErrorKind::Other, "BIO write failed"));
+                }

Also applies to: 1637-1658, 1829-1878

📜 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 56b9ede and b5d48e2.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • Lib/test/test_httplib.py is excluded by !Lib/**
  • Lib/test/test_ssl.py is excluded by !Lib/**
  • Lib/test/test_urllib2_localnet.py is excluded by !Lib/**
📒 Files selected for processing (8)
  • common/src/fileutils.rs (1 hunks)
  • scripts/make_ssl_data_rs.py (1 hunks)
  • stdlib/Cargo.toml (1 hunks)
  • stdlib/src/socket.rs (1 hunks)
  • stdlib/src/ssl.rs (45 hunks)
  • stdlib/src/ssl/ssl_data_111.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_300.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_31.rs (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

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

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/socket.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • common/src/fileutils.rs
  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl.rs
{vm,stdlib}/**/*.rs

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

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/socket.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl.rs
🧬 Code graph analysis (6)
stdlib/src/ssl/ssl_data_31.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1757-1759)
stdlib/src/socket.rs (1)
vm/src/builtins/memory.rs (1)
  • nbytes (575-577)
stdlib/src/ssl/ssl_data_300.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1768-1770)
common/src/fileutils.rs (4)
vm/src/stdlib/msvcrt.rs (2)
  • _open_osfhandle (91-92)
  • get_osfhandle (106-110)
common/src/os.rs (2)
  • last_os_error (23-34)
  • last_os_error (40-42)
vm/src/stdlib/nt.rs (1)
  • set_inheritable (93-100)
vm/src/stdlib/posix.rs (2)
  • set_inheritable (6-15)
  • set_inheritable (956-958)
stdlib/src/ssl/ssl_data_111.rs (2)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1757-1759)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1768-1770)
stdlib/src/ssl.rs (4)
common/src/fileutils.rs (4)
  • path (77-79)
  • std (318-326)
  • std (336-336)
  • fopen (445-493)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1757-1759)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1768-1770)
🪛 Flake8 (7.3.0)
scripts/make_ssl_data_rs.py

[error] 20-20: 'sys' imported but unused

(F401)

⏰ 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 the WASM package and demo
  • GitHub Check: Run snippets and cpython tests (windows-latest)
  • GitHub Check: Check Rust code with rustfmt and clippy
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (ubuntu-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 (5)
common/src/fileutils.rs (1)

449-456: LGTM: Mode validation is clear and appropriate.

The restriction to "rb" mode is well-documented and returns a clear error for unsupported modes. The extensibility comment provides helpful guidance for future enhancements.

stdlib/Cargo.toml (1)

18-20: Unable to verify cargo dependency tree in sandbox environment.

The verification script failed due to environment constraints (missing /proc/self/exe). While the first part showed "OK: phf not in default tree," the cargo commands cannot complete reliably.

Before applying the suggested refactor, manually verify that:

  1. phf is currently included in the default dependency tree (without explicit ssl feature)
  2. After applying the changes (making phf optional and adding dep:phf to the ssl feature), phf is no longer pulled when ssl is not enabled

Run: cargo tree -e no-dev and cargo tree -e no-dev -F ssl locally to confirm the dependency behavior.

stdlib/src/socket.rs (1)

1198-1224: recv_into: Verification complete—no API parity issues found.

Codebase scan confirms all callsites are safe: most pass only buffer (backward compatible), while Lib/ssl.py:1306 correctly passes buffer, nbytes, flags in the expected order. No instances of flags passed as 2nd positional argument. The implementation aligns with existing usage patterns.

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

6-6: LGTM on PHF usage and helper.

phf_map statics and encode_error_key are consistent with other versions.

stdlib/src/ssl.rs (1)

741-761: Setter protocol pinning: behavior matches CPython’s fixed-version protocols.

Pinning min/max when creating TLS1, TLS1_1, TLS1_2 contexts looks correct.

Comment on lines +124 to +131
reasons = []
reasons.extend(parse_openssl_error_text(args))
reasons.extend(parse_extra_reasons(args))
# sort by libname, numeric error code
args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))

lines = [
"// File generated by tools/make_ssl_data_rs.py",
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

Handle missing openssl.ec gracefully (avoid FileNotFoundError).

Guard parse_extra_reasons when crypto/err/openssl.ec is absent.

Apply:

-    reasons = []
-    reasons.extend(parse_openssl_error_text(args))
-    reasons.extend(parse_extra_reasons(args))
+    reasons = []
+    reasons.extend(parse_openssl_error_text(args))
+    if os.path.isfile(args.errcodes):
+        reasons.extend(parse_extra_reasons(args))
+    else:
+        # Older trees may not have openssl.ec; that's fine.
+        pass
📝 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
reasons = []
reasons.extend(parse_openssl_error_text(args))
reasons.extend(parse_extra_reasons(args))
# sort by libname, numeric error code
args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
lines = [
"// File generated by tools/make_ssl_data_rs.py",
reasons = []
reasons.extend(parse_openssl_error_text(args))
if os.path.isfile(args.errcodes):
reasons.extend(parse_extra_reasons(args))
else:
# Older trees may not have openssl.ec; that's fine.
pass
lines = [
"// File generated by tools/make_ssl_data_rs.py",
🤖 Prompt for AI Agents
In scripts/make_ssl_data_rs.py around lines 124 to 131, calling
parse_extra_reasons can raise FileNotFoundError when crypto/err/openssl.ec is
missing; update the code to guard this by checking for the file's existence
(e.g., os.path.exists with the expected path) or wrapping the
parse_extra_reasons call in a try/except FileNotFoundError and treating the
missing file as an empty list, so reasons.extend(parse_extra_reasons(args)) is
skipped or given [] when the openssl.ec file is absent.

Comment on lines +1 to +3
// File generated by tools/make_ssl_data_rs.py
// Generated on 2025-10-29T07:17:23.737586+00:00
// Source: OpenSSL from /tmp/openssl-3.0
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

🧩 Analysis chain

Generated-file header path is wrong (scripts vs tools).

Header says tools/make_ssl_data_rs.py but the generator lives under scripts/. Please fix the generator to emit the correct path to avoid confusion.


🏁 Script executed:

#!/bin/bash
# Search for the generator script in the repository
echo "=== Searching for make_ssl_data_rs.py ==="
fd -type f "make_ssl_data_rs.py"

echo -e "\n=== Checking scripts/ directory ==="
fd -type f . scripts/ | head -20

echo -e "\n=== Checking tools/ directory ==="
fd -type f . tools/ | head -20

echo -e "\n=== Searching for any make_ssl references ==="
rg -l "make_ssl" --type py

echo -e "\n=== Checking header of generated ssl_data file ==="
head -5 stdlib/src/ssl/ssl_data_300.rs

Length of output: 1027


🏁 Script executed:

#!/bin/bash
# Check the generator script for hardcoded path
echo "=== Checking generator script for hardcoded path ==="
rg -A2 -B2 "tools/make_ssl_data_rs.py" scripts/make_ssl_data_rs.py

echo -e "\n=== Checking all generated ssl_data files for header consistency ==="
rg "File generated by" stdlib/src/ssl/ssl_data_*.rs

echo -e "\n=== First few lines of generator script ==="
head -30 scripts/make_ssl_data_rs.py

Length of output: 1326


Fix generator to emit correct path: change tools/make_ssl_data_rs.py to scripts/make_ssl_data_rs.py.

The hardcoded header string in the generator script scripts/make_ssl_data_rs.py incorrectly references tools/make_ssl_data_rs.py. This wrong path propagates to all generated SSL data files (ssl_data_31.rs, ssl_data_300.rs, ssl_data_111.rs). Update the string in the generator to reflect the actual script location.

🤖 Prompt for AI Agents
In stdlib/src/ssl/ssl_data_300.rs around lines 1 to 3, the generated header
incorrectly states the generator path as "tools/make_ssl_data_rs.py"; update the
generator script at scripts/make_ssl_data_rs.py to emit the correct header
string "scripts/make_ssl_data_rs.py" (replace the hardcoded "tools/..." text),
then re-run the generator so ssl_data_31.rs, ssl_data_300.rs, ssl_data_111.rs
(and any other generated SSL files) contain the corrected header.

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

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

1-3: Fix generator header path; add “generated” preamble and clippy allowances via generator.

Header says tools/make_ssl_data_rs.py, but the generator lives under scripts/. Also add a clear “DO NOT EDIT” line and a minimal clippy allow to avoid unreadable-literal noise in generated constants.

Apply in scripts/make_ssl_data_rs.py (not here):

-    lines = [
-        "// File generated by tools/make_ssl_data_rs.py",
+    lines = [
+        "// Code generated by scripts/make_ssl_data_rs.py; DO NOT EDIT.",
+        "// File generated by scripts/make_ssl_data_rs.py",
         f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
         f"// Source: OpenSSL from {args.srcdir}",
         "// spell-checker: disable",
+        "#![allow(clippy::unreadable_literal)]",
         "",
         "use phf::phf_map;",
         "",
     ]
stdlib/src/ssl/ssl_data_111.rs (1)

1-3: Same header-path issue; add preamble and clippy allow via generator.

Update generator to emit scripts/make_ssl_data_rs.py and add DO NOT EDIT + clippy allow as in the 3.1 file.

See proposed diff in scripts/make_ssl_data_rs.py from the other comment.

common/src/fileutils.rs (2)

519-536: Windows: use crate::os::last_os_error() in set_inheritable for consistent errno/GetLastError handling.

     let handle = unsafe { libc::get_osfhandle(fd) };
     if handle == INVALID_HANDLE_VALUE as isize {
-        return Err(std::io::Error::last_os_error());
+        return Err(crate::os::last_os_error());
     }
@@
     let result = unsafe { SetHandleInformation(handle as HANDLE, HANDLE_FLAG_INHERIT, flags) };
     if result == 0 {
-        return Err(std::io::Error::last_os_error());
+        return Err(crate::os::last_os_error());
     }

471-491: Windows: fix HANDLE leak on _open_osfhandle failure; open in binary; use unified last_os_error().

  • If _open_osfhandle returns -1, the raw HANDLE from into_raw_handle() leaks; close it.
  • Use O_BINARY to avoid text-mode translation.
  • Prefer crate::os::last_os_error() (bridges errno/GetLastError).

Apply:

@@
-        use std::os::windows::io::IntoRawHandle;
+        use std::os::windows::io::IntoRawHandle;
+        use windows_sys::Win32::Foundation::CloseHandle;
@@
-        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY) };
+        let fd = unsafe { _open_osfhandle(handle as isize, libc::O_RDONLY | libc::O_BINARY) };
         if fd == -1 {
-            return Err(std::io::Error::last_os_error());
+            // _open_osfhandle did not take ownership; close raw HANDLE to avoid leak.
+            unsafe { CloseHandle(handle as _) };
+            return Err(crate::os::last_os_error());
         }
@@
         let fp = unsafe { _fdopen(fd, mode_cstr.as_ptr()) };
         if fp.is_null() {
-            unsafe { libc::close(fd) };
-            return Err(std::io::Error::last_os_error());
+            unsafe { libc::close(fd) };
+            return Err(crate::os::last_os_error());
         }
scripts/make_ssl_data_rs.py (3)

124-129: Don’t fail if crypto/err/openssl.ec is absent (older trees).

-    reasons = []
-    reasons.extend(parse_openssl_error_text(args))
-    reasons.extend(parse_extra_reasons(args))
+    reasons = []
+    reasons.extend(parse_openssl_error_text(args))
+    if os.path.isfile(args.errcodes):
+        reasons.extend(parse_extra_reasons(args))

131-138: Emit correct header path, DO NOT EDIT banner, and clippy allow in generated files.

Keeps headers accurate and reduces lint noise on huge constants.

-    lines = [
-        "// File generated by tools/make_ssl_data_rs.py",
+    lines = [
+        "// Code generated by scripts/make_ssl_data_rs.py; DO NOT EDIT.",
+        "// File generated by scripts/make_ssl_data_rs.py",
         f"// Generated on {datetime.datetime.now(datetime.timezone.utc).isoformat()}",
         f"// Source: OpenSSL from {args.srcdir}",
         "// spell-checker: disable",
+        "#![allow(clippy::unreadable_literal)]",
         "",
         "use phf::phf_map;",
         "",
     ]

20-20: Remove unused import.

-import sys
stdlib/src/ssl.rs (3)

618-619: CRITICAL: Still using hardcoded ex_data index 0 for PySslSocket.

This critical issue from the previous review remains unfixed. Index 0 is not guaranteed to be yours; using it risks collisions with other code or undefined behavior. You must allocate a dedicated index via SSL_get_ex_new_index.

Apply the fix from the previous review:

+    // Get or create an ex_data index for storing PySslSocket*
+    fn get_ssl_socket_ex_data_index() -> libc::c_int {
+        use std::sync::LazyLock;
+        static IDX: LazyLock<libc::c_int> = LazyLock::new(|| unsafe {
+            sys::SSL_get_ex_new_index(0, std::ptr::null_mut(), None, None, None)
+        });
+        *IDX
+    }

Then replace all three occurrences:

-            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, 0);
+            let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, get_ssl_socket_ex_data_index());
-                    sys::SSL_set_ex_data(ssl_ptr, 0, &*py_ref as *const _ as *mut _);
+                    sys::SSL_set_ex_data(ssl_ptr, get_ssl_socket_ex_data_index(), &*py_ref as *const _ as *mut _);

Also applies to: 1562-1563, 1695-1696


1003-1015: CRITICAL: Sentinel handling for protocol versions remains incorrect despite being marked "addressed".

The previous review clearly identified this issue with web search verification, yet the problematic code is unchanged:

In set_minimum_version (lines 1004-1015):

  • -2 (PY_PROTO_MINIMUM_SUPPORTED) still maps to TLS1_2_VERSION instead of 0
  • -1 (PY_PROTO_MAXIMUM_SUPPORTED) still maps to TLS1_3_VERSION instead of 0

In set_maximum_version (lines 1037-1048):

  • -2 still maps to TLS1_2_VERSION instead of 0

Both sentinels must map to 0 (OpenSSL's "unset/default") to maintain symmetry with the getters and allow OpenSSL to use its minimum/maximum supported versions.

Apply the previously suggested fix:

-            let proto_version = match value {
-                -2 => {
-                    // PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2)
-                    sys::TLS1_2_VERSION
-                }
-                -1 => {
-                    // PY_PROTO_MAXIMUM_SUPPORTED -> use maximum available
-                    // For max on min_proto_version, we use the newest available
-                    sys::TLS1_3_VERSION
-                }
-                _ => value,
-            };
+            // -2 (minimum supported) -> 0 to clear limit; -1 (maximum supported) is not meaningful here,
+            // but mirror CPython by treating it as 0 as well.
+            let proto_version = match value {
+                -2 | -1 => 0,
+                _ => value,
+            };
-            let proto_version = match value {
-                -1 => {
-                    // PY_PROTO_MAXIMUM_SUPPORTED -> use 0 for OpenSSL (means no limit)
-                    0
-                }
-                -2 => {
-                    // PY_PROTO_MINIMUM_SUPPORTED -> use minimum available (TLS 1.2)
-                    sys::TLS1_2_VERSION
-                }
-                _ => value,
-            };
+            // -1 (maximum supported) -> 0; -2 (minimum supported) is not meaningful here,
+            // but mirror CPython by treating it as 0 as well.
+            let proto_version = match value {
+                -1 | -2 => 0,
+                _ => value,
+            };

Also applies to: 1036-1048


1058-1072: CRITICAL: cfg gates for num_tickets still use ossl110 instead of ossl111.

This critical issue from the previous review remains unfixed. SSL_CTX_get_num_tickets and SSL_CTX_set_num_tickets were added in OpenSSL 1.1.1, not 1.1.0. Using #[cfg(ossl110)] will cause build failures on OpenSSL 1.1.0.

Change the cfg attributes at all four locations:

-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
             {
                 let ctx = self.ctx();
                 let num = unsafe { sys::SSL_CTX_get_num_tickets(ctx.as_ptr()) };
                 Ok(num)
             }
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]
             {
-            #[cfg(ossl110)]
+            #[cfg(ossl111)]
             {
                 let ctx = self.builder();
                 let result = unsafe { sys::SSL_CTX_set_num_tickets(ctx.as_ptr(), value as usize) };
                 if result != 1 {
                     return Err(vm.new_value_error("failed to set num tickets."));
                 }
                 Ok(())
             }
-            #[cfg(not(ossl110))]
+            #[cfg(not(ossl111))]
             {

Also applies to: 1074-1101

🧹 Nitpick comments (9)
stdlib/src/socket.rs (1)

1254-1266: Align recvfrom_into semantics with recv_into (clamp instead of error when nbytes > len(buf)).

recv_into now clamps nbytes to the buffer length, but recvfrom_into errors if nbytes exceeds buffer size. CPython semantics read “at most nbytes,” so clamping is preferable and consistent.

Apply this refactor:

@@
-            let buf = match nbytes {
-                OptionalArg::Present(i) => {
-                    let i = i.to_usize().ok_or_else(|| {
-                        vm.new_value_error("negative buffersize in recvfrom_into")
-                    })?;
-                    buf.get_mut(..i).ok_or_else(|| {
-                        vm.new_value_error("nbytes is greater than the length of the buffer")
-                    })?
-                }
-                OptionalArg::Missing => buf,
-            };
+            let read_len = match nbytes {
+                OptionalArg::Present(i) => {
+                    let i = i.to_usize().ok_or_else(|| {
+                        vm.new_value_error("negative buffersize in recvfrom_into")
+                    })?;
+                    i.min(buf.len())
+                }
+                OptionalArg::Missing => buf.len(),
+            };
+            let buf = &mut buf[..read_len];
stdlib/src/ssl/ssl_data_31.rs (1)

1768-1770: Make encode_error_key a const fn and avoid sign-ext casting.

Small polish and clippy-friendly style.

Apply in scripts/make_ssl_data_rs.py:

-            "pub fn encode_error_key(lib: i32, reason: i32) -> u64 {",
-            "    ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)",
+            "pub const fn encode_error_key(lib: i32, reason: i32) -> u64 {",
+            "    ((lib as u64) << 32) | u64::from(reason as u32)",
             "}",
stdlib/src/ssl/ssl_data_111.rs (1)

1345-1347: Make encode_error_key const and use u64::from for masking.

Covered by the generator change suggested earlier.

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

1757-1759: Promote encode_error_key to const and use u64::from(reason as u32).

Keeps consistency across versions and improves clarity.

Address centrally in scripts/make_ssl_data_rs.py as shown in earlier diffs.

common/src/fileutils.rs (1)

449-456: Optional: accept “r”/“rt” as aliases for “rb”.

Quality-of-life improvement; keeps strictness while easing caller usage.

-    if mode != "rb" {
+    if mode != "rb" && mode != "r" && mode != "rt" {
         return Err(std::io::Error::new(
             std::io::ErrorKind::InvalidInput,
             format!("unsupported mode: {}", mode),
         ));
     }
scripts/make_ssl_data_rs.py (2)

145-151: Generate const fn and clippy‑friendly mask in helper.

Keeps all versioned modules consistent and cleaner.

-            "pub fn encode_error_key(lib: i32, reason: i32) -> u64 {",
-            "    ((lib as u64) << 32) | (reason as u64 & 0xFFFFFFFF)",
+            "pub const fn encode_error_key(lib: i32, reason: i32) -> u64 {",
+            "    ((lib as u64) << 32) | u64::from(reason as u32)",
             "}",

77-86: Minor: comment says “keep the last one if duplicates” but code picks last by lexicographic libname.

Either clarify the comment or preserve input order so later defines in err.h win. Low impact.

-    # Deduplicate: keep the last one if there are duplicates
-    seen = {}
-    for libname in sorted(args.lib2errnum):
+    # Deduplicate: keep the last one by input order if there are duplicates
+    seen = {}
+    for libname in args.lib2errnum:  # relies on dict insertion order (Py3.7+)
         lib_num = args.lib2errnum[libname]
         seen[lib_num] = libname
stdlib/src/ssl.rs (2)

1829-1939: BIO pathway logic appears improved, but verify BioStream error handling.

The removal of SSL_set_bio (line 1635 comment) and implementation of Read/Write for BioStream addresses the previous critical issue about conflicting BIO setup.

However, the BioStream Read implementation (lines 1836-1856) returns WouldBlock for all negative BIO_read results without checking whether it's actually a retry condition. This might mask real errors.

Consider checking BIO_should_retry to distinguish between transient and permanent errors:

             let nbytes = sys::BIO_read(
                 self.inbio.bio,
                 buf.as_mut_ptr() as *mut _,
                 buf.len().min(i32::MAX as usize) as i32,
             );
             if nbytes < 0 {
-                // BIO_read returns -1 on error or when no data is available
-                // Check if it's a retry condition (WANT_READ)
-                Err(std::io::Error::new(
-                    std::io::ErrorKind::WouldBlock,
-                    "BIO has no data available",
-                ))
+                if sys::BIO_should_retry(self.inbio.bio) != 0 {
+                    Err(std::io::Error::new(
+                        std::io::ErrorKind::WouldBlock,
+                        "BIO has no data available",
+                    ))
+                } else {
+                    Err(std::io::Error::other("BIO_read failed"))
+                }
             } else {
                 Ok(nbytes as usize)
             }

1354-1417: SNI callback setter is safe given ownership structure, but consider adding safety comment.

The code sets self as a raw pointer for SSL_CTX_set_tlsext_servername_arg (line 1392). This is safe because:

  • PySslSocket holds PyRef<PySslContext>, keeping the context alive
  • The pointer is only dereferenced during the SNI callback (handshake)
  • The context cannot be moved while the SSL_CTX exists

Consider adding a safety comment at line 1392:

                     sys::SSL_CTX_set_tlsext_servername_arg(
                         self.ctx().as_ptr(),
+                        // SAFETY: self pointer is valid as long as this PySslContext exists.
+                        // PySslSocket holds a PyRef<PySslContext>, ensuring it outlives any SSL connection.
                         self as *const _ as *mut _,
                     );
📜 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 b5d48e2 and 6492cd8.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • Lib/test/test_httplib.py is excluded by !Lib/**
  • Lib/test/test_ssl.py is excluded by !Lib/**
  • Lib/test/test_urllib2_localnet.py is excluded by !Lib/**
📒 Files selected for processing (8)
  • common/src/fileutils.rs (1 hunks)
  • scripts/make_ssl_data_rs.py (1 hunks)
  • stdlib/Cargo.toml (1 hunks)
  • stdlib/src/socket.rs (1 hunks)
  • stdlib/src/ssl.rs (45 hunks)
  • stdlib/src/ssl/ssl_data_111.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_300.rs (1 hunks)
  • stdlib/src/ssl/ssl_data_31.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • stdlib/Cargo.toml
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rs

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

**/*.rs: Format Rust code with the default rustfmt style (run cargo fmt)
Run clippy and fix any warnings or lints introduced by your changes
Follow Rust best practices for error handling and memory management

Files:

  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/socket.rs
  • common/src/fileutils.rs
  • stdlib/src/ssl.rs
{vm,stdlib}/**/*.rs

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

Use RustPython macros (pyclass, pymodule, pyfunction, etc.) when implementing Python functionality in Rust

Files:

  • stdlib/src/ssl/ssl_data_111.rs
  • stdlib/src/ssl/ssl_data_31.rs
  • stdlib/src/ssl/ssl_data_300.rs
  • stdlib/src/socket.rs
  • stdlib/src/ssl.rs
🧬 Code graph analysis (5)
stdlib/src/ssl/ssl_data_31.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1757-1759)
stdlib/src/ssl/ssl_data_300.rs (2)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1768-1770)
stdlib/src/socket.rs (1)
vm/src/builtins/memory.rs (1)
  • nbytes (575-577)
common/src/fileutils.rs (5)
common/src/crt_fd.rs (2)
  • open (307-309)
  • close (331-333)
vm/src/stdlib/msvcrt.rs (2)
  • _open_osfhandle (91-92)
  • get_osfhandle (106-110)
common/src/os.rs (2)
  • last_os_error (23-34)
  • last_os_error (40-42)
vm/src/stdlib/nt.rs (1)
  • set_inheritable (93-100)
vm/src/stdlib/posix.rs (2)
  • set_inheritable (6-15)
  • set_inheritable (956-958)
stdlib/src/ssl.rs (5)
common/src/fileutils.rs (4)
  • path (77-79)
  • std (318-326)
  • std (336-336)
  • fopen (445-493)
stdlib/src/ssl/cert.rs (1)
  • cert_to_py (216-223)
stdlib/src/ssl/ssl_data_111.rs (1)
  • encode_error_key (1345-1347)
stdlib/src/ssl/ssl_data_300.rs (1)
  • encode_error_key (1757-1759)
stdlib/src/ssl/ssl_data_31.rs (1)
  • encode_error_key (1768-1770)
🪛 Flake8 (7.3.0)
scripts/make_ssl_data_rs.py

[error] 20-20: 'sys' imported but unused

(F401)

⏰ 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 rust tests (macos-latest)
  • GitHub Check: Run snippets and cpython tests (macos-latest)
  • GitHub Check: Run rust tests (ubuntu-latest)
  • GitHub Check: Ensure compilation on various targets
  • GitHub Check: Run snippets and cpython tests on wasm-wasi
  • GitHub Check: Check the WASM package and demo
  • GitHub Check: Check Rust code with rustfmt and clippy
  • GitHub Check: Run snippets and cpython tests (ubuntu-latest)
  • GitHub Check: Run snippets and cpython tests (windows-latest)
🔇 Additional comments (14)
stdlib/src/socket.rs (2)

1210-1221: nbytes validation and clamping look correct.

Negative nbytes raises ValueError; read_len is clamped to buffer length; slicing before recv avoids overrun. LGTM.


1198-1204: Review comment is incorrect; no backward-compatibility break exists.

The change adds a new nbytes parameter between buf and flags—it does not reorder existing arguments. The old signature was recv_into(self, buf, flags, vm); the new one is recv_into(self, buf, nbytes, flags, vm).

Evidence from codebase:

  • All Python call sites use either 1-arg form recv_into(buf) (unaffected) or 3-arg form recv_into(buffer, nbytes, flags) (matches the new signature exactly)
  • Zero call sites use the alleged old recv_into(buf, flags) form that would break
  • Lib/ssl.py:1306 already calls super().recv_into(buffer, nbytes, flags), proving the new signature is the expected one

The proposed Either<isize, i32> compatibility shim is unnecessary; real usage patterns are already compatible with the new signature.

Likely an incorrect or invalid review comment.

stdlib/src/ssl.rs (12)

5-20: LGTM! Clean version-specific module selection.

The conditional compilation for OpenSSL version-specific error codes is well-structured with appropriate fallback.


1490-1492: Good addition: null byte validation for server_hostname.

Prevents null byte injection when passing hostname to OpenSSL C APIs.


2501-2508: Good optimization: early return for zero-byte reads.

Avoids unnecessary SSL operations when n=0.


3030-3050: Improved: System error detection logic simplified.

The system error detection now correctly checks only lib == sys::ERR_LIB_SYS without the confusing intermediate checks from the previous version. The errno mapping for FileNotFoundError is appropriate.


3058-3163: Well-structured error conversion using static error tables.

The integration with version-specific ssl_data modules provides stable error names across OpenSSL versions. The fallback to OpenSSL's error strings and special handling for certificate verification errors is appropriate.


1226-1276: LGTM: cert_store_stats correctly counts certificates and CRLs.

The implementation properly iterates the certificate store and uses X509_check_ca to identify CA certificates. Memory handling is correct (borrowed pointers, no manual freeing needed).


1278-1305: LGTM: session_stats cleanly exposes SSL session statistics.

The macro-based approach reduces boilerplate. The underlying SSL_CTX_ctrl wrappers (defined at lines 2727-2828) are correctly implemented.


1307-1352: Good DH parameters loading with cross-platform file handling.

The use of rustpython_common::fileutils::fopen ensures cross-platform compatibility. The file is correctly closed (line 1333) and the DH object is freed (line 1344) after use.

Minor note: fclose is called even when dh.is_null(), but this is fine since the file should be closed in both success and failure cases.


2694-2838: LGTM: FFI declarations and wrapper functions correctly expose OpenSSL APIs.

The session statistics wrappers correctly use SSL_CTX_ctrl with appropriate constants. The conditional compilation for OpenSSL 1.1.0+ functions is appropriate.


3271-3287: LGTM: cipher_description safely formats cipher information.

The 256-byte buffer is sufficient (OpenSSL documents ~128 bytes needed), and null terminator handling is correct.


850-854: Good addition: cipher descriptions provide useful metadata.


782-782: Appropriate use of IMMUTABLETYPE flag.

The flag prevents subclassing of _SSLContext, which is correct for this C extension type despite internal mutability via locks.

Comment on lines +127 to +129
# sort by libname, numeric error code
args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))

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

Sort key doesn’t match the comment; use (libname, num) for deterministic output.

Current key (0,3) sorts by reason, not libname.

-    # sort by libname, numeric error code
-    args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
+    # sort by libname, numeric error code
+    args.reasons = sorted(reasons, key=operator.itemgetter(1, 3))
📝 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
# sort by libname, numeric error code
args.reasons = sorted(reasons, key=operator.itemgetter(0, 3))
# sort by libname, numeric error code
args.reasons = sorted(reasons, key=operator.itemgetter(1, 3))
🤖 Prompt for AI Agents
In scripts/make_ssl_data_rs.py around lines 127 to 129, the sort key currently
uses operator.itemgetter(0, 3) which orders by reason instead of libname; change
the key to use (libname, num) by replacing the itemgetter indices accordingly
(e.g., operator.itemgetter(2, 3)) so the list is sorted deterministically by
library name then numeric error code.

Comment on lines +527 to +650
// Callback data stored in SSL context for SNI
struct SniCallbackData {
ssl_context: PyRef<PySslContext>,
vm_ptr: *const VirtualMachine,
}

impl Drop for SniCallbackData {
fn drop(&mut self) {
// PyRef will handle reference counting
}
}

// Get or create an ex_data index for SNI callback data
fn get_sni_ex_data_index() -> libc::c_int {
use std::sync::LazyLock;
static SNI_EX_DATA_IDX: LazyLock<libc::c_int> = LazyLock::new(|| unsafe {
sys::SSL_get_ex_new_index(
0,
std::ptr::null_mut(),
None,
None,
Some(sni_callback_data_free),
)
});
*SNI_EX_DATA_IDX
}

// Free function for callback data
unsafe extern "C" fn sni_callback_data_free(
_parent: *mut libc::c_void,
ptr: *mut libc::c_void,
_ad: *mut sys::CRYPTO_EX_DATA,
_idx: libc::c_int,
_argl: libc::c_long,
_argp: *mut libc::c_void,
) {
if !ptr.is_null() {
unsafe {
let _ = Box::from_raw(ptr as *mut SniCallbackData);
}
}
}

// SNI callback function called by OpenSSL
unsafe extern "C" fn _servername_callback(
ssl_ptr: *mut sys::SSL,
_al: *mut libc::c_int,
arg: *mut libc::c_void,
) -> libc::c_int {
const SSL_TLSEXT_ERR_OK: libc::c_int = 0;
const SSL_TLSEXT_ERR_ALERT_FATAL: libc::c_int = 2;
const TLSEXT_NAMETYPE_host_name: libc::c_int = 0;

if arg.is_null() {
return SSL_TLSEXT_ERR_OK;
}

unsafe {
let ctx = &*(arg as *const PySslContext);

// Get the callback
let callback_opt = ctx.sni_callback.lock().clone();
let Some(callback) = callback_opt else {
return SSL_TLSEXT_ERR_OK;
};

// Get callback data from SSL ex_data
let idx = get_sni_ex_data_index();
let data_ptr = sys::SSL_get_ex_data(ssl_ptr, idx);
if data_ptr.is_null() {
return SSL_TLSEXT_ERR_ALERT_FATAL;
}

let callback_data = &*(data_ptr as *const SniCallbackData);

// SAFETY: vm_ptr is stored during wrap_socket and is valid for the lifetime
// of the SSL connection. The handshake happens synchronously in the same thread.
let vm = &*callback_data.vm_ptr;

// Get server name
let servername = sys::SSL_get_servername(ssl_ptr, TLSEXT_NAMETYPE_host_name);
let server_name_arg = if servername.is_null() {
vm.ctx.none()
} else {
let name_cstr = std::ffi::CStr::from_ptr(servername);
match name_cstr.to_str() {
Ok(name_str) => vm.ctx.new_str(name_str).into(),
Err(_) => vm.ctx.none(),
}
};

// Get SSL socket from SSL ex_data (stored as PySslSocket pointer)
let ssl_socket_ptr = sys::SSL_get_ex_data(ssl_ptr, 0); // Index 0 for SSL socket
let ssl_socket_obj = if !ssl_socket_ptr.is_null() {
let ssl_socket = &*(ssl_socket_ptr as *const PySslSocket);
// Try to get owner first
ssl_socket
.owner
.read()
.as_ref()
.and_then(|weak| weak.upgrade())
.unwrap_or_else(|| vm.ctx.none())
} else {
vm.ctx.none()
};

// Call the Python callback
match callback.call(
(
ssl_socket_obj,
server_name_arg,
callback_data.ssl_context.to_owned(),
),
vm,
) {
Ok(_) => SSL_TLSEXT_ERR_OK,
Err(exc) => {
// Log the exception but don't propagate it
vm.run_unraisable(exc, None, vm.ctx.none());
SSL_TLSEXT_ERR_ALERT_FATAL
}
}
}
}
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

Potential safety issue: VM pointer lifetime not validated.

The SNI callback stores a raw *const VirtualMachine pointer (line 530) and dereferences it in _servername_callback (line 604). While the comment claims this is safe because "the handshake happens synchronously in the same thread," there's no mechanism ensuring the VM outlives the SSL connection.

If the VM is dropped while an SSL connection with pending SNI callback exists, dereferencing vm_ptr in line 604 causes undefined behavior. Consider either:

  1. Store a reference-counted handle to ensure VM lifetime
  2. Document that SSL sockets must be dropped before the VM
  3. Add runtime validation that VM pointer is still valid

The synchronous handshake assumption may not hold if the SSL object is long-lived or if BIO-based connections defer handshakes.

🤖 Prompt for AI Agents
stdlib/src/ssl.rs around lines 527-650: the SniCallbackData stores a raw *const
VirtualMachine and dereferences it in the SNI callback, which can UB if the VM
is dropped; replace the raw pointer with an owning, thread-safe handle (e.g.
Arc<VirtualMachine> or a PyRef-like owned reference) so the VM is kept alive for
the lifetime of the SNI callback data, update SniCallbackData.ssl_context and
vm_ptr types accordingly, change wherever the SniCallbackData is created to
clone/clone_arc the VM handle into the Box stored in ex_data, and update
sni_callback_data_free and the callback to drop the Box< SniCallbackData>
(letting Arc decrement) and to access the VM via the safe handle instead of
dereferencing a raw pointer; alternatively, if ownership semantics cannot be
changed, add a runtime validity check and fail gracefully, but the preferred fix
is to use a reference-counted handle to guarantee VM lifetime.

@youknowone youknowone merged commit 614028f into RustPython:main Oct 29, 2025
12 checks passed
@youknowone youknowone deleted the more-ssl branch October 29, 2025 14:01
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