-
Notifications
You must be signed in to change notification settings - Fork 1.4k
more ssl impl #6228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
more ssl impl #6228
Conversation
WalkthroughAdds a cross-platform Changes
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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
sslfeature 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.
_wfopenon 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: PreferOsStrExt::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-idiomaticOsStrExt::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
sslkeeps 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
⛔ Files ignored due to path filters (4)
Cargo.lockis excluded by!**/*.lockLib/test/test_httplib.pyis excluded by!Lib/**Lib/test/test_ssl.pyis excluded by!Lib/**Lib/test/test_urllib2_localnet.pyis 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 (runcargo 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.rscommon/src/fileutils.rsstdlib/src/socket.rsstdlib/src/ssl/ssl_data_31.rsstdlib/src/ssl/ssl_data_300.rsstdlib/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.rsstdlib/src/socket.rsstdlib/src/ssl/ssl_data_31.rsstdlib/src/ssl/ssl_data_300.rsstdlib/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
ea0d242 to
56b9ede
Compare
There was a problem hiding this 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
PySslSocketpointer, similar to howget_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 functionsSSL_CTX_get_num_ticketsandSSL_CTX_set_num_ticketswere 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()withcrate::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.rsAlso 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 forLIBRARY_CODESandERROR_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///aboveLIBRARY_CODESandERROR_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
📒 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 (runcargo 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.rsstdlib/src/ssl/ssl_data_300.rsstdlib/src/ssl/ssl_data_111.rsstdlib/src/ssl.rsstdlib/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.rsstdlib/src/ssl/ssl_data_111.rsstdlib/src/ssl.rsstdlib/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_contextmethod callsSSL_set_SSL_CTXwhich 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:
- Handshake state: Can this be called mid-handshake or after handshake? CPython may restrict when this can be called.
- NULL return: Line 1989 checks for NULL but only returns a generic error. Consider capturing and converting the OpenSSL error stack.
- 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.cto see if there are additional checks on whencontextcan 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_DHparamsreturns NULL (line 1336), the code callsconvert_openssl_errorwhich returns early at line 1337, butfclose(fp)was already called at line 1333. However, ifPEM_read_DHparamsfails, the early return at line 1337 means we correctly don't proceed to use the nulldhpointer.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
SniCallbackDatastruct stores a raw pointer toVirtualMachineat 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:
- 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.
- Thread safety: The comment assumes synchronous handshake, but what if the socket is passed to another thread?
- Drop order: When
PySslSocketis 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_hostnameat 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 == 0at 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
PySslContextwith bothBASETYPEandIMMUTABLETYPEflags, while other SSL types (PySslSocket,PySslMemoryBio,PySslSession) use onlyIMMUTABLETYPE. TheBASETYPEflag allows Python subclassing.Questions:
- Does CPython's
_ssl.SSLContextallow subclassing? If not, removeBASETYPE.- Is it safe to subclass
SSLContextgiven it holdsSslContextBuilderand uses unsafe FFI?- Do any internal implementations rely on the type being final/non-subclassable?
Check CPython's implementation to confirm whether
SSLContextshould 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
| // 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()); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix Windows HANDLE leak on _open_osfhandle failure; pass O_BINARY for "rb".
- If
_open_osfhandlereturns-1, the rawHANDLEfrominto_raw_handle()is leaked. Close it withCloseHandle. - For binary read mode, pass
_O_BINARYto 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).
stdlib/src/ssl.rs
Outdated
| // 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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()throughoutfopen()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_osfhandlereturns-1, the rawHANDLEfrominto_raw_handle()is leaked because ownership was transferred butCloseHandleis not called. Additionally, for binary read mode,O_BINARYshould 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,
_fdopensetserrnobut notGetLastError, makingstd::io::Error::last_os_error()unreliable. Usecrate::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) andSetHandleInformation(Windows API) error handling should usecrate::os::last_os_error()instead ofstd::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 sysstdlib/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 -C2Also 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 -C2Also 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_keyis 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: Deduplicateencode_error_keytocert.rsor a shared utilities module.The function is identically defined in
stdlib/src/ssl/ssl_data_31.rs:1768,ssl_data_111.rs:1345, andssl_data_300.rs:1757. Move to a shared location and re-export from versioned files. Note:stdlib/src/ssl/mod.rsdoes not exist; considercert.rsas 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
⛔ Files ignored due to path filters (4)
Cargo.lockis excluded by!**/*.lockLib/test/test_httplib.pyis excluded by!Lib/**Lib/test/test_ssl.pyis excluded by!Lib/**Lib/test/test_urllib2_localnet.pyis 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 (runcargo 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.rsstdlib/src/socket.rsstdlib/src/ssl/ssl_data_300.rscommon/src/fileutils.rsstdlib/src/ssl/ssl_data_111.rsstdlib/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.rsstdlib/src/socket.rsstdlib/src/ssl/ssl_data_300.rsstdlib/src/ssl/ssl_data_111.rsstdlib/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," thecargocommands cannot complete reliably.Before applying the suggested refactor, manually verify that:
phfis currently included in the default dependency tree (without explicitsslfeature)- After applying the changes (making
phfoptional and addingdep:phfto thesslfeature),phfis no longer pulled whensslis not enabledRun:
cargo tree -e no-devandcargo tree -e no-dev -F ssllocally 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, flagsin 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.
| 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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| // 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.rsLength 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.pyLength 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.
There was a problem hiding this 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 sysstdlib/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 toTLS1_2_VERSIONinstead of0-1(PY_PROTO_MAXIMUM_SUPPORTED) still maps toTLS1_3_VERSIONinstead of0In
set_maximum_version(lines 1037-1048):
-2still maps toTLS1_2_VERSIONinstead of0Both 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_ticketsandSSL_CTX_set_num_ticketswere 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] = libnamestdlib/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 ofRead/WriteforBioStreamaddresses the previous critical issue about conflicting BIO setup.However, the BioStream Read implementation (lines 1836-1856) returns
WouldBlockfor all negativeBIO_readresults without checking whether it's actually a retry condition. This might mask real errors.Consider checking
BIO_should_retryto 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
selfas a raw pointer forSSL_CTX_set_tlsext_servername_arg(line 1392). This is safe because:
PySslSocketholdsPyRef<PySslContext>, keeping the context alive- The pointer is only dereferenced during the SNI callback (handshake)
- The context cannot be moved while the
SSL_CTXexistsConsider 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
⛔ Files ignored due to path filters (4)
Cargo.lockis excluded by!**/*.lockLib/test/test_httplib.pyis excluded by!Lib/**Lib/test/test_ssl.pyis excluded by!Lib/**Lib/test/test_urllib2_localnet.pyis 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 (runcargo 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.rsstdlib/src/ssl/ssl_data_31.rsstdlib/src/ssl/ssl_data_300.rsstdlib/src/socket.rscommon/src/fileutils.rsstdlib/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.rsstdlib/src/ssl/ssl_data_31.rsstdlib/src/ssl/ssl_data_300.rsstdlib/src/socket.rsstdlib/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
nbytesparameter betweenbufandflags—it does not reorder existing arguments. The old signature wasrecv_into(self, buf, flags, vm); the new one isrecv_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 formrecv_into(buffer, nbytes, flags)(matches the new signature exactly)- Zero call sites use the alleged old
recv_into(buf, flags)form that would breakLib/ssl.py:1306already callssuper().recv_into(buffer, nbytes, flags), proving the new signature is the expected oneThe 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_SYSwithout the confusing intermediate checks from the previous version. The errno mapping forFileNotFoundErroris appropriate.
3058-3163: Well-structured error conversion using static error tables.The integration with version-specific
ssl_datamodules 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_cato 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_ctrlwrappers (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::fopenensures cross-platform compatibility. The file is correctly closed (line 1333) and the DH object is freed (line 1344) after use.Minor note:
fcloseis called even whendh.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_ctrlwith 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.
| # sort by libname, numeric error code | ||
| args.reasons = sorted(reasons, key=operator.itemgetter(0, 3)) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| # 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.
| // 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 | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Store a reference-counted handle to ensure VM lifetime
- Document that SSL sockets must be dropped before the VM
- 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.
Summary by CodeRabbit
New Features
Chores