Skip to content

Conversation

@youknowone
Copy link
Member

@youknowone youknowone commented Dec 17, 2025

Summary by CodeRabbit

  • New Features

    • Added pending() method to SSL sockets to check available encrypted data.
  • Improvements

    • Enhanced SSL error handling with clearer, more consistent error messages across different failure scenarios.
    • Introduced more granular SSL exception types for improved error diagnostics and application-level handling.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 17, 2025

Walkthrough

This PR centralizes SSL error handling by introducing a shared ssl_error module with Python-exposed exception types, refactors the OpenSSL implementation to route errors through a unified new_ssl_error helper, introduces thread-local VM state management and RAII guards for handshake callbacks, implements ex_data-based SNI callback data management, adds a pending() method to SSLSocket, and updates constructor signatures to use Py<PyType>.

Changes

Cohort / File(s) Change Summary
Error infrastructure refactoring
crates/stdlib/src/ssl/error.rs, crates/stdlib/src/ssl.rs, crates/stdlib/src/ssl/compat.rs
Introduces new shared ssl_error module with centralized Python-exposed SSL exception hierarchy (SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, SSLSyscallError, SSLEOFError, SSLCertVerificationError) derived from PyOSError. Adds helper constructors (create_ssl_want_read_error, create_ssl_want_write_error, create_ssl_eof_error, create_ssl_zero_return_error) and custom __str__ for SSLError. Updates ssl.rs module annotation to wire external error provider. Refactors ssl/compat.rs to import errors from new location.
OpenSSL implementation rework
crates/stdlib/src/openssl.rs
Introduces new_ssl_error unified error construction helper for OS-subtype errors. Adds thread-local state with HandshakeVmGuard RAII guard to associate Python VM with OpenSSL handshake callbacks. Implements SniCallbackData struct and cleanup_sni_ex_data routine for SNI callback ex_data management. Updates SNI callback path to derive VM from thread-local storage and obtain SSL pointer safely via get_ssl_ptr_for_context_change. Reroutes error creation paths to use unified helper. Adds pending() method to SSLSocket and PySSLSocket. Updates constructor signatures from PyTypeRef to &Py<PyType> receiver.
IPv6 address handling
crates/stdlib/src/openssl/cert.rs
Changes IPv6 SAN construction from direct slice-based Ipv6Addr::from(ip[0..16]) to explicit array conversion <[u8; 16]>::try_from(&ip[0..16]).unwrap().

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • High-priority areas: Thread-local state management and HandshakeVmGuard logic in openssl.rs requires careful review for deadlock and memory safety concerns; SNI callback ex_data lifecycle and cleanup logic must be verified for resource leaks and double-frees.
  • Secondary concerns: The unified new_ssl_error error construction pathway across many call sites; constructor signature changes and their impact on callers; potential for unwrap panic in IPv6 address conversion.
  • Refactoring complexity: Error handling path consolidation across multiple modules requires tracing the old vs. new patterns to ensure behavioral equivalence.

Possibly related PRs

  • Fix SSL deferred error #6371: Modifies SSL handshake and deferred certificate-verification error-propagation paths, overlapping with this PR's callback and error handling rework.
  • New subclass payload layout #6319: Refactors PyOSError/new_os_subtype_error infrastructure that this PR's OS-subtype SSL error construction relies on.
  • minimize ssl lock #6376: Restructures SNI access and error propagation in the SSL subsystem to minimize locking, complementary to this PR's ex_data and callback management changes.

Suggested reviewers

  • coolreader18
  • ShaharNaveh
  • arihant2math

Poem

🐰 Thread-safe handshakes, no deadlocks to fear,
SSL errors now centralized and clear,
Ex_data dances with SNI's delight,
Pending() hops in, everything's tight!
Error pathways gleam in unified light,
Our warren's SSL now gets it just right!

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 summarizes the main changes: fixing OpenSSL build issues and establishing a shared SSL error module across multiple files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ 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 December 18, 2025 00:08
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/stdlib/src/ssl.rs (1)

1129-1175: create_ssl_error_with_reason calls leave .reason empty for PEM errors

In the load_cert_chain error mapping, both the generic PEM error branch and the fallback branch use:

super::compat::SslError::create_ssl_error_with_reason(
    vm,
    Some("SSL"),
    "",
    "PEM lib",
)

create_ssl_error_with_reason is documented to take a human‑readable reason string and a separate message, and it stores the reason into the reason attribute on the SSLError instance. Passing "" as the reason means SSLError.reason will be an empty string, which contradicts the helper’s intent and comments like // [SSL] PEM lib. The same pattern appears in load_certs_from_file_helper with Some("X509"), "", "PEM lib".

This will make library/reason metadata inconsistent and can break tests or user code that inspects exc.reason for PEM/X509 failures.

Consider instead populating the reason field with "PEM lib" while keeping the existing message text:

-                        super::compat::SslError::create_ssl_error_with_reason(
-                            vm,
-                            Some("SSL"),
-                            "",
-                            "PEM lib",
-                        )
+                        super::compat::SslError::create_ssl_error_with_reason(
+                            vm,
+                            Some("SSL"),
+                            "PEM lib",
+                            "PEM lib",
+                        )

and similarly in load_certs_from_file_helper:

-                    _ => super::compat::SslError::create_ssl_error_with_reason(
-                        vm,
-                        Some("X509"),
-                        "",
-                        "PEM lib",
-                    ),
+                    _ => super::compat::SslError::create_ssl_error_with_reason(
+                        vm,
+                        Some("X509"),
+                        "PEM lib",
+                        "PEM lib",
+                    ),

This keeps the user‑visible message unchanged while ensuring SSLError.reason carries a meaningful value instead of the empty string.

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

3404-3408: Remove debug print statement.

This eprintln! appears to be leftover debug output that should be removed before release. Debug prints to stderr can pollute application output and confuse users.

             // Successfully loaded at least one certificate from DER data.
             // Clear any trailing errors from EOF.
             // CPython clears errors when:
             // - DER: was_bio_eof is set (EOF reached)
             // - PEM: PEM_R_NO_START_LINE error (normal EOF)
             // Both cases mean successful completion with loaded certs.
-            eprintln!(
-                "[x509_stack_from_der] SUCCESS: Clearing errors and returning {} certs",
-                certs.len()
-            );
             sys::ERR_clear_error();
             Ok(certs)
🧹 Nitpick comments (2)
crates/stdlib/src/openssl/cert.rs (1)

161-178: IPv6 slice-to-array conversion is correct; can be slightly simplified

ip.len() == 16 guard makes the unwrap() on try_from(&ip[0..16]) safe, so behavior is correct. If you want to tighten it up, you can lean on TryInto for slightly clearer code:

-                        } else if ip.len() == 16 {
-                            // IPv6 - format with all zeros visible (not compressed)
-                            let ip_addr =
-                                std::net::Ipv6Addr::from(<[u8; 16]>::try_from(&ip[0..16]).unwrap());
+                        } else if ip.len() == 16 {
+                            // IPv6 - format with all zeros visible (not compressed)
+                            let bytes: [u8; 16] = ip.as_ref().try_into().unwrap();
+                            let ip_addr = std::net::Ipv6Addr::from(bytes);

Purely a readability/idiomatic tweak; no functional issue.

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

534-552: Consider removing unused _ssl_ptr field from HandshakeVmGuard.

The _ssl_ptr field is stored during construction but never accessed afterward. The Drop implementation only clears the thread-local storage without referencing this field. This appears to be dead code, possibly left over from an earlier design.

     // RAII guard to set/clear thread-local handshake context
     struct HandshakeVmGuard {
-        _ssl_ptr: *mut sys::SSL,
     }
 
     impl HandshakeVmGuard {
         fn new(vm: &VirtualMachine, ssl_ptr: *mut sys::SSL) -> Self {
             HANDSHAKE_VM.with(|cell| cell.set(Some(vm as *const _)));
             HANDSHAKE_SSL_PTR.with(|cell| cell.set(Some(ssl_ptr)));
-            HandshakeVmGuard { _ssl_ptr: ssl_ptr }
+            HandshakeVmGuard {}
         }
     }
📜 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 a4c93df and 96f3a2d.

📒 Files selected for processing (5)
  • crates/stdlib/src/openssl.rs (38 hunks)
  • crates/stdlib/src/openssl/cert.rs (1 hunks)
  • crates/stdlib/src/ssl.rs (2 hunks)
  • crates/stdlib/src/ssl/compat.rs (1 hunks)
  • crates/stdlib/src/ssl/error.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/compat.rs
  • crates/stdlib/src/openssl/cert.rs
  • crates/stdlib/src/ssl/error.rs
  • crates/stdlib/src/openssl.rs
  • crates/stdlib/src/ssl.rs
🧠 Learnings (2)
📚 Learning: 2025-12-09T08:46:58.660Z
Learnt from: youknowone
Repo: RustPython/RustPython PR: 6358
File: crates/vm/src/exception_group.rs:173-185
Timestamp: 2025-12-09T08:46:58.660Z
Learning: In crates/vm/src/exception_group.rs, the derive() method intentionally always creates a BaseExceptionGroup instance rather than preserving the original exception class type. This is a deliberate design decision that differs from CPython's behavior.

Applied to files:

  • crates/stdlib/src/openssl.rs
📚 Learning: 2025-11-29T12:17:28.606Z
Learnt from: CR
Repo: RustPython/RustPython PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T12:17:28.606Z
Learning: Applies to **/*.rs : Use the macro system (`pyclass`, `pymodule`, `pyfunction`, etc.) when implementing Python functionality in Rust

Applied to files:

  • crates/stdlib/src/ssl.rs
🧬 Code graph analysis (4)
crates/stdlib/src/ssl/compat.rs (1)
crates/stdlib/src/ssl/error.rs (4)
  • create_ssl_eof_error (102-108)
  • create_ssl_want_read_error (86-92)
  • create_ssl_want_write_error (94-100)
  • create_ssl_zero_return_error (110-116)
crates/stdlib/src/ssl/error.rs (1)
crates/vm/src/stdlib/os.rs (1)
  • strerror (1544-1548)
crates/stdlib/src/openssl.rs (1)
crates/stdlib/src/ssl.rs (2)
  • _msg_callback (1691-1693)
  • e (1131-1131)
crates/stdlib/src/ssl.rs (2)
crates/stdlib/src/csv.rs (1)
  • error (38-44)
crates/stdlib/src/ssl/error.rs (1)
  • create_ssl_want_read_error (86-92)
🔇 Additional comments (10)
crates/stdlib/src/ssl/compat.rs (1)

33-40: Shared SSL error imports are wired correctly

Importing PySSLSocket from super::_ssl and the various SSL error types/helpers from super::error matches the new centralized error module and keeps compat backend-agnostic. Looks good.

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

3-117: Centralized SSL error hierarchy and helpers look consistent

The new ssl_error module cleanly centralizes SSL exception types and the WANT/EOF/zero-return constructors. The custom PySSLError.__str__ behavior (preferring strerror, otherwise falling back to args) aligns with OSError semantics, and the helpers correctly subtype PyOSError with appropriate errno values. This should make error handling consistent across rustls and OpenSSL backends.

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

3340-3363: pending() correctly reports buffered plaintext using rustls Reader

PySSLSocket::pending locks the connection, creates a rustls Reader, and uses BufRead::fill_buf() to determine how many decrypted bytes are currently buffered without consuming them. Since rustls Reader<'_> implements BufRead, this is a non‑destructive way to emulate OpenSSL’s SSL_pending(). Returning 0 on errors (WouldBlock or others) also matches OpenSSL’s behavior of reporting “no pending data” instead of surfacing I/O errors via pending(). This is a good fit for asyncore‑style readable() checks.

crates/stdlib/src/openssl.rs (7)

593-606: LGTM - Manual ex_data cleanup is correctly implemented.

The cleanup_sni_ex_data function properly handles the lifecycle of SniCallbackData:

  • Checks for null before freeing (safe for multiple calls)
  • Clears the ex_data pointer to prevent double-free
  • Called explicitly after handshake to release references promptly

712-727: LGTM - Intentional no-op callback is well-documented.

The comment clearly explains the deadlock concerns (referencing CPython bpo-43577) and why this callback is intentionally empty. This is a reasonable design choice that prioritizes stability over the feature.


2188-2192: LGTM - Simple and correct pending() implementation.

The method correctly exposes OpenSSL's SSL_pending function, which returns the number of bytes available for immediate read. Implementation is straightforward and matches CPython's behavior.


3176-3180: LGTM - Unified error creation helper.

The new_ssl_error helper provides a consistent way to create SSL errors as proper OSError subtypes, which aligns with CPython's behavior. This centralizes the error creation pattern used throughout the module.


2406-2483: LGTM - Comprehensive handshake cleanup across all exit paths.

The do_handshake implementation correctly calls cleanup_sni_ex_data on all exit paths (success, timeout, closed, error). The HandshakeVmGuard RAII pattern ensures the thread-local state is properly managed. The safety comments appropriately document the lifetime requirements.


1704-1720: LGTM - Weak reference prevents reference cycle.

Using PyWeak for ssl_socket_weak correctly breaks the reference cycle that would otherwise occur: PySslSocketSslStreamSSL → ex_data → SniCallbackData → back to PySslSocket. The implementation properly minimizes lock contention by checking has_sni_callback before doing the more expensive weak reference creation.


554-562: LGTM - Correctly avoids deadlock during handshake callbacks.

The function properly handles the scenario where set_context might be called from within an SNI callback during handshake (when the connection lock is already held). By checking the thread-local HANDSHAKE_SSL_PTR first, it avoids attempting to re-acquire the lock.

@youknowone youknowone merged commit 9e43966 into RustPython:main Dec 18, 2025
13 checks passed
@youknowone youknowone deleted the ssl branch December 18, 2025 01:44
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