From 2e24fc32212ed884accbb2b243dc9a666fbefa83 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 08:04:36 +0900 Subject: [PATCH 1/3] Implement rustls-backed _ssl module --- Cargo.lock | 702 ++++- Cargo.toml | 10 + pyre/pyre-interpreter/src/importing.rs | 2 + pyre/pyre-interpreter/src/lib.rs | 13 + .../src/module/_socket/interp_socket.rs | 235 +- pyre/pyre-interpreter/src/module/_ssl/mod.rs | 2388 +++++++++++++++ pyre/pyre-interpreter/src/module/mod.rs | 3 + .../src/objspace/std/mapdict.rs | 22 + pyre/pyre-jit/src/eval.rs | 159 + pyre/pyre-native/Cargo.toml | 9 + pyre/pyre-native/src/lib.rs | 2 + pyre/pyre-native/src/ssl.rs | 2657 +++++++++++++++++ pyre/pyre-object/src/pyobject.rs | 13 + 13 files changed, 6153 insertions(+), 62 deletions(-) create mode 100644 pyre/pyre-interpreter/src/module/_ssl/mod.rs create mode 100644 pyre/pyre-native/src/ssl.rs diff --git a/Cargo.lock b/Cargo.lock index e8ec35e56af..98c6fdfc947 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[package]] +name = "aes" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8eb277bec05f56a0e0591f155a484cbd0f4f07ff2905051a48c72f004f7ed58" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +dependencies = [ + "aead", + "aes", + "cipher 0.5.2", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -71,6 +106,45 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -118,12 +192,41 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bincode" version = "1.3.3" @@ -206,6 +309,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "braininterp" version = "0.0.2" @@ -265,6 +386,15 @@ dependencies = [ "rustversion", ] +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "cc" version = "1.2.66" @@ -304,8 +434,19 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", ] [[package]] @@ -317,6 +458,21 @@ dependencies = [ "error-code", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cobs" version = "0.3.0" @@ -357,6 +513,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -373,6 +535,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -388,6 +560,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -397,6 +575,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "cranelift-assembler-x64" version = "0.132.3" @@ -615,6 +802,39 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + [[package]] name = "debugid" version = "0.8.0" @@ -624,6 +844,37 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "derive-where" version = "1.6.1" @@ -641,11 +892,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "directories-next" version = "2.0.0" @@ -720,6 +982,12 @@ dependencies = [ "majit-metainterp", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "dynasm" version = "5.0.0" @@ -871,6 +1139,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -1026,6 +1300,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + [[package]] name = "gimli" version = "0.33.0" @@ -1092,7 +1375,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -1104,6 +1396,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + [[package]] name = "i64env" version = "0.0.2" @@ -1272,6 +1573,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + [[package]] name = "insta" version = "1.48.0" @@ -1383,7 +1694,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1820,7 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -1874,6 +2185,12 @@ dependencies = [ "libmimalloc-sys", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1931,6 +2248,26 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -1940,6 +2277,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-integer" version = "0.1.46" @@ -2010,12 +2353,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2072,8 +2430,27 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", ] [[package]] @@ -2130,12 +2507,52 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs5" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "der", + "pbkdf2 0.13.0", + "rand_core 0.10.1", + "scrypt 0.12.0", + "sha2 0.11.0", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.10.1", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "polyval" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0fa31d631f2b2cb2a544d0aa321ce847a94764d701ca2becc411138b93d49cd" +dependencies = [ + "cpubits", + "cpufeatures 0.3.0", + "universal-hash", +] + [[package]] name = "postcard" version = "1.1.3" @@ -2159,6 +2576,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2376,12 +2799,19 @@ version = "0.0.2" dependencies = [ "blake2b_simd", "blake2s_simd", + "der", "flate2", "md-5", - "scrypt", + "pem-rfc7468", + "pkcs8", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "scrypt 0.11.0", "sha1", - "sha2", + "sha2 0.10.9", "sha3", + "x509-parser", ] [[package]] @@ -2427,7 +2857,7 @@ dependencies = [ name = "pyre-wasm-runner" version = "0.0.2" dependencies = [ - "sha2", + "sha2 0.10.9", "wasmi", "wasmprinter", "wasmtime", @@ -2535,7 +2965,7 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2545,7 +2975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -2557,6 +2987,12 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rapidhash" version = "4.5.1" @@ -2669,6 +3105,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -2681,6 +3131,15 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -2694,6 +3153,62 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustpython-codegen" version = "0.5.0" @@ -2990,7 +3505,17 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "salsa20" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" +dependencies = [ + "cfg-if", + "cipher 0.5.2", ] [[package]] @@ -3023,9 +3548,44 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "pbkdf2", - "salsa20", - "sha2", + "pbkdf2 0.12.2", + "salsa20 0.10.2", + "sha2 0.10.9", +] + +[[package]] +name = "scrypt" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" +dependencies = [ + "cfg-if", + "pbkdf2 0.13.0", + "salsa20 0.11.0", + "sha2 0.11.0", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3097,8 +3657,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -3108,8 +3668,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3118,7 +3689,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ - "digest", + "digest 0.10.7", "keccak", ] @@ -3186,6 +3757,16 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3256,7 +3837,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags 2.13.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3347,6 +3928,36 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny2" version = "0.0.2" @@ -3562,6 +4173,22 @@ dependencies = [ "rand", ] +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -3891,7 +4518,7 @@ dependencies = [ "semver", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "smallvec", "target-lexicon", "wasm-encoder 0.248.0", @@ -3914,7 +4541,7 @@ dependencies = [ "rustix", "serde", "serde_derive", - "sha2", + "sha2 0.10.9", "toml", "wasmtime-environ", "windows-sys 0.61.2", @@ -4376,6 +5003,23 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "yoke" version = "0.8.3" @@ -4440,6 +5084,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 1c32b7dc0ab..696aec5d189 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,16 @@ flate2 = { version = "1.1.9", default-features = false } base64 = "0.22" crc32fast = "1.4" +# TLS backend (pyre-native only). The interpreter sees opaque, non-inlined +# entry points so rustls and its crypto provider stay outside Charon/LLBC. +rustls = { version = "0.23.39", default-features = false, features = ["std", "tls12", "aws_lc_rs"] } +rustls-pemfile = "2.2" +x509-parser = "0.18" +pem-rfc7468 = { version = "1", features = ["alloc"] } +der = { version = "0.8", features = ["alloc", "pem"] } +pkcs8 = { version = "0.11", features = ["encryption", "pkcs5", "pem"] } +rustls-native-certs = "0.8" + # math pymath = { version = "0.2.0", features = ["malachite-bigint"] } diff --git a/pyre/pyre-interpreter/src/importing.rs b/pyre/pyre-interpreter/src/importing.rs index 18bbdbd8eb0..45071eae27d 100644 --- a/pyre/pyre-interpreter/src/importing.rs +++ b/pyre/pyre-interpreter/src/importing.rs @@ -681,6 +681,8 @@ pub fn install_builtin_modules() { pyre_install_module!(termios); pyre_install_module!(_socket); #[cfg(not(target_arch = "wasm32"))] + pyre_install_module!(_ssl); + #[cfg(not(target_arch = "wasm32"))] pyre_install_module!(mmap); pyre_install_module!(_ctypes); #[cfg(not(target_arch = "wasm32"))] diff --git a/pyre/pyre-interpreter/src/lib.rs b/pyre/pyre-interpreter/src/lib.rs index 0181bc20887..356d1f71ebd 100644 --- a/pyre/pyre-interpreter/src/lib.rs +++ b/pyre/pyre-interpreter/src/lib.rs @@ -1124,6 +1124,19 @@ pub fn all_subclass_range_aliases() -> Vec()), + // rustls-backed `_ssl` native payloads. They are appended after the + // last pre-existing native class in the same order `build_gc` + // registers them, so no established type id moves. + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] + subclass_range_alias(170, typed::()), + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] + subclass_range_alias(171, typed::()), + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] + subclass_range_alias(172, typed::()), + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] + subclass_range_alias(173, typed::()), + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] + subclass_range_alias(174, typed::()), ] } diff --git a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs index 1e61756fef2..6daced67e35 100644 --- a/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs +++ b/pyre/pyre-interpreter/src/module/_socket/interp_socket.rs @@ -46,6 +46,16 @@ struct HostentRaw { h_addr_list: *mut *mut libc::c_char, } +/// Darwin's resolver may pack the pointer arrays owned by `hostent` at +/// byte-aligned addresses (for example, immediately after a C string). +/// C permits the resulting unaligned pointer loads; Rust references do not. +/// Read each array slot as raw C storage, matching RPython's rffi access in +/// `rsocket.gethost_common` without manufacturing an aligned reference. +#[cfg(unix)] +unsafe fn hostent_pointer_at(array: *mut *mut libc::c_char, index: usize) -> *mut libc::c_char { + unsafe { std::ptr::read_unaligned(array.add(index)) } +} + /// Minimal mirror of `struct servent` — we read `s_name` and `s_port`. #[cfg(unix)] #[repr(C)] @@ -57,6 +67,15 @@ struct ServentRaw { s_proto: *const libc::c_char, } +/// RPython `rsocket._get_netdb_lock_thread`: legacy gethostbyname/ +/// gethostbyaddr return pointers into one process-global static hostent. +/// Keep lookup and copying under the same process-global lock. +#[cfg(unix)] +fn netdb_lock() -> std::sync::MutexGuard<'static, ()> { + static NETDB_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); + NETDB_LOCK.lock().unwrap_or_else(|poisoned| poisoned.into_inner()) +} + /// _socket module — PyPy: pypy/module/_socket/. /// /// **Slice S1: constants + name resolution helpers.** @@ -852,6 +871,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let host_bytes = socket_idna_converter(args[0])?; let c = std::ffi::CString::new(host_bytes.clone()) .map_err(|_| crate::PyError::value_error("embedded null"))?; + let _netdb = netdb_lock(); let he = unsafe { gethostbyname(c.as_ptr()) }; if he.is_null() { let host_repr = String::from_utf8_lossy(&host_bytes).into_owned(); @@ -863,16 +883,16 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } unsafe { let h = &*he; - if h.h_length != 4 || (*h.h_addr_list).is_null() { + let first_addr = hostent_pointer_at(h.h_addr_list, 0); + if h.h_length != 4 || first_addr.is_null() { return Err(socket_converted_error( "gaierror", None, "gethostbyname: no IPv4 address", )); } - let addr_ptr = *h.h_addr_list; let addr = libc::in_addr { - s_addr: *(addr_ptr as *const u32), + s_addr: std::ptr::read_unaligned(first_addr as *const u32), }; let p = inet_ntoa(addr); Ok(pyre_object::w_str_new( @@ -901,6 +921,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let host_bytes = socket_idna_converter(args[0])?; let c = std::ffi::CString::new(host_bytes.clone()) .map_err(|_| crate::PyError::value_error("embedded null"))?; + let _netdb = netdb_lock(); let he = unsafe { gethostbyname(c.as_ptr()) }; if he.is_null() { let host_repr = String::from_utf8_lossy(&host_bytes).into_owned(); @@ -934,6 +955,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { let host_bytes = socket_idna_converter(args[0])?; let c = std::ffi::CString::new(host_bytes.clone()) .map_err(|_| crate::PyError::value_error("embedded null"))?; + let _netdb = netdb_lock(); // Try IPv4 first, then IPv6, then fall back to // gethostbyname → hostent.h_addr to obtain a raw // bytestring for gethostbyaddr. @@ -994,7 +1016,8 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } unsafe { let h = &*he; - if (*h.h_addr_list).is_null() { + let first_addr = hostent_pointer_at(h.h_addr_list, 0); + if first_addr.is_null() { return Err(socket_converted_error( "herror", None, @@ -1003,7 +1026,7 @@ pub fn register_module(ns: pyre_object::PyObjectRef) { } ( h.h_addrtype as libc::c_int, - *h.h_addr_list as *const libc::c_void, + first_addr as *const libc::c_void, h.h_length as libc::socklen_t, ) } @@ -1582,31 +1605,45 @@ fn unpack_hostent(he: *mut HostentRaw) -> Result Result pyre_object::PyObjectRef { *SOCKET_TYPE_OBJ.get_or_init(|| { let tp = crate::typedef::make_builtin_type("socket", init_socket_type); unsafe { pyre_object::typeobject::w_type_set_hasdict(tp, true) }; + unsafe { pyre_object::w_type_set_hasuserdel(tp, true) }; tp as usize }) as pyre_object::PyObjectRef } @@ -1970,6 +2016,71 @@ fn socket_io_err(e: std::io::Error) -> crate::PyError { crate::PyError::os_error_errno_strerror(errno, strerror) } +#[cfg(unix)] +fn socket_io_err_for_operation( + obj: pyre_object::PyObjectRef, + e: std::io::Error, +) -> crate::PyError { + let errno = e.raw_os_error().unwrap_or(0); + if errno == libc::EAGAIN || errno == libc::EWOULDBLOCK { + let d = crate::baseobjspace::getdict_native(obj); + if !d.is_null() + && let Some(timeout) = unsafe { pyre_object::w_dict_getitem_str(d, "_timeout") } + && unsafe { pyre_object::is_float(timeout) } + && unsafe { pyre_object::floatobject::w_float_get_value(timeout) } > 0.0 + { + // RPython's RSocket._select() turns expiry of a positive timeout + // into SocketTimeout, which interp_socket maps to TimeoutError. + // This backend uses SO_RCVTIMEO/SO_SNDTIMEO, whose equivalent + // expiry signal is EAGAIN/EWOULDBLOCK. + return socket_converted_error("timeout", None, "timed out"); + } + } + socket_io_err(e) +} + +/// RPython `RSocket._select(False)` used by `RSocket.accept`: a positive +/// Python timeout is enforced with poll before entering the libc operation. +/// Darwin does not reliably apply SO_RCVTIMEO to accept(), so relying on that +/// socket option leaves timeout-driven server loops blocked indefinitely. +#[cfg(unix)] +fn socket_wait_readable( + obj: pyre_object::PyObjectRef, + fd: libc::c_int, +) -> Result<(), crate::PyError> { + let dict = crate::baseobjspace::getdict_native(obj); + let Some(timeout) = (!dict.is_null()) + .then(|| unsafe { pyre_object::w_dict_getitem_str(dict, "_timeout") }) + .flatten() + .filter(|timeout| unsafe { pyre_object::is_float(*timeout) }) + .map(|timeout| unsafe { pyre_object::floatobject::w_float_get_value(timeout) }) + .filter(|timeout| *timeout > 0.0) + else { + return Ok(()); + }; + let timeout_ms = (timeout * 1000.0 + 0.5).min(i32::MAX as f64) as i32; + loop { + let mut pollfd = libc::pollfd { + fd, + events: libc::POLLIN, + revents: 0, + }; + let (ready, errno) = crate::module::thread::call_external_function(|| unsafe { + libc::poll(&mut pollfd, 1, timeout_ms) + }); + if ready > 0 { + return Ok(()); + } + if ready == 0 { + return Err(socket_converted_error("timeout", None, "timed out")); + } + if errno != libc::EINTR { + return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + } + crate::module::signal::interp_signal::checksignals_now()?; + } +} + #[cfg(unix)] fn socket_get_attr_i64(obj: pyre_object::PyObjectRef, key: &str) -> i64 { let d = crate::baseobjspace::getdict_native(obj); @@ -2269,12 +2380,19 @@ fn pack_inet_addr( sin.sin_family = libc::AF_INET as libc::sa_family_t; sin.sin_port = port; // inet_pton handles both "0.0.0.0" and dotted-quad. - let r = unsafe { - inet_pton( - libc::AF_INET, - c_host.as_ptr(), - &mut sin.sin_addr as *mut _ as *mut libc::c_void, - ) + let r = if host.is_empty() { + // RSocket.makeipaddr('', result) uses the wildcard address for + // bind(), as required by socketserver and socket_helper.bind_port. + sin.sin_addr.s_addr = libc::INADDR_ANY; + 1 + } else { + unsafe { + inet_pton( + libc::AF_INET, + c_host.as_ptr(), + &mut sin.sin_addr as *mut _ as *mut libc::c_void, + ) + } }; if r != 1 { // `rsocket.makeipaddr` resolves names through getaddrinfo and @@ -2322,12 +2440,16 @@ fn pack_inet_addr( sin6.sin6_family = libc::AF_INET6 as libc::sa_family_t; sin6.sin6_port = port; let mut buf = [0u8; 16]; - let r = unsafe { - inet_pton( - libc::AF_INET6, - c_host.as_ptr(), - buf.as_mut_ptr() as *mut libc::c_void, - ) + let r = if host.is_empty() { + 1 + } else { + unsafe { + inet_pton( + libc::AF_INET6, + c_host.as_ptr(), + buf.as_mut_ptr() as *mut libc::c_void, + ) + } }; if r != 1 { return Err(crate::PyError::os_error(format!( @@ -2450,6 +2572,30 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { Ok(pyre_object::w_instance_new(cls)) }), ) }; + unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ns, + "__del__", + crate::make_builtin_function_with_arity( + "__del__", + |args| { + let obj = args.first().copied().unwrap_or(pyre_object::PY_NULL); + let fd = socket_get_attr_i64(obj, "_fd") as libc::c_int; + if fd >= 0 { + if let Ok(repr) = unsafe { crate::display::py_repr_wtf8(obj) } { + let _ = crate::warn::warn_category( + &format!("unclosed {}", repr.to_string_lossy()), + "ResourceWarning", + 1, + ); + } + let _ = unsafe { libc::close(fd) }; + socket_set_attr(obj, "_fd", pyre_object::w_int_new(-1)); + } + Ok(pyre_object::w_none()) + }, + 1, + ), + ) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__init__", @@ -2768,6 +2914,7 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { |args| { let obj = args.first().copied().unwrap_or(pyre_object::PY_NULL); let fd = socket_fd(obj)?; + socket_wait_readable(obj, fd)?; let family = socket_get_attr_i64(obj, "_family") as libc::c_int; let ty = socket_get_attr_i64(obj, "_type") as libc::c_int; let proto = socket_get_attr_i64(obj, "_proto") as libc::c_int; @@ -2781,7 +2928,10 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { break r; } if errno != libc::EINTR { - return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + return Err(socket_io_err_for_operation( + obj, + std::io::Error::from_raw_os_error(errno), + )); } // EINTR: deliver a pending signal, then retry // (`converted_error` eintr_retry). @@ -2814,6 +2964,7 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { |args| { let obj = args.first().copied().unwrap_or(pyre_object::PY_NULL); let fd = socket_fd(obj)?; + socket_wait_readable(obj, fd)?; let mut storage: libc::sockaddr_storage = unsafe { std::mem::zeroed() }; let mut slen = core::mem::size_of::() as libc::socklen_t; let cfd = loop { @@ -2824,7 +2975,10 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { break r; } if errno != libc::EINTR { - return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + return Err(socket_io_err_for_operation( + obj, + std::io::Error::from_raw_os_error(errno), + )); } // EINTR: deliver a pending signal, then retry // (`converted_error` eintr_retry). @@ -2946,7 +3100,10 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { return Ok(r); } if errno != libc::EINTR { - return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + return Err(socket_io_err_for_operation( + obj, + std::io::Error::from_raw_os_error(errno), + )); } // EINTR: deliver a pending signal, then retry // (`converted_error` eintr_retry). @@ -2996,7 +3153,10 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { crate::module::signal::interp_signal::checksignals_now()?; continue; } - return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + return Err(socket_io_err_for_operation( + obj, + std::io::Error::from_raw_os_error(errno), + )); } off += n as usize; } @@ -3042,7 +3202,10 @@ fn init_socket_type(ns: pyre_object::PyObjectRef) { break r; } if errno != libc::EINTR { - return Err(socket_io_err(std::io::Error::from_raw_os_error(errno))); + return Err(socket_io_err_for_operation( + obj, + std::io::Error::from_raw_os_error(errno), + )); } // EINTR: deliver a pending signal, then retry // (`converted_error` eintr_retry). diff --git a/pyre/pyre-interpreter/src/module/_ssl/mod.rs b/pyre/pyre-interpreter/src/module/_ssl/mod.rs new file mode 100644 index 00000000000..2dc769ea261 --- /dev/null +++ b/pyre/pyre-interpreter/src/module/_ssl/mod.rs @@ -0,0 +1,2388 @@ +//! Rustls-backed `_ssl` module. +//! +//! The public policy and object wrappers remain the unmodified CPython +//! `ssl.py`. This module supplies its low-level primitives while the actual +//! TLS engine lives in `pyre-native`, outside the translated interpreter. + +use pyre_object::*; + +const PROTOCOL_TLS: i32 = 2; +const PROTOCOL_TLS_CLIENT: i32 = 16; +const PROTOCOL_TLS_SERVER: i32 = 17; +const CERT_NONE: i32 = 0; +const CERT_OPTIONAL: i32 = 1; +const CERT_REQUIRED: i32 = 2; + +/// The mapdict prefix is required because `ssl.SSLContext` is an app-level +/// subclass of this native type. PyPy composes `MapdictStorageMixin` into +/// that allocation; pyre preserves the same native prefix. +#[crate::pyre_class("_ssl._SSLContext")] +#[derive(Default)] +pub struct W_SSLContext { + pub map: *const u8, + pub storage: *mut pyre_object::object_array::ItemsBlock, + pub backend: *mut pyre_native::ssl::Context, + pub sni_callback: PyObjectRef, + pub msg_callback: PyObjectRef, + pub keylog_filename: PyObjectRef, + pub host_flags: i32, + pub post_handshake_auth: bool, + pub num_tickets: i32, +} + +/// `ssl.MemoryBIO` is subclassable in CPython, so it uses the same mapdict +/// prefix rather than relying on a side table for subclass attributes. +#[crate::pyre_class("_ssl.MemoryBIO")] +#[derive(Default)] +pub struct W_MemoryBIO { + pub map: *const u8, + pub storage: *mut pyre_object::object_array::ItemsBlock, + pub backend: *mut pyre_native::ssl::MemoryBio, +} + +const _: () = assert!( + std::mem::offset_of!(W_SSLContext, map) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, map), + "W_SSLContext must keep W_ObjectObject's map offset" +); +const _: () = assert!( + std::mem::offset_of!(W_SSLContext, storage) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, storage), + "W_SSLContext must keep W_ObjectObject's storage offset" +); +const _: () = assert!( + std::mem::offset_of!(W_MemoryBIO, map) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, map), + "W_MemoryBIO must keep W_ObjectObject's map offset" +); +const _: () = assert!( + std::mem::offset_of!(W_MemoryBIO, storage) + == std::mem::offset_of!(pyre_object::objectobject::W_ObjectObject, storage), + "W_MemoryBIO must keep W_ObjectObject's storage offset" +); + +#[crate::pyre_class("_ssl.SSLSession")] +#[derive(Default)] +pub struct W_SSLSession { + pub backend: *mut pyre_native::ssl::NativeSession, +} + +/// One TLS state machine plus its Python-owned transport endpoints. Rustls +/// remains opaque in `pyre-native`; these references preserve the same +/// per-object ownership shape as PyPy's `W_SSLObject`. +#[crate::pyre_class("_ssl._SSLSocket")] +#[derive(Default)] +pub struct W_SSLSocket { + pub backend: *mut pyre_native::ssl::TlsConnection, + pub context: PyObjectRef, + pub socket: PyObjectRef, + pub socket_send: PyObjectRef, + pub socket_recv: PyObjectRef, + pub incoming: PyObjectRef, + pub outgoing: PyObjectRef, + pub owner: PyObjectRef, + pub server_hostname: PyObjectRef, + pub server_side: bool, + pub shutdown_started: bool, + pub requested_session: *mut pyre_native::ssl::NativeSession, +} + +#[crate::pyre_class("_ssl.Certificate")] +#[derive(Default)] +pub struct W_Certificate { + pub der: PyObjectRef, +} + +fn ssl_error(message: impl Into) -> crate::PyError { + let message = message.into(); + let mut err = crate::PyError::os_error(message.clone()); + if let Some(cls) = crate::builtins::lookup_exc_class("_ssl.SSLError") { + if let Ok(exc) = + crate::builtins::exc_exception_new(&[cls, w_int_new(0), w_str_new(&message)]) + { + if let Some(close) = message.find(']') + && message.starts_with('[') + && let Some((library, reason)) = message[1..close].split_once(": ") + { + let _ = crate::baseobjspace::setattr_str(exc, "library", w_str_new(library)); + let _ = crate::baseobjspace::setattr_str(exc, "reason", w_str_new(reason)); + } + err.exc_object = exc; + } + } + err +} + +/// CPython's `SSLError` is an `OSError` subclass, but its string form is the +/// TLS message rather than the generic two-item exception tuple. Keep this +/// policy in the `_ssl` boundary instead of teaching the process-wide +/// exception formatter about an extension-module class. +fn ssl_error_str(args: &[PyObjectRef]) -> crate::PyResult { + let w_args = unsafe { pyre_object::interp_exceptions::w_exception_get_args(args[0]) }; + let len = unsafe { pyre_object::w_tuple_len(w_args) }; + if len >= 2 { + let message = unsafe { + pyre_object::w_tuple_getitem(w_args, 1) + .expect("SSLError argument tuple has a second element") + }; + crate::builtins::builtin_str(&[message]) + } else { + Ok(pyre_object::w_str_from_wtf8(unsafe { + crate::display::base_exception_str_wtf8(args[0])? + })) + } +} + +fn native_result(result: Result) -> Result { + result.map_err(|(errno, message)| { + if errno != 0 { + crate::PyError::os_error_with_errno(errno, message) + } else { + ssl_error(message) + } + }) +} + +fn tls_error(code: i32, message: String) -> crate::PyError { + let verify_code = (code >= pyre_native::ssl::TLS_ERROR_CERT_VERIFY_BASE) + .then_some(code - pyre_native::ssl::TLS_ERROR_CERT_VERIFY_BASE); + let class_name = match code { + pyre_native::ssl::TLS_ERROR_WANT_READ => "_ssl.SSLWantReadError", + pyre_native::ssl::TLS_ERROR_WANT_WRITE => "_ssl.SSLWantWriteError", + pyre_native::ssl::TLS_ERROR_ZERO_RETURN => "_ssl.SSLZeroReturnError", + pyre_native::ssl::TLS_ERROR_EOF => "_ssl.SSLEOFError", + _ if verify_code.is_some() => "_ssl.SSLCertVerificationError", + _ => "_ssl.SSLError", + }; + let mut error = ssl_error(message.clone()); + let public_errno = if verify_code.is_some() { + pyre_native::ssl::TLS_ERROR_SSL + } else { + code + }; + if let Some(class) = crate::builtins::lookup_exc_class(class_name) + && let Ok(exception) = crate::builtins::exc_os_error_new(&[ + class, + w_int_new(public_errno as i64), + w_str_new(&message), + ]) + { + if let Some(close) = message.find(']') + && message.starts_with('[') + && let Some((library, reason)) = message[1..close].split_once(": ") + { + let _ = crate::baseobjspace::setattr_str(exception, "library", w_str_new(library)); + let _ = crate::baseobjspace::setattr_str(exception, "reason", w_str_new(reason)); + } + if let Some(verify_code) = verify_code { + let _ = crate::baseobjspace::setattr_str( + exception, + "verify_code", + w_int_new(verify_code as i64), + ); + let _ = crate::baseobjspace::setattr_str( + exception, + "verify_message", + w_str_new(pyre_native::ssl::certificate_verify_message(verify_code)), + ); + let _ = crate::baseobjspace::setattr_str(exception, "library", w_str_new("SSL")); + let _ = crate::baseobjspace::setattr_str( + exception, + "reason", + w_str_new("CERTIFICATE_VERIFY_FAILED"), + ); + } + error.exc_object = exception; + } + error +} + +fn tls_result(result: pyre_native::ssl::TlsResult) -> Result { + result.map_err(|(code, message)| tls_error(code, message)) +} + +fn path_string(obj: PyObjectRef) -> Result { + if obj.is_null() || unsafe { is_none(obj) || is_bool(obj) } { + return Err(crate::PyError::type_error( + "path should be string, bytes, os.PathLike or integer, not NoneType", + )); + } + Ok(crate::gateway::fspath_buf(obj)? + .to_string_lossy() + .into_owned()) +} + +fn password_bytes(obj: PyObjectRef) -> Result>, crate::PyError> { + if obj.is_null() || unsafe { is_none(obj) } { + return Ok(None); + } + let callable = !unsafe { is_str(obj) } + && !unsafe { bytesobject::is_bytes_like(obj) } + && crate::baseobjspace::getattr_str(obj, "__call__").is_ok(); + let value = if callable { + crate::call::call_function_impl_result(obj, &[])? + } else { + obj + }; + let bytes = if unsafe { is_str(value) } { + crate::baseobjspace::str_utf8_w(value)?.as_bytes().to_vec() + } else if let Some(buffer) = crate::baseobjspace::simple_buffer_bytes(value)? { + let bytes = buffer.as_bytes().to_vec(); + buffer.release(); + bytes + } else { + return Err(crate::PyError::type_error(if callable { + "password callback must return a string" + } else { + "password should be a string or callable" + })); + }; + if bytes.len() > 1024 { + return Err(crate::PyError::value_error( + "password cannot be longer than 1024 bytes", + )); + } + Ok(Some(bytes)) +} + +fn dict_from_pairs(items: &[(&str, PyObjectRef)]) -> PyObjectRef { + let dict = w_dict_new(); + for (key, value) in items { + unsafe { w_dict_setitem_str(dict, key, *value) }; + } + dict +} + +fn decoded_name(cert: *const pyre_native::ssl::DecodedCertificate, subject: bool) -> PyObjectRef { + let rdn_count = unsafe { pyre_native::ssl::certificate_name_rdn_count(cert, subject) }; + let mut rdns = Vec::with_capacity(rdn_count); + for rdn in 0..rdn_count { + let attribute_count = + unsafe { pyre_native::ssl::certificate_name_attribute_count(cert, subject, rdn) }; + let mut attributes = Vec::with_capacity(attribute_count); + for attribute in 0..attribute_count { + let key = unsafe { + pyre_native::ssl::certificate_name_attribute_key(cert, subject, rdn, attribute) + }; + let value = unsafe { + pyre_native::ssl::certificate_name_attribute_value(cert, subject, rdn, attribute) + }; + attributes.push(w_tuple_new(vec![w_str_new(&key), w_str_new(&value)])); + } + rdns.push(w_tuple_new(attributes)); + } + w_tuple_new(rdns) +} + +fn decoded_directory_name( + cert: *const pyre_native::ssl::DecodedCertificate, + san: usize, +) -> PyObjectRef { + let rdn_count = unsafe { pyre_native::ssl::certificate_san_directory_rdn_count(cert, san) }; + let mut rdns = Vec::with_capacity(rdn_count); + for rdn in 0..rdn_count { + let count = + unsafe { pyre_native::ssl::certificate_san_directory_attribute_count(cert, san, rdn) }; + let mut attributes = Vec::with_capacity(count); + for attribute in 0..count { + let key = unsafe { + pyre_native::ssl::certificate_san_directory_attribute_key(cert, san, rdn, attribute) + }; + let value = unsafe { + pyre_native::ssl::certificate_san_directory_attribute_value( + cert, san, rdn, attribute, + ) + }; + attributes.push(w_tuple_new(vec![w_str_new(&key), w_str_new(&value)])); + } + rdns.push(w_tuple_new(attributes)); + } + w_tuple_new(rdns) +} + +fn decoded_urls( + cert: *const pyre_native::ssl::DecodedCertificate, + kind: i32, +) -> Option { + let count = unsafe { pyre_native::ssl::certificate_url_count(cert, kind) }; + if count == 0 { + return None; + } + Some(w_tuple_new( + (0..count) + .map(|index| { + w_str_new(&unsafe { pyre_native::ssl::certificate_url(cert, kind, index) }) + }) + .collect(), + )) +} + +fn decoded_certificate_dict(cert: *mut pyre_native::ssl::DecodedCertificate) -> PyObjectRef { + let dict = dict_from_pairs(&[ + ("issuer", decoded_name(cert, false)), + ( + "notAfter", + w_str_new(&unsafe { pyre_native::ssl::certificate_not_after(cert) }), + ), + ( + "notBefore", + w_str_new(&unsafe { pyre_native::ssl::certificate_not_before(cert) }), + ), + ( + "serialNumber", + w_str_new(&unsafe { pyre_native::ssl::certificate_serial_number(cert) }), + ), + ("subject", decoded_name(cert, true)), + ( + "version", + w_int_new(unsafe { pyre_native::ssl::certificate_version(cert) } as i64), + ), + ]); + for (name, kind) in [("OCSP", 0), ("caIssuers", 1), ("crlDistributionPoints", 2)] { + if let Some(urls) = decoded_urls(cert, kind) { + unsafe { w_dict_setitem_str(dict, name, urls) }; + } + } + let san_count = unsafe { pyre_native::ssl::certificate_san_count(cert) }; + if san_count != 0 { + let mut names = Vec::with_capacity(san_count); + for index in 0..san_count { + let kind = unsafe { pyre_native::ssl::certificate_san_kind(cert, index) }; + let value = if kind == "DirName" { + decoded_directory_name(cert, index) + } else { + w_str_new(&unsafe { pyre_native::ssl::certificate_san_value(cert, index) }) + }; + names.push(w_tuple_new(vec![w_str_new(kind), value])); + } + unsafe { w_dict_setitem_str(dict, "subjectAltName", w_tuple_new(names)) }; + } + unsafe { pyre_native::ssl::certificate_free(cert) }; + dict +} + +fn parse_server_hostname( + value: Option, +) -> Result<(Option, PyObjectRef), crate::PyError> { + let Some(value) = value.filter(|value| !unsafe { is_none(*value) }) else { + return Ok((None, w_none())); + }; + if !unsafe { is_str(value) } { + return Err(crate::PyError::type_error( + "server_hostname must be a string", + )); + } + let hostname = crate::baseobjspace::str_utf8_w(value)?.to_string(); + if hostname.as_bytes().contains(&0) { + return Err(crate::PyError::type_error( + "argument must be encoded string without null bytes", + )); + } + if hostname.is_empty() || hostname.starts_with('.') { + return Err(crate::PyError::value_error( + "server_hostname cannot be an empty string or start with a leading dot", + )); + } + if !hostname.is_ascii() { + return Err(crate::PyError::value_error( + "server_hostname must contain ASCII only", + )); + } + Ok((Some(hostname.clone()), w_str_new(&hostname))) +} + +fn import_module(name: &str) -> Result { + crate::importing::dunder_import( + name, + w_none(), + w_none(), + w_none(), + 0, + crate::call::getexecutioncontext(), + )?; + crate::importing::get_sys_module(name).ok_or_else(|| { + crate::PyError::new( + crate::PyErrorKind::ImportError, + format!("No module named '{name}'"), + ) + }) +} + +fn allocate_ssl_socket( + backend: *mut pyre_native::ssl::TlsConnection, + context: PyObjectRef, + socket: PyObjectRef, + socket_send: PyObjectRef, + socket_recv: PyObjectRef, + incoming: PyObjectRef, + outgoing: PyObjectRef, + owner: PyObjectRef, + server_hostname: PyObjectRef, + server_side: bool, + requested_session: *mut pyre_native::ssl::NativeSession, +) -> PyObjectRef { + let _ = ssl_socket_methods::type_object(); + let _roots = pyre_object::gc_roots::push_roots(); + for value in [ + context, + socket, + socket_send, + socket_recv, + incoming, + outgoing, + owner, + server_hostname, + ] { + pyre_object::gc_roots::pin_root(value); + } + let first = pyre_object::gc_roots::shadow_stack_len() - 8; + let rooted_socket = unsafe { pyre_object::gc_roots::shadow_stack_get(first + 1) }; + if !unsafe { is_none(rooted_socket) } { + let weak_socket = pyre_object::weakref::w_gc_weakref_box_new_or_strong(rooted_socket); + pyre_object::gc_roots::shadow_stack_set(first + 1, weak_socket); + } + let rooted_owner = unsafe { pyre_object::gc_roots::shadow_stack_get(first + 6) }; + if !unsafe { is_none(rooted_owner) } { + let weak_owner = pyre_object::weakref::w_gc_weakref_box_new_or_strong(rooted_owner); + pyre_object::gc_roots::shadow_stack_set(first + 6, weak_owner); + } + W_SSLSocket::allocate_stable(W_SSLSocket { + ob: PyObject { + ob_type: std::ptr::null(), + w_class: std::ptr::null_mut(), + }, + backend, + context: unsafe { pyre_object::gc_roots::shadow_stack_get(first) }, + socket: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 1) }, + socket_send: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 2) }, + socket_recv: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 3) }, + incoming: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 4) }, + outgoing: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 5) }, + owner: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 6) }, + server_hostname: unsafe { pyre_object::gc_roots::shadow_stack_get(first + 7) }, + server_side, + shutdown_started: false, + requested_session, + }) +} + +fn allocate_ssl_session(backend: *mut pyre_native::ssl::NativeSession) -> PyObjectRef { + let _ = ssl_session_methods::type_object(); + W_SSLSession::allocate_stable(W_SSLSession { + ob: PyObject { + ob_type: std::ptr::null(), + w_class: std::ptr::null_mut(), + }, + backend, + }) +} + +fn clone_requested_session( + value: Option, + context: *mut pyre_native::ssl::Context, +) -> Result<*mut pyre_native::ssl::NativeSession, crate::PyError> { + let Some(value) = value.filter(|value| !unsafe { is_none(*value) }) else { + return Ok(std::ptr::null_mut()); + }; + let session = W_SSLSession::from_obj(value) + .ok_or_else(|| crate::PyError::type_error("Value is not a SSLSession."))?; + if unsafe { pyre_native::ssl::session_context_identity(session.backend) } != context as usize { + return Err(crate::PyError::value_error( + "Session refers to a different SSLContext.", + )); + } + Ok(unsafe { pyre_native::ssl::session_clone(session.backend) }) +} + +mod context_methods { + use super::*; + + fn pin_and_allocate_context( + cls: PyObjectRef, + backend: *mut pyre_native::ssl::Context, + ) -> PyObjectRef { + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(cls); + let cls_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let obj = W_SSLContext::allocate_stable(W_SSLContext { + ob: PyObject { + ob_type: std::ptr::null(), + w_class: std::ptr::null_mut(), + }, + map: std::ptr::null(), + storage: std::ptr::null_mut(), + backend, + sni_callback: w_none(), + msg_callback: w_none(), + keylog_filename: w_none(), + host_flags: 0, + post_handshake_auth: false, + num_tickets: 2, + }); + crate::typedef::tag_subclass_instance(obj, unsafe { + pyre_object::gc_roots::shadow_stack_get(cls_slot) + }) + } + + #[crate::pyre_methods(doc = "An SSLContext holds TLS configuration and state.")] + impl W_SSLContext { + #[staticmethod] + fn __new__(cls: PyObjectRef, args: &[PyObjectRef]) -> Result { + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + if crate::builtins::has_real_kwargs(kwargs) { + return Err(crate::PyError::type_error( + "_SSLContext.__new__() takes no keyword arguments", + )); + } + let protocol_obj = positional.get(1).copied().ok_or_else(|| { + crate::PyError::type_error( + "_SSLContext.__new__() missing required argument 'protocol'", + ) + })?; + if positional.len() != 2 { + return Err(crate::PyError::type_error(format!( + "_SSLContext.__new__() takes 2 arguments ({} given)", + positional.len() + ))); + } + crate::typedef::check_user_subclass(type_object(), cls)?; + let protocol = crate::baseobjspace::int_w(protocol_obj)?; + let protocol = i32::try_from(protocol).map_err(|_| { + crate::PyError::value_error("invalid or unsupported protocol version") + })?; + let backend = pyre_native::ssl::context_new(protocol) + .map_err(|message| crate::PyError::value_error(message))?; + if matches!(protocol, PROTOCOL_TLS | 3 | 4 | 5) { + crate::warn::warn_deprecation(&format!( + "ssl.PROTOCOL_{} is deprecated", + match protocol { + PROTOCOL_TLS => "TLS", + 3 => "TLSv1", + 4 => "TLSv1_1", + 5 => "TLSv1_2", + _ => unreachable!(), + } + ))?; + } + Ok(pin_and_allocate_context(cls, backend)) + } + + #[getter] + fn protocol(&self) -> i64 { + unsafe { pyre_native::ssl::context_protocol(self.backend) as i64 } + } + + #[getter] + fn check_hostname(&self) -> bool { + unsafe { pyre_native::ssl::context_check_hostname(self.backend) } + } + + #[setter] + fn set_check_hostname(&mut self, value: bool) { + unsafe { + pyre_native::ssl::context_set_check_hostname(self.backend, value); + if value && pyre_native::ssl::context_verify_mode(self.backend) == CERT_NONE { + pyre_native::ssl::context_set_verify_mode(self.backend, CERT_REQUIRED); + } + } + } + + #[getter] + fn verify_mode(&self) -> i64 { + unsafe { pyre_native::ssl::context_verify_mode(self.backend) as i64 } + } + + #[setter] + fn set_verify_mode(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let mode = crate::baseobjspace::int_w(value)?; + if !(CERT_NONE as i64..=CERT_REQUIRED as i64).contains(&mode) { + return Err(crate::PyError::value_error("invalid verify mode")); + } + if mode == CERT_NONE as i64 && self.check_hostname() { + return Err(crate::PyError::value_error( + "Cannot set verify_mode to CERT_NONE when check_hostname is enabled", + )); + } + unsafe { pyre_native::ssl::context_set_verify_mode(self.backend, mode as i32) }; + Ok(()) + } + + #[getter] + fn verify_flags(&self) -> i64 { + unsafe { pyre_native::ssl::context_verify_flags(self.backend) as i64 } + } + + #[setter] + fn set_verify_flags(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = crate::baseobjspace::int_w(value)?; + let value = i32::try_from(value) + .map_err(|_| crate::PyError::overflow_error("verify_flags out of range"))?; + unsafe { pyre_native::ssl::context_set_verify_flags(self.backend, value) }; + Ok(()) + } + + #[getter] + fn options(&self) -> i64 { + unsafe { pyre_native::ssl::context_options(self.backend) as i64 } + } + + #[setter] + fn set_options(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = crate::baseobjspace::uint_w(value)?; + let old = unsafe { pyre_native::ssl::context_options(self.backend) }; + const DEPRECATED: u64 = + 0x0200_0000 | 0x0400_0000 | 0x1000_0000 | 0x0800_0000 | 0x2000_0000; + if (!old & value) & DEPRECATED != 0 { + crate::warn::warn_deprecation( + "ssl.OP_NO_SSL*/ssl.OP_NO_TLS* options are deprecated", + )?; + } + unsafe { pyre_native::ssl::context_set_options(self.backend, value) }; + Ok(()) + } + + #[getter] + fn minimum_version(&self) -> i64 { + unsafe { pyre_native::ssl::context_minimum_version(self.backend) as i64 } + } + + #[setter] + fn set_minimum_version(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = validate_tls_version(crate::baseobjspace::int_w(value)?)?; + let value = if value == -1 { 0x304 } else { value }; + unsafe { pyre_native::ssl::context_set_minimum_version(self.backend, value) }; + Ok(()) + } + + #[getter] + fn maximum_version(&self) -> i64 { + unsafe { pyre_native::ssl::context_maximum_version(self.backend) as i64 } + } + + #[setter] + fn set_maximum_version(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = validate_tls_version(crate::baseobjspace::int_w(value)?)?; + let value = if value == -2 { 0x303 } else { value }; + unsafe { pyre_native::ssl::context_set_maximum_version(self.backend, value) }; + Ok(()) + } + + #[getter] + fn _host_flags(&self) -> i64 { + self.host_flags as i64 + } + + #[setter("_host_flags")] + fn set_host_flags(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = crate::baseobjspace::int_w(value)?; + self.host_flags = i32::try_from(value) + .map_err(|_| crate::PyError::overflow_error("_host_flags out of range"))?; + Ok(()) + } + + #[getter] + fn post_handshake_auth(&self) -> bool { + self.post_handshake_auth + } + + #[setter] + fn set_post_handshake_auth(&mut self, value: bool) { + self.post_handshake_auth = value; + } + + #[getter] + fn num_tickets(&self) -> i64 { + self.num_tickets as i64 + } + + #[setter] + fn set_num_tickets(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + let value = crate::baseobjspace::int_w(value)?; + if value < 0 { + return Err(crate::PyError::value_error( + "num_tickets must be a non-negative integer", + )); + } + if self.protocol() != PROTOCOL_TLS_SERVER as i64 { + return Err(crate::PyError::value_error( + "num_tickets can only be set on server-side contexts", + )); + } + self.num_tickets = i32::try_from(value) + .map_err(|_| crate::PyError::overflow_error("num_tickets out of range"))?; + Ok(()) + } + + #[getter] + fn sni_callback(&self) -> PyObjectRef { + self.sni_callback + } + + #[setter] + fn set_sni_callback(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + if !unsafe { is_none(value) } + && crate::baseobjspace::getattr_str(value, "__call__").is_err() + { + return Err(crate::PyError::type_error("not a callable object")); + } + pyre_object::gc_hook::try_gc_write_barrier(self as *mut W_SSLContext as *mut u8); + self.sni_callback = value; + Ok(()) + } + + #[getter] + fn _msg_callback(&self) -> PyObjectRef { + self.msg_callback + } + + #[setter("_msg_callback")] + fn set_msg_callback(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + if !unsafe { is_none(value) } + && crate::baseobjspace::getattr_str(value, "__call__").is_err() + { + return Err(crate::PyError::type_error(format!( + "{} is not callable.", + crate::type_methods::arg_type_name(value) + ))); + } + pyre_object::gc_hook::try_gc_write_barrier(self as *mut W_SSLContext as *mut u8); + self.msg_callback = value; + Ok(()) + } + + fn load_cert_chain(&mut self, args: &[PyObjectRef]) -> Result<(), crate::PyError> { + const KEYWORDS: &[&str] = &["certfile", "keyfile", "password"]; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let user = positional.get(1..).unwrap_or(&[]); + if user.len() > 3 { + return Err(crate::PyError::type_error( + "load_cert_chain() takes at most 3 arguments", + )); + } + let cert = + crate::builtins::bind_pos_or_kw(user, kwargs, 0, "certfile", "load_cert_chain", 1)? + .ok_or_else(|| { + crate::PyError::type_error( + "load_cert_chain() missing required argument 'certfile'", + ) + })?; + let key = + crate::builtins::bind_pos_or_kw(user, kwargs, 1, "keyfile", "load_cert_chain", 2)?; + let password = + crate::builtins::bind_pos_or_kw(user, kwargs, 2, "password", "load_cert_chain", 3)?; + crate::builtins::kwarg_reject_unknown(kwargs, KEYWORDS, "load_cert_chain")?; + let cert_path = path_string(cert)?; + let key_path = match key { + Some(value) if !unsafe { is_none(value) } => path_string(value)?, + _ => cert_path.clone(), + }; + // OpenSSL asks its callback only after PEM parsing discovers an + // encrypted key. Preserve that observable ordering: an irrelevant + // callback must not run for an unencrypted key file. + let password = if std::fs::read(&key_path).ok().is_some_and(|data| { + data.windows(b"ENCRYPTED PRIVATE KEY".len()) + .any(|window| window == b"ENCRYPTED PRIVATE KEY") + || data + .windows(b"Proc-Type: 4,ENCRYPTED".len()) + .any(|window| window == b"Proc-Type: 4,ENCRYPTED") + }) { + password_bytes(password.unwrap_or_else(w_none))? + } else { + None + }; + native_result(unsafe { + pyre_native::ssl::context_load_cert_chain( + self.backend, + &cert_path, + &key_path, + password.as_deref(), + ) + }) + } + + fn load_verify_locations(&mut self, args: &[PyObjectRef]) -> Result<(), crate::PyError> { + const KEYWORDS: &[&str] = &["cafile", "capath", "cadata"]; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let user = positional.get(1..).unwrap_or(&[]); + if user.len() > 3 { + return Err(crate::PyError::type_error( + "load_verify_locations() takes at most 3 arguments", + )); + } + let cafile = crate::builtins::bind_pos_or_kw( + user, + kwargs, + 0, + "cafile", + "load_verify_locations", + 1, + )?; + let capath = crate::builtins::bind_pos_or_kw( + user, + kwargs, + 1, + "capath", + "load_verify_locations", + 2, + )?; + let cadata = crate::builtins::bind_pos_or_kw( + user, + kwargs, + 2, + "cadata", + "load_verify_locations", + 3, + )?; + crate::builtins::kwarg_reject_unknown(kwargs, KEYWORDS, "load_verify_locations")?; + let cafile = cafile.filter(|value| !unsafe { is_none(*value) }); + let capath = capath.filter(|value| !unsafe { is_none(*value) }); + let cadata = cadata.filter(|value| !unsafe { is_none(*value) }); + if cafile.is_none() && capath.is_none() && cadata.is_none() { + return Err(crate::PyError::type_error( + "cafile, capath and cadata cannot be all omitted", + )); + } + if let Some(cafile) = cafile { + let path = path_string(cafile)?; + native_result(unsafe { + pyre_native::ssl::context_load_verify_file(self.backend, &path) + })?; + } + if let Some(capath) = capath { + let path = path_string(capath)?; + if !std::path::Path::new(&path).is_dir() { + return Err(crate::PyError::os_error_with_errno( + libc::ENOENT, + "CA directory does not exist", + )); + } + unsafe { pyre_native::ssl::context_add_verify_dir(self.backend, &path) }; + } + if let Some(cadata) = cadata { + if unsafe { is_str(cadata) } { + let data = crate::baseobjspace::str_utf8_w(cadata)?; + unsafe { + pyre_native::ssl::context_load_verify_data( + self.backend, + data.as_bytes(), + true, + ) + } + .map_err(|_| { + ssl_error("no start line: cadata does not contain a certificate") + })?; + } else { + let buffer = + crate::baseobjspace::simple_buffer_bytes(cadata)?.ok_or_else(|| { + crate::PyError::type_error( + "cadata should be an ASCII string or a bytes-like object", + ) + })?; + let result = unsafe { + pyre_native::ssl::context_load_verify_data( + self.backend, + buffer.as_bytes(), + false, + ) + } + .map_err(|_| { + ssl_error("not enough data: cadata does not contain a certificate") + }); + buffer.release(); + result?; + } + } + Ok(()) + } + + fn set_default_verify_paths(&mut self) -> Result<(), crate::PyError> { + if let Ok(Some(path)) = crate::host_seam::getenv(b"SSL_CERT_FILE") { + let path = String::from_utf8_lossy(&path).into_owned(); + if std::path::Path::new(&path).is_file() { + native_result(unsafe { + pyre_native::ssl::context_load_verify_file(self.backend, &path) + })?; + // OpenSSL defers hashed `SSL_CERT_DIR` loading until chain + // lookup, so it does not contribute to cert_store_stats here. + return Ok(()); + } + } + native_result(unsafe { pyre_native::ssl::context_load_native_roots(self.backend) })?; + Ok(()) + } + + fn cert_store_stats(&self) -> PyObjectRef { + let (x509, ca) = unsafe { pyre_native::ssl::context_cert_store_stats(self.backend) }; + dict_from_pairs(&[ + ("x509", w_int_new(x509 as i64)), + ("crl", w_int_new(0)), + ("x509_ca", w_int_new(ca as i64)), + ]) + } + + fn get_ca_certs( + &self, + #[default(false)] binary_form: bool, + ) -> Result { + let certs = unsafe { pyre_native::ssl::context_ca_certs(self.backend) }; + let mut result = Vec::with_capacity(certs.len()); + for cert in certs { + result.push(if binary_form { + w_bytes_from_bytes(&cert) + } else { + let decoded = native_result(pyre_native::ssl::certificate_decode_der(&cert))?; + decoded_certificate_dict(decoded) + }); + } + Ok(w_list_new(result)) + } + + fn set_ciphers(&mut self, cipherlist: PyObjectRef) -> Result<(), crate::PyError> { + if !unsafe { is_str(cipherlist) } { + return Err(crate::PyError::type_error("cipherlist must be a string")); + } + let cipherlist = crate::baseobjspace::str_utf8_w(cipherlist)?; + unsafe { pyre_native::ssl::context_set_cipher_list(self.backend, cipherlist.as_ref()) } + .map_err(ssl_error) + } + + fn get_ciphers(&self) -> PyObjectRef { + let mut ciphers = Vec::with_capacity(pyre_native::ssl::cipher_count()); + for index in 0..pyre_native::ssl::cipher_count() { + let name = pyre_native::ssl::cipher_name(index); + let bits = pyre_native::ssl::cipher_bits(index); + ciphers.push(dict_from_pairs(&[ + ("id", w_int_new(index as i64)), + ("name", w_str_new(name)), + ( + "protocol", + w_str_new(pyre_native::ssl::cipher_protocol(index)), + ), + ("description", w_str_new(name)), + ("strength_bits", w_int_new(bits as i64)), + ("alg_bits", w_int_new(bits as i64)), + ("aead", w_bool_from(pyre_native::ssl::cipher_aead(index))), + ( + "symmetric", + w_str_new(pyre_native::ssl::cipher_symmetric(index)), + ), + ("digest", w_str_new(pyre_native::ssl::cipher_digest(index))), + ("kea", w_str_new(pyre_native::ssl::cipher_kea(index))), + ("auth", w_str_new(pyre_native::ssl::cipher_auth(index))), + ])); + } + w_list_new(ciphers) + } + + fn session_stats(&self) -> PyObjectRef { + let (accept, hits) = unsafe { pyre_native::ssl::context_session_stats(self.backend) }; + dict_from_pairs(&[ + ("number", w_int_new(0)), + ("connect", w_int_new(0)), + ("connect_good", w_int_new(0)), + ("connect_renegotiate", w_int_new(0)), + ("accept", w_int_new(accept as i64)), + ("accept_good", w_int_new(accept as i64)), + ("accept_renegotiate", w_int_new(0)), + ("hits", w_int_new(hits as i64)), + ("misses", w_int_new(0)), + ("timeouts", w_int_new(0)), + ("cache_full", w_int_new(0)), + ]) + } + + fn load_dh_params(&self, path: PyObjectRef) -> Result<(), crate::PyError> { + if path.is_null() || unsafe { is_none(path) } { + return Err(crate::PyError::type_error( + "load_dh_params() missing required path argument", + )); + } + let path = path_string(path)?; + let data = std::fs::read(&path).map_err(|error| { + crate::PyError::os_error_with_errno( + error.raw_os_error().unwrap_or(libc::EIO), + error.to_string(), + ) + })?; + if !data + .windows(b"BEGIN DH PARAMETERS".len()) + .any(|window| window == b"BEGIN DH PARAMETERS") + { + return Err(ssl_error("[PEM: NO_START_LINE] no start line")); + } + Ok(()) + } + + fn set_ecdh_curve(&self, curve: PyObjectRef) -> Result<(), crate::PyError> { + let curve = if unsafe { is_str(curve) } { + crate::baseobjspace::str_utf8_w(curve)?.to_string() + } else if unsafe { bytesobject::is_bytes_like(curve) } { + String::from_utf8(unsafe { bytesobject::bytes_like_data(curve) }.to_vec()) + .map_err(|_| crate::PyError::value_error("invalid curve name"))? + } else { + return Err(crate::PyError::type_error("curve_name must be a string")); + }; + unsafe { pyre_native::ssl::context_set_ecdh_curve(self.backend, &curve) } + .map_err(crate::PyError::value_error) + } + + #[getter] + fn security_level(&self) -> i64 { + 2 + } + + fn _wrap_bio(&self, args: &[PyObjectRef]) -> Result { + const KEYWORDS: &[&str] = &[ + "incoming", + "outgoing", + "server_side", + "server_hostname", + "owner", + "session", + ]; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let user = positional.get(1..).unwrap_or(&[]); + if user.len() > KEYWORDS.len() { + return Err(crate::PyError::type_error( + "_wrap_bio() takes at most 6 arguments", + )); + } + let incoming = + crate::builtins::bind_pos_or_kw(user, kwargs, 0, "incoming", "_wrap_bio", 1)? + .ok_or_else(|| { + crate::PyError::type_error( + "_wrap_bio() missing required argument 'incoming'", + ) + })?; + let outgoing = + crate::builtins::bind_pos_or_kw(user, kwargs, 1, "outgoing", "_wrap_bio", 2)? + .ok_or_else(|| { + crate::PyError::type_error( + "_wrap_bio() missing required argument 'outgoing'", + ) + })?; + let server_side = + crate::builtins::bind_pos_or_kw(user, kwargs, 2, "server_side", "_wrap_bio", 3)? + .map(crate::baseobjspace::is_true) + .transpose()? + .unwrap_or(false); + let hostname = crate::builtins::bind_pos_or_kw( + user, + kwargs, + 3, + "server_hostname", + "_wrap_bio", + 4, + )?; + let owner = crate::builtins::bind_pos_or_kw(user, kwargs, 4, "owner", "_wrap_bio", 5)? + .unwrap_or_else(w_none); + let session = + crate::builtins::bind_pos_or_kw(user, kwargs, 5, "session", "_wrap_bio", 6)?; + crate::builtins::kwarg_reject_unknown(kwargs, KEYWORDS, "_wrap_bio")?; + if W_MemoryBIO::from_obj(incoming).is_none() + || W_MemoryBIO::from_obj(outgoing).is_none() + { + return Err(crate::PyError::type_error( + "incoming and outgoing must be MemoryBIO objects", + )); + } + if let Some(session) = session.filter(|value| !unsafe { is_none(*value) }) + && W_SSLSession::from_obj(session).is_none() + { + return Err(crate::PyError::type_error("Value is not a SSLSession.")); + } + let (hostname, hostname_obj) = parse_server_hostname(hostname)?; + if !server_side + && unsafe { pyre_native::ssl::context_check_hostname(self.backend) } + && hostname.is_none() + { + return Err(crate::PyError::value_error( + "check_hostname requires server_hostname", + )); + } + let protocol = unsafe { pyre_native::ssl::context_protocol(self.backend) }; + if server_side && protocol == PROTOCOL_TLS_CLIENT { + return Err(ssl_error( + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context", + )); + } + if !server_side && protocol == PROTOCOL_TLS_SERVER { + return Err(ssl_error( + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context", + )); + } + let requested_session = clone_requested_session(session, self.backend)?; + Ok(allocate_ssl_socket( + std::ptr::null_mut(), + self as *const W_SSLContext as PyObjectRef, + w_none(), + w_none(), + w_none(), + incoming, + outgoing, + owner, + hostname_obj, + server_side, + requested_session, + )) + } + + fn _wrap_socket(&self, args: &[PyObjectRef]) -> Result { + const KEYWORDS: &[&str] = + &["sock", "server_side", "server_hostname", "owner", "session"]; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + let user = positional.get(1..).unwrap_or(&[]); + if user.len() > KEYWORDS.len() { + return Err(crate::PyError::type_error( + "_wrap_socket() takes at most 5 arguments", + )); + } + let socket = + crate::builtins::bind_pos_or_kw(user, kwargs, 0, "sock", "_wrap_socket", 1)? + .ok_or_else(|| { + crate::PyError::type_error( + "_wrap_socket() missing required argument 'sock'", + ) + })?; + let server_side = + crate::builtins::bind_pos_or_kw(user, kwargs, 1, "server_side", "_wrap_socket", 2)? + .map(crate::baseobjspace::is_true) + .transpose()? + .unwrap_or(false); + let hostname = crate::builtins::bind_pos_or_kw( + user, + kwargs, + 2, + "server_hostname", + "_wrap_socket", + 3, + )?; + let owner = + crate::builtins::bind_pos_or_kw(user, kwargs, 3, "owner", "_wrap_socket", 4)? + .unwrap_or_else(w_none); + let session = + crate::builtins::bind_pos_or_kw(user, kwargs, 4, "session", "_wrap_socket", 5)?; + crate::builtins::kwarg_reject_unknown(kwargs, KEYWORDS, "_wrap_socket")?; + if let Some(session) = session.filter(|value| !unsafe { is_none(*value) }) + && W_SSLSession::from_obj(session).is_none() + { + return Err(crate::PyError::type_error("Value is not a SSLSession.")); + } + let (hostname, hostname_obj) = parse_server_hostname(hostname)?; + if !server_side + && unsafe { pyre_native::ssl::context_check_hostname(self.backend) } + && hostname.is_none() + { + return Err(crate::PyError::value_error( + "check_hostname requires server_hostname", + )); + } + let protocol = unsafe { pyre_native::ssl::context_protocol(self.backend) }; + if server_side && protocol == PROTOCOL_TLS_CLIENT { + return Err(ssl_error( + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context", + )); + } + if !server_side && protocol == PROTOCOL_TLS_SERVER { + return Err(ssl_error( + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context", + )); + } + let requested_session = clone_requested_session(session, self.backend)?; + let socket_module = import_module("socket")?; + let socket_class = crate::baseobjspace::getattr_str(socket_module, "socket")?; + let socket_send = crate::baseobjspace::getattr_str(socket_class, "send")?; + let socket_recv = crate::baseobjspace::getattr_str(socket_class, "recv")?; + Ok(allocate_ssl_socket( + std::ptr::null_mut(), + self as *const W_SSLContext as PyObjectRef, + socket, + socket_send, + socket_recv, + w_none(), + w_none(), + owner, + hostname_obj, + server_side, + requested_session, + )) + } + + fn _set_alpn_protocols(&mut self, data: PyObjectRef) -> Result<(), crate::PyError> { + let buffer = crate::baseobjspace::simple_buffer_bytes(data)? + .ok_or_else(|| crate::PyError::type_error("a bytes-like object is required"))?; + let protocols = parse_length_prefixed_protocols(buffer.as_bytes())?; + unsafe { pyre_native::ssl::context_set_alpn(self.backend, protocols) }; + buffer.release(); + Ok(()) + } + + fn _set_npn_protocols(&mut self, _data: PyObjectRef) -> Result<(), crate::PyError> { + Err(ssl_error("NPN is not supported by rustls")) + } + } + + fn validate_tls_version(value: i64) -> Result { + let value = i32::try_from(value).map_err(|_| { + crate::PyError::value_error(format!("invalid protocol version: {value}")) + })?; + if value != -2 && value != -1 && !(0x300..=0x304).contains(&value) { + return Err(crate::PyError::value_error(format!( + "invalid protocol version: {value}" + ))); + } + Ok(value) + } + + fn parse_length_prefixed_protocols(data: &[u8]) -> Result>, crate::PyError> { + let mut protocols = Vec::new(); + let mut offset = 0usize; + while offset < data.len() { + let len = data[offset] as usize; + offset += 1; + if len == 0 || offset + len > data.len() { + return Err(ssl_error("invalid ALPN protocol list")); + } + protocols.push(data[offset..offset + len].to_vec()); + offset += len; + } + Ok(protocols) + } +} // context_methods + +mod memory_bio_methods { + use super::*; + + #[crate::pyre_methods(doc = "Memory BIO for TLS protocol data.")] + impl W_MemoryBIO { + #[staticmethod] + fn __new__(cls: PyObjectRef, args: &[PyObjectRef]) -> Result { + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + if positional.len() != 1 || crate::builtins::has_real_kwargs(kwargs) { + return Err(crate::PyError::type_error("MemoryBIO() takes no arguments")); + } + crate::typedef::check_user_subclass(type_object(), cls)?; + let _roots = pyre_object::gc_roots::push_roots(); + pyre_object::gc_roots::pin_root(cls); + let cls_slot = pyre_object::gc_roots::shadow_stack_len() - 1; + let obj = W_MemoryBIO::allocate_stable(W_MemoryBIO { + ob: PyObject { + ob_type: std::ptr::null(), + w_class: std::ptr::null_mut(), + }, + map: std::ptr::null(), + storage: std::ptr::null_mut(), + backend: pyre_native::ssl::memory_bio_new(), + }); + Ok(crate::typedef::tag_subclass_instance(obj, unsafe { + pyre_object::gc_roots::shadow_stack_get(cls_slot) + })) + } + + fn read(&mut self, #[default(-1)] size: i64) -> Result { + let size = if size < 0 { + unsafe { pyre_native::ssl::memory_bio_pending(self.backend) } + } else { + usize::try_from(size) + .map_err(|_| crate::PyError::overflow_error("read size out of range"))? + }; + let bytes = unsafe { pyre_native::ssl::memory_bio_read(self.backend, size) }; + Ok(w_bytes_from_bytes(&bytes)) + } + + fn write(&mut self, data: PyObjectRef) -> Result { + let buffer = crate::baseobjspace::simple_buffer_bytes(data)? + .ok_or_else(|| crate::PyError::type_error("a bytes-like object is required"))?; + let result = + unsafe { pyre_native::ssl::memory_bio_write(self.backend, buffer.as_bytes()) } + .map_err(ssl_error); + buffer.release(); + result + } + + fn write_eof(&mut self) { + unsafe { pyre_native::ssl::memory_bio_write_eof(self.backend) }; + } + + #[getter] + fn pending(&self) -> usize { + unsafe { pyre_native::ssl::memory_bio_pending(self.backend) } + } + + #[getter] + fn eof(&self) -> bool { + unsafe { pyre_native::ssl::memory_bio_eof(self.backend) } + } + } +} // memory_bio_methods + +mod ssl_socket_methods { + use super::*; + + fn call_transport( + callable: PyObjectRef, + socket: PyObjectRef, + args: &[PyObjectRef], + ) -> Result { + let mut call_args = Vec::with_capacity(args.len() + 1); + call_args.push(socket); + call_args.extend_from_slice(args); + crate::call::call_function_impl_result(callable, &call_args) + } + + fn is_blocking_error(error: &crate::PyError) -> bool { + let Some(class) = crate::builtins::lookup_exc_class("BlockingIOError") else { + return false; + }; + !error.exc_object.is_null() + && unsafe { crate::baseobjspace::isinstance_w(error.exc_object, class) } + } + + fn ensure_connection(socket: &mut W_SSLSocket) -> Result<(), crate::PyError> { + if !socket.backend.is_null() { + return Ok(()); + } + let context = + W_SSLContext::from_obj(socket.context).expect("SSL socket owns a live SSLContext"); + let hostname = if unsafe { is_none(socket.server_hostname) } { + None + } else { + Some(crate::baseobjspace::str_utf8_w(socket.server_hostname)?.to_string()) + }; + socket.backend = native_result(unsafe { + pyre_native::ssl::connection_new( + context.backend, + socket.server_side, + hostname.as_deref(), + socket.requested_session, + ) + })?; + Ok(()) + } + + fn transport_socket(socket: &W_SSLSocket) -> Result { + let value = + unsafe { pyre_object::weakref::w_gc_weakref_box_or_strong_deref(socket.socket) }; + if value.is_null() { + Err(ssl_error("underlying socket has been collected")) + } else { + Ok(value) + } + } + + fn receive_tls(socket: &mut W_SSLSocket, data: &[u8]) -> Result { + let read = match unsafe { pyre_native::ssl::connection_receive_tls(socket.backend, data) } { + Ok(read) => read, + Err(error) => { + // rustls may have queued the fatal alert describing this + // protocol error. Send it before unwinding the Python call so + // the peer cannot remain blocked in its handshake. + let _ = flush_transport(socket); + return tls_result(Err(error)); + } + }; + run_message_callbacks(socket)?; + if let Some(root) = + unsafe { pyre_native::ssl::connection_take_verified_root(socket.backend) } + { + let context = + W_SSLContext::from_obj(socket.context).expect("SSL socket owns its context"); + native_result(unsafe { + pyre_native::ssl::context_add_verified_root(context.backend, root) + })?; + } + Ok(read) + } + + fn run_message_callbacks(socket: &mut W_SSLSocket) -> Result<(), crate::PyError> { + let events = unsafe { pyre_native::ssl::connection_take_message_events(socket.backend) }; + if events.is_empty() { + return Ok(()); + } + let context = W_SSLContext::from_obj(socket.context).expect("SSL socket owns its context"); + if unsafe { is_none(context.msg_callback) } { + return Ok(()); + } + let owner = unsafe { pyre_object::weakref::w_gc_weakref_box_or_strong_deref(socket.owner) }; + if owner.is_null() { + return Ok(()); + } + for event in events { + crate::call::call_function_impl_result( + context.msg_callback, + &[ + owner, + w_str_new(if event.write { "write" } else { "read" }), + w_int_new(event.version as i64), + w_int_new(event.content_type as i64), + w_int_new(event.message_type as i64), + w_bytes_from_bytes(&event.data), + ], + )?; + } + Ok(()) + } + + fn flush_transport(socket: &mut W_SSLSocket) -> Result<(), crate::PyError> { + ensure_connection(socket)?; + if !unsafe { is_none(socket.outgoing) } { + let data = + tls_result(unsafe { pyre_native::ssl::connection_take_tls(socket.backend) })?; + run_message_callbacks(socket)?; + if !data.is_empty() { + let outgoing = W_MemoryBIO::from_obj(socket.outgoing) + .expect("_wrap_bio validated its outgoing MemoryBIO"); + unsafe { pyre_native::ssl::memory_bio_write(outgoing.backend, &data) } + .map_err(ssl_error)?; + } + return Ok(()); + } + + loop { + let data = + tls_result(unsafe { pyre_native::ssl::connection_peek_tls(socket.backend) })?; + run_message_callbacks(socket)?; + if data.is_empty() { + return Ok(()); + } + let sent = match call_transport( + socket.socket_send, + transport_socket(socket)?, + &[w_bytes_from_bytes(&data)], + ) { + Ok(value) => crate::baseobjspace::int_w(value)?, + Err(error) if is_blocking_error(&error) => { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_WRITE, + "The operation did not complete (write)".to_string(), + )); + } + Err(error) => return Err(error), + }; + if sent <= 0 { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_WRITE, + "The operation did not complete (write)".to_string(), + )); + } + unsafe { pyre_native::ssl::connection_consume_tls(socket.backend, sent as usize) }; + } + } + + fn receive_transport(socket: &mut W_SSLSocket) -> Result<(), crate::PyError> { + ensure_connection(socket)?; + if !unsafe { is_none(socket.incoming) } { + let incoming = W_MemoryBIO::from_obj(socket.incoming) + .expect("_wrap_bio validated its incoming MemoryBIO"); + let pending = unsafe { pyre_native::ssl::memory_bio_pending(incoming.backend) }; + if pending == 0 { + if unsafe { pyre_native::ssl::memory_bio_eof(incoming.backend) } { + let _ = receive_tls(socket, &[]); + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_EOF, + "EOF occurred in violation of protocol".to_string(), + )); + } + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_READ, + "The operation did not complete (read)".to_string(), + )); + } + let data = unsafe { pyre_native::ssl::memory_bio_read(incoming.backend, pending) }; + receive_tls(socket, &data)?; + return Ok(()); + } + + let value = match call_transport( + socket.socket_recv, + transport_socket(socket)?, + &[w_int_new(32 * 1024)], + ) { + Ok(value) => value, + Err(error) if is_blocking_error(&error) => { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_READ, + "The operation did not complete (read)".to_string(), + )); + } + Err(error) => return Err(error), + }; + let buffer = crate::baseobjspace::simple_buffer_bytes(value)?.ok_or_else(|| { + crate::PyError::type_error("socket recv() returned a non-bytes value") + })?; + if buffer.as_bytes().is_empty() { + buffer.release(); + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_EOF, + "EOF occurred in violation of protocol".to_string(), + )); + } + let result = receive_tls(socket, buffer.as_bytes()); + buffer.release(); + result.map(|_| ()) + } + + fn reject_sni(socket: &mut W_SSLSocket, alert: u8, reason: &str) -> Result<(), crate::PyError> { + tls_result(unsafe { pyre_native::ssl::connection_reject_server(socket.backend, alert) })?; + // The peer must receive the fatal alert before the server-side + // operation reports its callback failure. + flush_transport(socket)?; + Err(tls_error( + pyre_native::ssl::TLS_ERROR_SSL, + format!("[SSL: {reason}] SNI callback failed"), + )) + } + + /// Finish a server configuration after rustls has parsed ClientHello. + /// + /// This must operate through a raw pointer: the Python SNI callback may + /// re-enter `_SSLSocket.context` and mutate this exact object. Keeping a + /// Rust `&mut W_SSLSocket` live across that call would violate its unique + /// alias and lets optimized builds reuse the pre-callback context. PyPy's + /// `servername_callback` likewise reloads the SSL object's context after + /// the app-level callback returns. + #[inline(never)] + unsafe fn configure_accepted_server(socket: *mut W_SSLSocket) -> Result<(), crate::PyError> { + let backend = unsafe { (*socket).backend }; + if !unsafe { (*socket).server_side } + || !unsafe { pyre_native::ssl::connection_waiting_for_server_config(backend) } + { + return Ok(()); + } + + let initial_context = unsafe { (*socket).context }; + let context = W_SSLContext::from_obj(initial_context) + .expect("SSL socket owns a live initial context"); + let callback = context.sni_callback; + if !unsafe { is_none(callback) } { + let owner = + unsafe { pyre_object::weakref::w_gc_weakref_box_or_strong_deref((*socket).owner) }; + if owner.is_null() { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_SSL, + "[SSL: PARSE_TLSEXT] SNI callback owner is no longer available".to_string(), + )); + } + let server_name = unsafe { + pyre_native::ssl::connection_server_name(backend) + .map(|name| w_str_new(&name)) + .unwrap_or_else(w_none) + }; + let result = match crate::call::call_function_impl_result( + callback, + &[owner, server_name, initial_context], + ) { + Ok(result) => result, + Err(mut error) => { + error.write_unraisable( + w_none(), + rustpython_wtf8::Wtf8::new("in SNI callback"), + callback, + ); + return reject_sni(unsafe { &mut *socket }, 40, "CALLBACK_FAILED"); + } + }; + if !unsafe { is_none(result) } { + let alert = match crate::baseobjspace::int_w(result) { + Ok(alert) if (0..=u8::MAX as i64).contains(&alert) => alert as u8, + _ => { + let mut error = crate::PyError::type_error( + "SNI callback must return None or a TLS alert integer", + ); + error.write_unraisable( + w_none(), + rustpython_wtf8::Wtf8::new("in SNI callback"), + callback, + ); + return reject_sni(unsafe { &mut *socket }, 80, "CALLBACK_FAILED"); + } + }; + return reject_sni(unsafe { &mut *socket }, alert, "CALLBACK_FAILED"); + } + } + + // The callback is allowed to assign ssl_sock.context. Reload through + // the raw object pointer after re-entry; `initial_context` is only the + // third callback argument, never the selected configuration anchor. + let selected = W_SSLContext::from_obj(unsafe { (*socket).context }) + .expect("SNI callback preserves an SSLContext on the socket"); + match unsafe { pyre_native::ssl::connection_accept_server(backend, selected.backend) } { + Ok(()) => Ok(()), + Err(error) => { + // Accepted::into_connection returns the alert bytes alongside + // the error. Deliver those bytes before reporting failure. + let _ = flush_transport(unsafe { &mut *socket }); + tls_result(Err(error)) + } + } + } + + #[crate::pyre_methods] + impl W_SSLSocket { + #[staticmethod] + fn __new__(_cls: PyObjectRef) -> Result { + Err(crate::PyError::type_error( + "cannot create '_ssl._SSLSocket' instances", + )) + } + + #[getter] + fn owner(&self) -> PyObjectRef { + let owner = + unsafe { pyre_object::weakref::w_gc_weakref_box_or_strong_deref(self.owner) }; + if owner.is_null() { w_none() } else { owner } + } + + #[getter] + fn server_side(&self) -> bool { + self.server_side + } + + #[getter] + fn server_hostname(&self) -> PyObjectRef { + self.server_hostname + } + + #[getter] + fn context(&self) -> PyObjectRef { + self.context + } + + #[setter] + fn set_context(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + if W_SSLContext::from_obj(value).is_none() { + return Err(crate::PyError::type_error("context must be an SSLContext")); + } + pyre_object::gc_hook::try_gc_write_barrier(self as *mut W_SSLSocket as *mut u8); + self.context = value; + Ok(()) + } + + #[getter] + fn session_reused(&self) -> PyObjectRef { + if self.backend.is_null() { + return w_none(); + } + unsafe { pyre_native::ssl::connection_session_reused(self.backend) } + .map(w_bool_from) + .unwrap_or_else(w_none) + } + + #[getter] + fn session(&self) -> PyObjectRef { + if self.server_side || self.backend.is_null() { + return w_none(); + } + let backend = unsafe { pyre_native::ssl::connection_session(self.backend) }; + if backend.is_null() { + w_none() + } else { + allocate_ssl_session(backend) + } + } + + #[setter] + fn set_session(&mut self, value: PyObjectRef) -> Result<(), crate::PyError> { + if !unsafe { is_none(value) } && W_SSLSession::from_obj(value).is_none() { + return Err(crate::PyError::type_error("Value is not a SSLSession.")); + } + if !self.backend.is_null() + && !unsafe { pyre_native::ssl::connection_is_handshaking(self.backend) } + { + return Err(crate::PyError::value_error( + "Cannot set session after handshake.", + )); + } + let replacement = clone_requested_session(Some(value), { + let context = + W_SSLContext::from_obj(self.context).expect("SSL socket owns its context"); + context.backend + })?; + unsafe { pyre_native::ssl::session_free(self.requested_session) }; + self.requested_session = replacement; + Ok(()) + } + + fn do_handshake(&mut self) -> Result<(), crate::PyError> { + ensure_connection(self)?; + if self.server_side { + let context = + W_SSLContext::from_obj(self.context).expect("SSL socket owns its context"); + let owner = + unsafe { pyre_object::weakref::w_gc_weakref_box_or_strong_deref(self.owner) }; + if !unsafe { is_none(context.sni_callback) } && owner.is_null() { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_SSL, + "[SSL: PARSE_TLSEXT] SNI callback owner is no longer available".to_string(), + )); + } + } + loop { + unsafe { configure_accepted_server(self as *mut W_SSLSocket) }?; + flush_transport(self)?; + if !unsafe { pyre_native::ssl::connection_is_handshaking(self.backend) } { + return Ok(()); + } + receive_transport(self)?; + unsafe { configure_accepted_server(self as *mut W_SSLSocket) }?; + if !unsafe { is_none(self.incoming) } { + flush_transport(self)?; + if unsafe { pyre_native::ssl::connection_is_handshaking(self.backend) } { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_READ, + "The operation did not complete (read)".to_string(), + )); + } + return Ok(()); + } + } + } + + fn write(&mut self, data: PyObjectRef) -> Result { + if self.shutdown_started { + return Err(ssl_error("TLS/SSL connection has been closed")); + } + ensure_connection(self)?; + let buffer = crate::baseobjspace::simple_buffer_bytes(data)? + .ok_or_else(|| crate::PyError::type_error("a bytes-like object is required"))?; + let result = tls_result(unsafe { + pyre_native::ssl::connection_write_plain(self.backend, buffer.as_bytes()) + }); + buffer.release(); + let written = result?; + flush_transport(self)?; + Ok(written) + } + + fn read(&mut self, args: &[PyObjectRef]) -> Result { + ensure_connection(self)?; + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + if crate::builtins::has_real_kwargs(kwargs) || positional.len() > 3 { + return Err(crate::PyError::type_error( + "read() takes at most 2 arguments", + )); + } + let requested = positional + .get(1) + .copied() + .map(crate::baseobjspace::int_w) + .transpose()? + .unwrap_or(1024); + let output_buffer = positional.get(2).copied(); + if requested < 0 && output_buffer.is_none() { + return Err(crate::PyError::value_error("size should not be negative")); + } + if requested == 0 && output_buffer.is_none() { + return Ok(w_bytes_from_bytes(&[])); + } + let mut writable = if let Some(buffer) = output_buffer { + Some( + unsafe { crate::builtins::WritableBuffer::acquire(buffer) }.map_err(|_| { + crate::PyError::type_error("read() argument 2 must be a writable buffer") + })?, + ) + } else { + None + }; + let size = if let Some(buffer) = writable.as_mut() { + let capacity = unsafe { buffer.as_mut_slice() }.len(); + if requested < 0 { + capacity + } else { + capacity.min(requested as usize) + } + } else { + requested as usize + }; + let data = loop { + match unsafe { pyre_native::ssl::connection_read_plain(self.backend, size) } { + Ok(data) => break data, + Err((code, _)) if code == pyre_native::ssl::TLS_ERROR_WANT_READ => { + receive_transport(self)?; + flush_transport(self)?; + } + Err((code, message)) => return Err(tls_error(code, message)), + } + }; + if let Some(buffer) = writable.as_mut() { + let target = unsafe { buffer.as_mut_slice() }; + target[..data.len()].copy_from_slice(&data); + Ok(w_int_new(data.len() as i64)) + } else { + Ok(w_bytes_from_bytes(&data)) + } + } + + fn pending(&mut self) -> usize { + if self.backend.is_null() { + 0 + } else { + unsafe { pyre_native::ssl::connection_pending_plaintext(self.backend) } + } + } + + fn selected_alpn_protocol(&self) -> PyObjectRef { + if self.backend.is_null() { + return w_none(); + } + unsafe { pyre_native::ssl::connection_alpn(self.backend) } + .map(|value| w_str_new(&String::from_utf8_lossy(&value))) + .unwrap_or_else(w_none) + } + + fn version(&self) -> PyObjectRef { + if self.backend.is_null() { + return w_none(); + } + unsafe { pyre_native::ssl::connection_version(self.backend) } + .map(w_str_new) + .unwrap_or_else(w_none) + } + + fn compression(&self) -> PyObjectRef { + w_none() + } + + fn shared_ciphers(&self) -> PyObjectRef { + if !self.server_side || self.backend.is_null() { + return w_none(); + } + let Some((name, bits)) = (unsafe { pyre_native::ssl::connection_cipher(self.backend) }) + else { + return w_none(); + }; + let version = + unsafe { pyre_native::ssl::connection_version(self.backend) }.unwrap_or("unknown"); + w_list_new(vec![w_tuple_new(vec![ + w_str_new(&name), + w_str_new(version), + w_int_new(bits as i64), + ])]) + } + + fn cipher(&self) -> PyObjectRef { + if self.backend.is_null() { + return w_none(); + } + let Some(version) = (unsafe { pyre_native::ssl::connection_version(self.backend) }) + else { + return w_none(); + }; + let Some((name, bits)) = (unsafe { pyre_native::ssl::connection_cipher(self.backend) }) + else { + return w_none(); + }; + w_tuple_new(vec![ + w_str_new(&name), + w_str_new(version), + w_int_new(bits as i64), + ]) + } + + fn getpeercert( + &self, + #[default(false)] binary_form: bool, + ) -> Result { + if self.backend.is_null() + || unsafe { pyre_native::ssl::connection_is_handshaking(self.backend) } + { + return Err(crate::PyError::value_error("handshake not done yet")); + } + let Some(der) = + (unsafe { pyre_native::ssl::connection_peer_certificate(self.backend) }) + else { + return Ok(w_none()); + }; + if binary_form { + return Ok(w_bytes_from_bytes(&der)); + } + let context = + W_SSLContext::from_obj(self.context).expect("SSL socket owns its context"); + if unsafe { pyre_native::ssl::context_verify_mode(context.backend) } == CERT_NONE { + return Ok(w_dict_new()); + } + let decoded = native_result(pyre_native::ssl::certificate_decode_der(&der))?; + Ok(decoded_certificate_dict(decoded)) + } + + fn get_channel_binding( + &self, + #[default("tls-unique")] kind: &str, + ) -> Result { + if kind != "tls-unique" { + return Err(crate::PyError::value_error( + "'channel_binding_type' must be 'tls-unique'", + )); + } + // CPython permits querying a lazily-created SSLObject before its + // first handshake step. There is no TLS channel to bind yet. + if self.backend.is_null() { + return Ok(w_none()); + } + Ok( + unsafe { pyre_native::ssl::connection_tls_unique(self.backend) } + .map(|binding| w_bytes_from_bytes(&binding)) + .unwrap_or_else(w_none), + ) + } + + fn shutdown(&mut self) -> Result { + ensure_connection(self)?; + if !self.shutdown_started { + unsafe { pyre_native::ssl::connection_send_close_notify(self.backend) }; + self.shutdown_started = true; + } + flush_transport(self)?; + if !unsafe { is_none(self.incoming) } { + receive_transport(self)?; + if !unsafe { pyre_native::ssl::connection_peer_closed(self.backend) } { + return Err(tls_error( + pyre_native::ssl::TLS_ERROR_WANT_READ, + "The operation did not complete (read)".to_string(), + )); + } + } + if unsafe { is_none(self.socket) } { + Ok(w_none()) + } else { + transport_socket(self) + } + } + } +} // ssl_socket_methods + +mod ssl_session_methods { + use super::*; + + #[crate::pyre_methods] + impl W_SSLSession { + #[staticmethod] + fn __new__(_cls: PyObjectRef) -> Result { + Err(crate::PyError::type_error( + "cannot create '_ssl.SSLSession' instances", + )) + } + + #[getter] + fn id(&self) -> PyObjectRef { + w_bytes_from_bytes(&unsafe { pyre_native::ssl::session_id(self.backend) }) + } + + #[getter] + fn time(&self) -> i64 { + unsafe { pyre_native::ssl::session_creation_time(self.backend) as i64 } + } + + #[getter] + fn timeout(&self) -> i64 { + unsafe { pyre_native::ssl::session_timeout(self.backend) as i64 } + } + + #[getter] + fn ticket_lifetime_hint(&self) -> i64 { + unsafe { pyre_native::ssl::session_timeout(self.backend) as i64 } + } + + #[getter] + fn has_ticket(&self) -> bool { + true + } + + fn __eq__(&self, other: PyObjectRef) -> PyObjectRef { + let Some(other) = W_SSLSession::from_obj(other) else { + return pyre_object::w_not_implemented(); + }; + w_bool_from( + unsafe { pyre_native::ssl::session_id(self.backend) } + == unsafe { pyre_native::ssl::session_id(other.backend) }, + ) + } + + fn __repr__(&self) -> PyObjectRef { + w_str_new("<_ssl.SSLSession>") + } + } +} // ssl_session_methods + +mod certificate_methods { + use super::*; + + #[crate::pyre_methods] + impl W_Certificate { + #[staticmethod] + fn __new__(_cls: PyObjectRef) -> Result { + Err(crate::PyError::type_error( + "cannot create '_ssl.Certificate' instances", + )) + } + + fn public_bytes(&self, #[default(2)] encoding: i64) -> Result { + if encoding == 2 { + return Ok(self.der); + } + Err(crate::PyError::value_error( + "unsupported certificate encoding", + )) + } + } +} // certificate_methods + +/// Sweep-time cleanup for opaque native TLS state. +/// +/// # Safety +/// `obj` must be a GC-dead instance of the named class. +pub unsafe fn w_ssl_context_dealloc(obj: PyObjectRef) { + if let Some(context) = W_SSLContext::from_obj(obj) { + unsafe { pyre_native::ssl::context_free(context.backend) }; + context.backend = std::ptr::null_mut(); + } +} + +/// # Safety +/// `obj` must be a GC-dead `W_MemoryBIO`. +pub unsafe fn w_memory_bio_dealloc(obj: PyObjectRef) { + if let Some(bio) = W_MemoryBIO::from_obj(obj) { + unsafe { pyre_native::ssl::memory_bio_free(bio.backend) }; + bio.backend = std::ptr::null_mut(); + } +} + +/// # Safety +/// `obj` must be a GC-dead `W_SSLSession`. +pub unsafe fn w_ssl_session_dealloc(obj: PyObjectRef) { + if let Some(session) = W_SSLSession::from_obj(obj) { + unsafe { pyre_native::ssl::session_free(session.backend) }; + session.backend = std::ptr::null_mut(); + } +} + +/// # Safety +/// `obj` must be a GC-dead `W_SSLSocket`. +pub unsafe fn w_ssl_socket_dealloc(obj: PyObjectRef) { + if let Some(socket) = W_SSLSocket::from_obj(obj) { + unsafe { pyre_native::ssl::connection_free(socket.backend) }; + unsafe { pyre_native::ssl::session_free(socket.requested_session) }; + socket.backend = std::ptr::null_mut(); + socket.requested_session = std::ptr::null_mut(); + } +} + +fn rand_status(_args: &[PyObjectRef]) -> Result { + Ok(w_bool_from(true)) +} + +fn rand_add(_args: &[PyObjectRef]) -> Result { + Ok(w_none()) +} + +fn rand_bytes(args: &[PyObjectRef]) -> Result { + let size = crate::baseobjspace::int_w(args[0])?; + if size < 0 { + return Err(crate::PyError::value_error("num must be positive")); + } + let size = usize::try_from(size) + .map_err(|_| crate::PyError::overflow_error("RAND_bytes size out of range"))?; + let bytes = crate::importing::host::os::urandom(size) + .map_err(|err| ssl_error(format!("RAND_bytes failed: {err}")))?; + Ok(w_bytes_from_bytes(&bytes)) +} + +#[derive(Clone, Copy)] +struct OidEntry { + nid: i32, + short_name: &'static str, + long_name: &'static str, + oid: &'static str, +} + +const OIDS: &[OidEntry] = &[ + OidEntry { + nid: 13, + short_name: "CN", + long_name: "commonName", + oid: "2.5.4.3", + }, + OidEntry { + nid: 14, + short_name: "C", + long_name: "countryName", + oid: "2.5.4.6", + }, + OidEntry { + nid: 15, + short_name: "L", + long_name: "localityName", + oid: "2.5.4.7", + }, + OidEntry { + nid: 16, + short_name: "ST", + long_name: "stateOrProvinceName", + oid: "2.5.4.8", + }, + OidEntry { + nid: 17, + short_name: "O", + long_name: "organizationName", + oid: "2.5.4.10", + }, + OidEntry { + nid: 18, + short_name: "OU", + long_name: "organizationalUnitName", + oid: "2.5.4.11", + }, + OidEntry { + nid: 129, + short_name: "serverAuth", + long_name: "TLS Web Server Authentication", + oid: "1.3.6.1.5.5.7.3.1", + }, + OidEntry { + nid: 130, + short_name: "clientAuth", + long_name: "TLS Web Client Authentication", + oid: "1.3.6.1.5.5.7.3.2", + }, +]; + +fn oid_tuple(entry: OidEntry) -> PyObjectRef { + w_tuple_new(vec![ + w_int_new(entry.nid as i64), + w_str_new(entry.short_name), + w_str_new(entry.long_name), + w_str_new(entry.oid), + ]) +} + +fn txt2obj(args: &[PyObjectRef]) -> Result { + let (positional, kwargs) = crate::builtins::split_builtin_kwargs(args); + if positional.is_empty() || positional.len() > 2 { + return Err(crate::PyError::type_error( + "txt2obj() takes 1 or 2 arguments", + )); + } + let value = crate::baseobjspace::str_utf8_w(positional[0])?; + let name_arg = crate::builtins::bind_pos_or_kw(positional, kwargs, 1, "name", "txt2obj", 2)?; + crate::builtins::kwarg_reject_unknown(kwargs, &["name"], "txt2obj")?; + let allow_names = name_arg + .map(crate::baseobjspace::is_true) + .transpose()? + .unwrap_or(false); + let name = value.as_ref(); + let entry = OIDS.iter().copied().find(|entry| { + entry.oid == name + || (allow_names + && (entry.short_name == name || entry.long_name.eq_ignore_ascii_case(name))) + }); + entry + .map(oid_tuple) + .ok_or_else(|| crate::PyError::value_error(format!("unknown object '{name}'"))) +} + +fn nid2obj(args: &[PyObjectRef]) -> Result { + let nid = crate::baseobjspace::int_w(args[0])?; + OIDS.iter() + .copied() + .find(|entry| entry.nid as i64 == nid) + .map(oid_tuple) + .ok_or_else(|| crate::PyError::value_error(format!("unknown NID {nid}"))) +} + +fn get_default_verify_paths(_args: &[PyObjectRef]) -> Result { + Ok(w_tuple_new(vec![ + w_str_new("SSL_CERT_FILE"), + w_str_new("/etc/ssl/cert.pem"), + w_str_new("SSL_CERT_DIR"), + w_str_new("/etc/ssl/certs"), + ])) +} + +fn test_decode_cert(args: &[PyObjectRef]) -> Result { + let path = path_string(args[0])?; + let cert = native_result(pyre_native::ssl::certificate_decode_file(&path))?; + Ok(decoded_certificate_dict(cert)) +} + +crate::py_module! { + "_ssl", + interpleveldefs: { + "_SSLContext" => context_methods::type_object(), + "MemoryBIO" => memory_bio_methods::type_object(), + "SSLSession" => ssl_session_methods::type_object(), + "_SSLSocket" => ssl_socket_methods::type_object(), + "Certificate" => certificate_methods::type_object(), + "SSLError" => crate::builtins::make_exc_type( + "_ssl.SSLError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("OSError").expect("OSError installed"), + ), + "SSLZeroReturnError" => crate::builtins::make_exc_type( + "_ssl.SSLZeroReturnError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), + ), + "SSLWantReadError" => crate::builtins::make_exc_type( + "_ssl.SSLWantReadError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), + ), + "SSLWantWriteError" => crate::builtins::make_exc_type( + "_ssl.SSLWantWriteError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), + ), + "SSLSyscallError" => crate::builtins::make_exc_type( + "_ssl.SSLSyscallError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), + ), + "SSLEOFError" => crate::builtins::make_exc_type( + "_ssl.SSLEOFError", + crate::builtins::exc_os_error_new, + crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), + ), + "OPENSSL_VERSION" => w_str_new("AWS-LC (rustls/0.23)"), + "OPENSSL_VERSION_NUMBER" => w_int_new(0x3000_0000), + "OPENSSL_VERSION_INFO" => w_tuple_new(vec![w_int_new(3), w_int_new(0), w_int_new(0), w_int_new(0), w_int_new(15)]), + "_OPENSSL_API_VERSION" => w_tuple_new(vec![w_int_new(3), w_int_new(0), w_int_new(0), w_int_new(0), w_int_new(15)]), + "_DEFAULT_CIPHERS" => w_str_new("rustls default cipher suites"), + "HAS_SNI" => w_bool_from(true), + "HAS_ECDH" => w_bool_from(true), + "HAS_NPN" => w_bool_from(false), + "HAS_ALPN" => w_bool_from(true), + "HAS_SSLv2" => w_bool_from(false), + "HAS_SSLv3" => w_bool_from(false), + "HAS_TLSv1" => w_bool_from(false), + "HAS_TLSv1_1" => w_bool_from(false), + "HAS_TLSv1_2" => w_bool_from(true), + "HAS_TLSv1_3" => w_bool_from(true), + "HAS_PSK" => w_bool_from(false), + "HAS_PHA" => w_bool_from(false) + }, + int_constants: { + "PROTOCOL_SSLv23" => PROTOCOL_TLS, + "PROTOCOL_TLS" => PROTOCOL_TLS, + "PROTOCOL_TLS_CLIENT" => PROTOCOL_TLS_CLIENT, + "PROTOCOL_TLS_SERVER" => PROTOCOL_TLS_SERVER, + "PROTOCOL_TLSv1" => 3, + "PROTOCOL_TLSv1_1" => 4, + "PROTOCOL_TLSv1_2" => 5, + "PROTOCOL_TLSv1_3" => 6, + "PROTO_MINIMUM_SUPPORTED" => -2, + "PROTO_MAXIMUM_SUPPORTED" => -1, + "PROTO_SSLv3" => 0x300, + "PROTO_TLSv1" => 0x301, + "PROTO_TLSv1_1" => 0x302, + "PROTO_TLSv1_2" => 0x303, + "PROTO_TLSv1_3" => 0x304, + "CERT_NONE" => CERT_NONE, + "CERT_OPTIONAL" => CERT_OPTIONAL, + "CERT_REQUIRED" => CERT_REQUIRED, + "VERIFY_DEFAULT" => 0, + "VERIFY_CRL_CHECK_LEAF" => 4, + "VERIFY_CRL_CHECK_CHAIN" => 12, + "VERIFY_X509_STRICT" => 32, + "VERIFY_ALLOW_PROXY_CERTS" => 64, + "VERIFY_X509_TRUSTED_FIRST" => 32768, + "VERIFY_X509_PARTIAL_CHAIN" => 0x80000, + "OP_ALL" => 0x00000bfb, + "OP_NO_SSLv2" => 0, + "OP_NO_SSLv3" => 0x02000000, + "OP_NO_TLSv1" => 0x04000000, + "OP_NO_TLSv1_1" => 0x10000000, + "OP_NO_TLSv1_2" => 0x08000000, + "OP_NO_TLSv1_3" => 0x20000000, + "OP_NO_COMPRESSION" => 0x00020000, + "OP_CIPHER_SERVER_PREFERENCE" => 0x00400000, + "OP_SINGLE_DH_USE" => 0, + "OP_SINGLE_ECDH_USE" => 0, + "OP_NO_TICKET" => 0x00004000, + "OP_LEGACY_SERVER_CONNECT" => 4, + "OP_NO_RENEGOTIATION" => 0x40000000, + "OP_IGNORE_UNEXPECTED_EOF" => 0x80, + "OP_ENABLE_MIDDLEBOX_COMPAT" => 0x00100000, + "SSL_ERROR_NONE" => 0, + "SSL_ERROR_SSL" => 1, + "SSL_ERROR_WANT_READ" => 2, + "SSL_ERROR_WANT_WRITE" => 3, + "SSL_ERROR_WANT_X509_LOOKUP" => 4, + "SSL_ERROR_SYSCALL" => 5, + "SSL_ERROR_ZERO_RETURN" => 6, + "SSL_ERROR_WANT_CONNECT" => 7, + "SSL_ERROR_EOF" => 8, + "SSL_ERROR_INVALID_ERROR_CODE" => 10, + "HOSTFLAG_NEVER_CHECK_SUBJECT" => 0x20, + "ENCODING_PEM" => 1, + "ENCODING_DER" => 2, + "ENCODING_PEM_AUX" => 0x101 + }, + functions: { + "RAND_status" / 0 = rand_status, + "RAND_add" / * = rand_add, + "RAND_bytes" / 1 = rand_bytes, + "txt2obj" / * = txt2obj, + "nid2obj" / 1 = nid2obj, + "get_default_verify_paths" / 0 = get_default_verify_paths, + "_test_decode_cert" / 1 = test_decode_cert + }, + extra_init: |ns| { + let ssl_error = crate::builtins::lookup_exc_class("_ssl.SSLError") + .expect("SSLError installed"); + let ssl_error_dict = + unsafe { pyre_object::w_type_get_dict_ptr(ssl_error) as PyObjectRef }; + unsafe { + pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( + ssl_error_dict, + "__str__", + crate::make_builtin_function_with_arity("__str__", ssl_error_str, 1), + ); + } + let value_error = crate::builtins::lookup_exc_class("ValueError") + .expect("ValueError installed"); + let cert_error = crate::builtins::make_exc_type_multi( + "_ssl.SSLCertVerificationError", + crate::builtins::exc_exception_new, + &[ssl_error, value_error], + ); + crate::module_ns_store(ns, "SSLCertVerificationError", cert_error); + for (name, value) in [ + ("ALERT_DESCRIPTION_CLOSE_NOTIFY", 0), + ("ALERT_DESCRIPTION_UNEXPECTED_MESSAGE", 10), + ("ALERT_DESCRIPTION_BAD_RECORD_MAC", 20), + ("ALERT_DESCRIPTION_DECRYPTION_FAILED", 21), + ("ALERT_DESCRIPTION_RECORD_OVERFLOW", 22), + ("ALERT_DESCRIPTION_DECOMPRESSION_FAILURE", 30), + ("ALERT_DESCRIPTION_HANDSHAKE_FAILURE", 40), + ("ALERT_DESCRIPTION_NO_CERTIFICATE", 41), + ("ALERT_DESCRIPTION_BAD_CERTIFICATE", 42), + ("ALERT_DESCRIPTION_UNSUPPORTED_CERTIFICATE", 43), + ("ALERT_DESCRIPTION_CERTIFICATE_REVOKED", 44), + ("ALERT_DESCRIPTION_CERTIFICATE_EXPIRED", 45), + ("ALERT_DESCRIPTION_CERTIFICATE_UNKNOWN", 46), + ("ALERT_DESCRIPTION_ILLEGAL_PARAMETER", 47), + ("ALERT_DESCRIPTION_UNKNOWN_CA", 48), + ("ALERT_DESCRIPTION_ACCESS_DENIED", 49), + ("ALERT_DESCRIPTION_DECODE_ERROR", 50), + ("ALERT_DESCRIPTION_DECRYPT_ERROR", 51), + ("ALERT_DESCRIPTION_EXPORT_RESTRICTION", 60), + ("ALERT_DESCRIPTION_PROTOCOL_VERSION", 70), + ("ALERT_DESCRIPTION_INSUFFICIENT_SECURITY", 71), + ("ALERT_DESCRIPTION_INTERNAL_ERROR", 80), + ("ALERT_DESCRIPTION_INAPPROPRIATE_FALLBACK", 86), + ("ALERT_DESCRIPTION_USER_CANCELLED", 90), + ("ALERT_DESCRIPTION_NO_RENEGOTIATION", 100), + ("ALERT_DESCRIPTION_MISSING_EXTENSION", 109), + ("ALERT_DESCRIPTION_UNSUPPORTED_EXTENSION", 110), + ("ALERT_DESCRIPTION_CERTIFICATE_UNOBTAINABLE", 111), + ("ALERT_DESCRIPTION_UNRECOGNIZED_NAME", 112), + ("ALERT_DESCRIPTION_BAD_CERTIFICATE_STATUS_RESPONSE", 113), + ("ALERT_DESCRIPTION_BAD_CERTIFICATE_HASH_VALUE", 114), + ("ALERT_DESCRIPTION_UNKNOWN_PSK_IDENTITY", 115), + ("ALERT_DESCRIPTION_CERTIFICATE_REQUIRED", 116), + ("ALERT_DESCRIPTION_NO_APPLICATION_PROTOCOL", 120), + ] { + crate::module_ns_store(ns, name, w_int_new(value)); + } + }, +} diff --git a/pyre/pyre-interpreter/src/module/mod.rs b/pyre/pyre-interpreter/src/module/mod.rs index ba7e720146a..03d66651958 100644 --- a/pyre/pyre-interpreter/src/module/mod.rs +++ b/pyre/pyre-interpreter/src/module/mod.rs @@ -58,6 +58,9 @@ pub mod _random; pub mod _socket; pub mod _sre; #[allow(non_snake_case)] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] +pub mod _ssl; +#[allow(non_snake_case)] pub mod _symtable; #[allow(non_snake_case)] pub mod _template; diff --git a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs index 74341de69ee..c97beabc763 100644 --- a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs +++ b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs @@ -556,6 +556,27 @@ unsafe fn is_generated_user_layout_family(obj: PyObjectRef) -> bool { } } +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] +#[inline] +unsafe fn is_ssl_mapdict_layout(obj: PyObjectRef) -> bool { + use pyre_object::lltype::PyreClassPyTypeOf; + unsafe { + pyre_object::py_type_check( + obj, + &*::PYTYPE, + ) || pyre_object::py_type_check( + obj, + &*::PYTYPE, + ) + } +} + +#[cfg(not(all(not(target_arch = "wasm32"), not(feature = "sandbox"))))] +#[inline] +unsafe fn is_ssl_mapdict_layout(_obj: PyObjectRef) -> bool { + false +} + /// Whether `obj`'s physical allocation carries the slots supplied by /// `MapdictStorageMixin` (`mapdict.py:748-761, 905-910`). Ordinary instances /// and `_random.Random` keep the historical prefix. The generated tuple/int/str @@ -575,6 +596,7 @@ pub unsafe fn has_mapdict_layout(obj: PyObjectRef) -> bool { } if (unsafe { pyre_object::is_instance(obj) }) || unsafe { pyre_object::py_type_check(obj, &crate::module::_random::RANDOM_TYPE) } + || unsafe { is_ssl_mapdict_layout(obj) } { return true; } diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index e6f3e6fc7ba..6d685d6d61f 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -690,6 +690,34 @@ unsafe fn hashlib_hmac_destructor(obj_addr: usize) { } } +#[cfg(not(target_arch = "wasm32"))] +unsafe fn ssl_context_destructor(obj_addr: usize) { + unsafe { + pyre_interpreter::module::_ssl::w_ssl_context_dealloc(obj_addr as pyre_object::PyObjectRef) + }; +} + +#[cfg(not(target_arch = "wasm32"))] +unsafe fn memory_bio_destructor(obj_addr: usize) { + unsafe { + pyre_interpreter::module::_ssl::w_memory_bio_dealloc(obj_addr as pyre_object::PyObjectRef) + }; +} + +#[cfg(not(target_arch = "wasm32"))] +unsafe fn ssl_session_destructor(obj_addr: usize) { + unsafe { + pyre_interpreter::module::_ssl::w_ssl_session_dealloc(obj_addr as pyre_object::PyObjectRef) + }; +} + +#[cfg(not(target_arch = "wasm32"))] +unsafe fn ssl_socket_destructor(obj_addr: usize) { + unsafe { + pyre_interpreter::module::_ssl::w_ssl_socket_dealloc(obj_addr as pyre_object::PyObjectRef) + }; +} + /// Custom trace for objects carrying the `MapdictStorageMixin` prefix /// (`W_ObjectObject` and native-layout Python subclasses such as /// `W_Random`; instance `map`+`storage`, @@ -745,6 +773,40 @@ unsafe fn random_object_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut maj f(std::ptr::addr_of_mut!(inst.rnd) as *mut majit_ir::GcRef); } +/// `_ssl._SSLContext` has the native-layout mapdict prefix plus the three +/// Python callback/path references owned by the context wrapper. +#[cfg(not(target_arch = "wasm32"))] +unsafe fn ssl_context_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { + unsafe { object_object_custom_trace(obj_addr, f) }; + let context = unsafe { &mut *(obj_addr as *mut pyre_interpreter::module::_ssl::W_SSLContext) }; + f(std::ptr::addr_of_mut!(context.sni_callback) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(context.msg_callback) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(context.keylog_filename) as *mut majit_ir::GcRef); +} + +/// `ssl.MemoryBIO` is subclassable and therefore carries mapdict storage even +/// though its rustls transport state contains no Python references. +#[cfg(not(target_arch = "wasm32"))] +unsafe fn memory_bio_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { + unsafe { object_object_custom_trace(obj_addr, f) }; +} + +/// `_ssl._SSLSocket` owns its context, transport endpoints, cached unbound +/// socket methods, public owner, and hostname directly on the typed object. +#[cfg(not(target_arch = "wasm32"))] +unsafe fn ssl_socket_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { + let socket = unsafe { &mut *(obj_addr as *mut pyre_interpreter::module::_ssl::W_SSLSocket) }; + f(std::ptr::addr_of_mut!(socket.ob.w_class) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.context) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.socket) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.socket_send) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.socket_recv) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.incoming) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.outgoing) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.owner) as *mut majit_ir::GcRef); + f(std::ptr::addr_of_mut!(socket.server_hostname) as *mut majit_ir::GcRef); +} + /// Custom trace for `W_ModuleDictObject` /// (`dictmultiobject.py:328 W_ModuleDictObject`). /// @@ -3558,6 +3620,103 @@ fn build_gc() -> Box { ::DESCRIPTOR, ); + // `_ssl` keeps rustls objects behind opaque native pointers. Context and + // MemoryBIO are subclassable native layouts, so their marker walks the + // mapdict prefix; Context additionally owns Python callbacks/path values. + // Their sweep destructors release the opaque rustls allocations. + #[cfg(not(target_arch = "wasm32"))] + { + let context_descr = ::DESCRIPTOR; + let context_tid = gc.register_type( + TypeInfo::object_subclass_with_custom_trace( + context_descr.object_size, + object_tid, + ssl_context_custom_trace, + ) + .with_destructor_fn(ssl_context_destructor), + ); + context_descr.gc_type_id.set(context_tid); + majit_gc::GcAllocator::register_vtable_for_type( + &mut gc, + context_descr.pytype_ptr as usize, + context_tid, + ); + pytype_to_tid.insert(context_descr.pytype_ptr as usize, context_tid); + pyre_object::gc_hook::register_pyre_class_offsets( + context_descr.pytype_ptr as usize, + context_descr.ptr_offsets, + ); + + let bio_descr = ::DESCRIPTOR; + let bio_tid = gc.register_type( + TypeInfo::object_subclass_with_custom_trace( + bio_descr.object_size, + object_tid, + memory_bio_custom_trace, + ) + .with_destructor_fn(memory_bio_destructor), + ); + bio_descr.gc_type_id.set(bio_tid); + majit_gc::GcAllocator::register_vtable_for_type( + &mut gc, + bio_descr.pytype_ptr as usize, + bio_tid, + ); + pytype_to_tid.insert(bio_descr.pytype_ptr as usize, bio_tid); + pyre_object::gc_hook::register_pyre_class_offsets( + bio_descr.pytype_ptr as usize, + bio_descr.ptr_offsets, + ); + + let session_descr = ::DESCRIPTOR; + let session_tid = gc.register_type( + TypeInfo::object_subclass(session_descr.object_size, object_tid) + .with_destructor_fn(ssl_session_destructor), + ); + session_descr.gc_type_id.set(session_tid); + majit_gc::GcAllocator::register_vtable_for_type( + &mut gc, + session_descr.pytype_ptr as usize, + session_tid, + ); + pytype_to_tid.insert(session_descr.pytype_ptr as usize, session_tid); + pyre_object::gc_hook::register_pyre_class_offsets( + session_descr.pytype_ptr as usize, + session_descr.ptr_offsets, + ); + + let socket_descr = ::DESCRIPTOR; + let socket_tid = gc.register_type( + TypeInfo::object_subclass_with_custom_trace( + socket_descr.object_size, + object_tid, + ssl_socket_custom_trace, + ) + .with_destructor_fn(ssl_socket_destructor), + ); + socket_descr.gc_type_id.set(socket_tid); + majit_gc::GcAllocator::register_vtable_for_type( + &mut gc, + socket_descr.pytype_ptr as usize, + socket_tid, + ); + pytype_to_tid.insert(socket_descr.pytype_ptr as usize, socket_tid); + pyre_object::gc_hook::register_pyre_class_offsets( + socket_descr.pytype_ptr as usize, + socket_descr.ptr_offsets, + ); + + register_pyre_class( + &mut gc, + &mut pytype_to_tid, + ::DESCRIPTOR, + ); + } // `rrandom.Random` — the Mersenne Twister `interp_random.py:21` allocates // beside its holder. Like W_DequeBlock it is GC-managed without being an // rclass.OBJECT subclass and has no Python-visible vtable, so it takes a diff --git a/pyre/pyre-native/Cargo.toml b/pyre/pyre-native/Cargo.toml index 42a8c2b4f96..88c72e38958 100644 --- a/pyre/pyre-native/Cargo.toml +++ b/pyre/pyre-native/Cargo.toml @@ -15,3 +15,12 @@ blake2b_simd = { workspace = true } blake2s_simd = { workspace = true } scrypt = { workspace = true } flate2 = { workspace = true, features = ["zlib-rs"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rustls = { workspace = true } +rustls-pemfile = { workspace = true } +x509-parser = { workspace = true } +pem-rfc7468 = { workspace = true } +der = { workspace = true } +pkcs8 = { workspace = true } +rustls-native-certs = { workspace = true } diff --git a/pyre/pyre-native/src/lib.rs b/pyre/pyre-native/src/lib.rs index 6168b83124d..7e5287b9cdd 100644 --- a/pyre/pyre-native/src/lib.rs +++ b/pyre/pyre-native/src/lib.rs @@ -5,4 +5,6 @@ //! lowered into the meta-traceable `.ullbc`. pub mod hash; +#[cfg(not(target_arch = "wasm32"))] +pub mod ssl; pub mod zlib; diff --git a/pyre/pyre-native/src/ssl.rs b/pyre/pyre-native/src/ssl.rs new file mode 100644 index 00000000000..39cbb4f1e23 --- /dev/null +++ b/pyre/pyre-native/src/ssl.rs @@ -0,0 +1,2657 @@ +//! rustls backend for Python's `_ssl` module. +//! +//! This crate is deliberately outside the Charon/LLBC extraction. The +//! interpreter owns each backend value through an opaque pointer and reaches +//! it only through these non-generic, non-inlined functions, exactly as it +//! reaches the native hash and zlib engines. No Python object lives here. + +use std::io::Cursor; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, Once}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use rustls::client::ClientSessionStore; +use rustls::pki_types::{ + CertificateDer, CertificateRevocationListDer, PrivateKeyDer, PrivatePkcs8KeyDer, +}; +use rustls::sign::CertifiedKey; +use x509_parser::prelude::FromDer; + +static INSTALL_PROVIDER: Once = Once::new(); + +/// Install the process-wide rustls provider before constructing TLS state. +/// +/// Rustls requires one provider for a process. AWS-LC is selected by the +/// workspace feature because it is RustPython's tested rustls provider and its +/// name is already admitted by CPython's `test_ssl` backend-version check. +#[inline(never)] +pub fn ensure_provider() { + INSTALL_PROVIDER.call_once(|| { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + }); +} + +/// In-memory encrypted transport used by `SSLObject`. +/// +/// Keeping the unread suffix as `(Vec, start)` avoids the repeated whole-buffer +/// shifts in RustPython's `Vec::drain(..n)` implementation. Compaction happens +/// only after a substantial prefix has been consumed. +pub struct MemoryBio { + buffer: Vec, + start: usize, + eof_written: bool, +} + +impl MemoryBio { + fn pending(&self) -> usize { + self.buffer.len() - self.start + } + + fn compact(&mut self) { + if self.start == self.buffer.len() { + self.buffer.clear(); + self.start = 0; + } else if self.start >= 4096 && self.start * 2 >= self.buffer.len() { + self.buffer.copy_within(self.start.., 0); + self.buffer.truncate(self.buffer.len() - self.start); + self.start = 0; + } + } +} + +#[inline(never)] +pub fn memory_bio_new() -> *mut MemoryBio { + Box::into_raw(Box::new(MemoryBio { + buffer: Vec::new(), + start: 0, + eof_written: false, + })) +} + +/// # Safety +/// `bio` must be null or a pointer returned by [`memory_bio_new`] that has not +/// already been freed. +#[inline(never)] +pub unsafe fn memory_bio_free(bio: *mut MemoryBio) { + if !bio.is_null() { + unsafe { drop(Box::from_raw(bio)) }; + } +} + +/// # Safety +/// `bio` must point to a live [`MemoryBio`]. +#[inline(never)] +pub unsafe fn memory_bio_read(bio: *mut MemoryBio, size: usize) -> Vec { + let bio = unsafe { &mut *bio }; + let count = size.min(bio.pending()); + let end = bio.start + count; + let out = bio.buffer[bio.start..end].to_vec(); + bio.start = end; + bio.compact(); + out +} + +/// # Safety +/// `bio` must point to a live [`MemoryBio`]. +#[inline(never)] +pub unsafe fn memory_bio_write(bio: *mut MemoryBio, data: &[u8]) -> Result { + let bio = unsafe { &mut *bio }; + if bio.eof_written { + return Err("cannot write() after write_eof()"); + } + bio.compact(); + bio.buffer.extend_from_slice(data); + Ok(data.len()) +} + +/// # Safety +/// `bio` must point to a live [`MemoryBio`]. +#[inline(never)] +pub unsafe fn memory_bio_write_eof(bio: *mut MemoryBio) { + unsafe { (*bio).eof_written = true }; +} + +/// # Safety +/// `bio` must point to a live [`MemoryBio`]. +#[inline(never)] +pub unsafe fn memory_bio_pending(bio: *const MemoryBio) -> usize { + unsafe { (&*bio).pending() } +} + +/// # Safety +/// `bio` must point to a live [`MemoryBio`]. +#[inline(never)] +pub unsafe fn memory_bio_eof(bio: *const MemoryBio) -> bool { + let bio = unsafe { &*bio }; + bio.eof_written && bio.pending() == 0 +} + +/// Mutable Python-visible SSL context settings plus rustls trust material. +/// Connection configs are built from this state when `_wrap_socket` or +/// `_wrap_bio` is called; rustls configs themselves are intentionally not +/// mutated after publication. +pub struct Context { + protocol: i32, + check_hostname: bool, + verify_mode: i32, + verify_flags: i32, + options: u64, + minimum_version: i32, + maximum_version: i32, + alpn_protocols: Vec>, + roots: rustls::RootCertStore, + root_der: Vec>, + // OpenSSL's hashed CA directories are lookup sources, not eagerly loaded + // stores. Keep the directories themselves and materialize candidates in + // each immutable rustls ClientConfig; the certificate actually selected + // for a successful chain is published to `root_der` after the handshake. + capaths: Vec, + crls: Vec>, + cipher_suites: Option>, + ecdh_curve: Option, + certified_keys: Vec>, + server_session_store: Arc, + server_ticketer: Arc, + accept_count: AtomicUsize, + session_hits: AtomicUsize, +} + +#[derive(Clone, Copy)] +enum EcdhCurve { + Secp256r1, + Secp384r1, + X25519, +} + +pub const PROTOCOL_TLS: i32 = 2; +pub const PROTOCOL_TLS_CLIENT: i32 = 16; +pub const PROTOCOL_TLS_SERVER: i32 = 17; +pub const CERT_NONE: i32 = 0; +pub const CERT_OPTIONAL: i32 = 1; +pub const CERT_REQUIRED: i32 = 2; +pub const DEFAULT_OPTIONS: u64 = + 0x0000_0bfb | 0x0200_0000 | 0x0002_0000 | 0x0040_0000 | 0x0010_0000; + +impl Context { + fn new(protocol: i32) -> Result { + ensure_provider(); + if !matches!( + protocol, + PROTOCOL_TLS | PROTOCOL_TLS_CLIENT | PROTOCOL_TLS_SERVER | 5 | 6 + ) { + return Err("invalid or unsupported protocol version"); + } + let client = protocol == PROTOCOL_TLS_CLIENT; + let server_ticketer = rustls::crypto::aws_lc_rs::Ticketer::new() + .map_err(|_| "failed to initialize TLS session ticket encryption")?; + Ok(Self { + protocol, + check_hostname: client, + verify_mode: if client { CERT_REQUIRED } else { CERT_NONE }, + verify_flags: 32768, + options: DEFAULT_OPTIONS, + minimum_version: match protocol { + 5 => 0x303, + 6 => 0x304, + _ => -2, + }, + maximum_version: match protocol { + 5 => 0x303, + 6 => 0x304, + _ => -1, + }, + alpn_protocols: Vec::new(), + roots: rustls::RootCertStore::empty(), + root_der: Vec::new(), + capaths: Vec::new(), + crls: Vec::new(), + cipher_suites: None, + ecdh_curve: None, + certified_keys: Vec::new(), + server_session_store: rustls::server::ServerSessionMemoryCache::new(256), + server_ticketer, + accept_count: AtomicUsize::new(0), + session_hits: AtomicUsize::new(0), + }) + } +} + +#[inline(never)] +pub unsafe fn context_session_stats(context: *const Context) -> (usize, usize) { + let context = unsafe { &*context }; + ( + context.accept_count.load(Ordering::Relaxed), + context.session_hits.load(Ordering::Relaxed), + ) +} + +#[inline(never)] +pub fn context_new(protocol: i32) -> Result<*mut Context, &'static str> { + Context::new(protocol).map(|context| Box::into_raw(Box::new(context))) +} + +/// # Safety +/// `context` must be null or a pointer returned by [`context_new`] that has not +/// already been freed. +#[inline(never)] +pub unsafe fn context_free(context: *mut Context) { + if !context.is_null() { + unsafe { drop(Box::from_raw(context)) }; + } +} + +macro_rules! context_scalar { + ($get:ident, $set:ident, $field:ident, $ty:ty) => { + #[inline(never)] + pub unsafe fn $get(context: *const Context) -> $ty { + unsafe { (*context).$field } + } + + #[inline(never)] + pub unsafe fn $set(context: *mut Context, value: $ty) { + unsafe { (*context).$field = value }; + } + }; +} + +context_scalar!(context_protocol, context_set_protocol, protocol, i32); +context_scalar!( + context_check_hostname, + context_set_check_hostname, + check_hostname, + bool +); +context_scalar!( + context_verify_mode, + context_set_verify_mode, + verify_mode, + i32 +); +context_scalar!( + context_verify_flags, + context_set_verify_flags, + verify_flags, + i32 +); +context_scalar!(context_options, context_set_options, options, u64); +context_scalar!( + context_minimum_version, + context_set_minimum_version, + minimum_version, + i32 +); +context_scalar!( + context_maximum_version, + context_set_maximum_version, + maximum_version, + i32 +); + +/// Store the wire-format ALPN list after the interpreter has validated it. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_set_alpn(context: *mut Context, protocols: Vec>) { + unsafe { (*context).alpn_protocols = protocols }; +} + +/// Add DER trust anchors, returning `(accepted, rejected)`. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_add_roots( + context: *mut Context, + certs: Vec>, +) -> (usize, usize) { + let context = unsafe { &mut *context }; + let mut accepted = 0; + let mut rejected = 0; + for cert in certs { + match context.add_root(cert.as_ref().to_vec()) { + Ok(true) => accepted += 1, + Ok(false) => {} + Err(_) => rejected += 1, + } + } + (accepted, rejected) +} + +pub type NativeResult = Result; + +fn io_error(error: std::io::Error) -> (i32, String) { + (error.raw_os_error().unwrap_or(-1), error.to_string()) +} + +fn pem_error(message: impl std::fmt::Display) -> (i32, String) { + (0, format!("[SSL] PEM routines: {message}")) +} + +impl Context { + /// Add one trust anchor while retaining its full DER exactly once. The + /// full certificate is needed for `get_ca_certs`; rustls intentionally + /// stores only the parsed trust-anchor projection. + fn add_root(&mut self, der: Vec) -> NativeResult { + if self.root_der.iter().any(|known| known == &der) { + return Ok(false); + } + self.roots + .add(CertificateDer::from(der.clone())) + .map_err(|error| pem_error(error))?; + self.root_der.push(der); + Ok(true) + } +} + +fn read_pem_certificates(data: &[u8]) -> NativeResult>> { + let mut cursor = Cursor::new(data); + let certs = rustls_pemfile::certs(&mut cursor) + .collect::, _>>() + .map_err(pem_error)?; + if certs.is_empty() { + return Err(pem_error("no start line")); + } + Ok(certs) +} + +fn read_private_key(data: &[u8], password: Option<&[u8]>) -> NativeResult> { + if let Some(password) = password { + use der::SecretDocument; + use pkcs8::EncryptedPrivateKeyInfoRef; + + let pem = String::from_utf8_lossy(data); + if let Some(start) = pem.find("-----BEGIN ENCRYPTED PRIVATE KEY-----") { + let tail = &pem[start..]; + let end_marker = "-----END ENCRYPTED PRIVATE KEY-----"; + let end = tail + .find(end_marker) + .ok_or_else(|| pem_error("unterminated encrypted private key"))? + + end_marker.len(); + let (_, document) = SecretDocument::from_pem(&tail[..end]) + .map_err(|error| pem_error(format!("bad encrypted private key: {error}")))?; + let encrypted = EncryptedPrivateKeyInfoRef::try_from(document.as_bytes()) + .map_err(|error| pem_error(format!("bad encrypted private key: {error}")))?; + let decrypted = encrypted + .decrypt(password) + .map_err(|error| pem_error(format!("bad decrypt: {error}")))?; + return Ok(PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( + decrypted.as_bytes().to_vec(), + ))); + } + } + + rustls_pemfile::private_key(&mut Cursor::new(data)) + .map_err(pem_error)? + .ok_or_else(|| pem_error("no private key found")) +} + +/// Load and validate the context's certificate/private-key pair. +/// +/// The replacement is committed only after parsing, provider key loading, and +/// public/private key matching all succeed, so concurrent connection creation +/// never observes a half-updated pair. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_load_cert_chain( + context: *mut Context, + cert_path: &str, + key_path: &str, + password: Option<&[u8]>, +) -> NativeResult<()> { + ensure_provider(); + let cert_data = std::fs::read(cert_path).map_err(io_error)?; + let certs = read_pem_certificates(&cert_data)?; + let key_data = std::fs::read(key_path).map_err(io_error)?; + let key = read_private_key(&key_data, password)?; + let signing_key = rustls::crypto::aws_lc_rs::sign::any_supported_type(&key) + .map_err(|error| pem_error(format!("unsupported private key: {error}")))?; + let certified = CertifiedKey::new(certs, signing_key); + certified.keys_match().map_err(|_| { + ( + 0, + "[SSL: KEY_VALUES_MISMATCH] key values mismatch".to_string(), + ) + })?; + let context = unsafe { &mut *context }; + let algorithm = certified.key.algorithm(); + let certified = Arc::new(certified); + if let Some(existing) = context + .certified_keys + .iter_mut() + .find(|known| known.key.algorithm() == algorithm) + { + *existing = certified; + } else { + context.certified_keys.push(certified); + } + Ok(()) +} + +fn parse_concatenated_der(mut data: &[u8]) -> NativeResult>> { + let mut certs = Vec::new(); + while !data.is_empty() { + let before = data.len(); + let (remaining, _) = x509_parser::parse_x509_certificate(data) + .map_err(|error| pem_error(format!("not enough data: {error}")))?; + let consumed = before - remaining.len(); + if consumed == 0 { + return Err(pem_error("not enough data")); + } + certs.push(data[..consumed].to_vec()); + data = remaining; + } + Ok(certs) +} + +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_load_verify_file(context: *mut Context, path: &str) -> NativeResult { + let data = std::fs::read(path).map_err(io_error)?; + let items = rustls_pemfile::read_all(&mut Cursor::new(data)) + .collect::, _>>() + .map_err(pem_error)?; + let mut certificates = Vec::new(); + let mut crls = Vec::new(); + for item in items { + match item { + rustls_pemfile::Item::X509Certificate(cert) => { + certificates.push(cert.as_ref().to_vec()) + } + rustls_pemfile::Item::Crl(crl) => crls.push(crl), + _ => {} + } + } + if certificates.is_empty() && crls.is_empty() { + return Err(pem_error("no start line")); + } + let added = unsafe { context_add_verify_der(context, certificates.into_iter())? }; + let context = unsafe { &mut *context }; + for crl in crls { + if !context + .crls + .iter() + .any(|known| known.as_ref() == crl.as_ref()) + { + context.crls.push(crl); + } + } + Ok(added) +} + +/// Register an OpenSSL-style hashed certificate directory for lazy lookup. +/// The directory has already been validated by the interpreter boundary. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_add_verify_dir(context: *mut Context, path: &str) { + let context = unsafe { &mut *context }; + let path = std::path::PathBuf::from(path); + if !context.capaths.iter().any(|known| known == &path) { + context.capaths.push(path); + } +} + +/// Publish the trust anchor selected from a lazy `capath` lookup. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_add_verified_root(context: *mut Context, der: Vec) -> NativeResult { + unsafe { (&mut *context).add_root(der) } +} + +/// Load PEM text or one/more concatenated DER certificates. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_load_verify_data( + context: *mut Context, + data: &[u8], + pem: bool, +) -> NativeResult { + let certs: Vec> = if pem { + read_pem_certificates(data)? + .into_iter() + .map(|cert| cert.as_ref().to_vec()) + .collect() + } else { + parse_concatenated_der(data)? + }; + unsafe { context_add_verify_der(context, certs.into_iter()) } +} + +unsafe fn context_add_verify_der( + context: *mut Context, + certs: impl Iterator>, +) -> NativeResult { + let context = unsafe { &mut *context }; + let mut added = 0; + for der in certs { + added += usize::from(context.add_root(der)?); + } + Ok(added) +} + +/// Load trust anchors from the platform provider (Keychain on macOS, native +/// certificate stores on Windows, discovered bundle/directories on Unix). +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_load_native_roots(context: *mut Context) -> NativeResult { + let result = rustls_native_certs::load_native_certs(); + let added = unsafe { + context_add_verify_der( + context, + result.certs.into_iter().map(|cert| cert.as_ref().to_vec()), + )? + }; + if added == 0 && !result.errors.is_empty() { + return Err(pem_error(&result.errors[0])); + } + Ok(added) +} + +fn certificate_is_ca(der: &[u8]) -> bool { + x509_parser::certificate::X509Certificate::from_der(der) + .ok() + .map(|(_, cert)| { + cert.basic_constraints() + .ok() + .flatten() + .is_some_and(|constraints| constraints.value.ca) + // OpenSSL's X509_check_ca retains its legacy rule for a + // self-issued X.509v1 trust anchor without BasicConstraints. + || (cert.version().0 == 0 && cert.subject() == cert.issuer()) + }) + .unwrap_or(false) +} + +/// Return `(all_x509, ca_x509)`. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_cert_store_stats(context: *const Context) -> (usize, usize) { + let context = unsafe { &*context }; + ( + context.root_der.len(), + context + .root_der + .iter() + .filter(|der| certificate_is_ca(der)) + .count(), + ) +} + +/// Full DER for CA certificates only. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_ca_certs(context: *const Context) -> Vec> { + unsafe { &*context } + .root_der + .iter() + .filter(|der| certificate_is_ca(der)) + .cloned() + .collect() +} + +type DistinguishedName = Vec>; + +struct SubjectAlternativeName { + kind: &'static str, + value: String, + directory_name: DistinguishedName, +} + +/// Owned projection of the X.509 fields exposed by CPython's private +/// `_test_decode_cert()` helper and by `SSLContext.get_ca_certs()`. Keeping +/// this projection native prevents x509-parser's borrowed type graph from +/// entering the translated interpreter. +pub struct DecodedCertificate { + issuer: DistinguishedName, + subject: DistinguishedName, + not_after: String, + not_before: String, + serial_number: String, + version: i32, + ocsp: Vec, + ca_issuers: Vec, + crl_distribution_points: Vec, + subject_alt_names: Vec, +} + +fn oid_attribute_name(oid: &str) -> String { + match oid { + "2.5.4.3" => "commonName".to_string(), + "2.5.4.6" => "countryName".to_string(), + "2.5.4.7" => "localityName".to_string(), + "2.5.4.8" => "stateOrProvinceName".to_string(), + "2.5.4.10" => "organizationName".to_string(), + "2.5.4.11" => "organizationalUnitName".to_string(), + "1.2.840.113549.1.9.1" => "emailAddress".to_string(), + _ => oid.to_string(), + } +} + +fn decode_name(name: &x509_parser::x509::X509Name<'_>) -> DistinguishedName { + name.iter() + .map(|rdn| { + rdn.iter() + .map(|attribute| { + let oid = attribute.attr_type().to_id_string(); + let value = attribute + .attr_value() + .as_str() + .map(str::to_string) + .unwrap_or_else(|_| { + String::from_utf8_lossy(attribute.attr_value().data).into_owned() + }); + (oid_attribute_name(&oid), value) + }) + .collect() + }) + .collect() +} + +fn format_certificate_time(value: &x509_parser::time::ASN1Time) -> String { + let date = value.to_datetime(); + const MONTHS: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + ]; + format!( + "{} {:>2} {:02}:{:02}:{:02} {:04} GMT", + MONTHS[date.month() as usize - 1], + date.day(), + date.hour(), + date.minute(), + date.second(), + date.year(), + ) +} + +fn format_ip_address(ip: &[u8]) -> String { + match ip.len() { + 4 => format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]), + 16 => ip + .chunks_exact(2) + .map(|part| format!("{:X}", u16::from_be_bytes([part[0], part[1]]))) + .collect::>() + .join(":"), + _ => "".to_string(), + } +} + +fn decode_certificate(der: &[u8]) -> NativeResult { + use x509_parser::extensions::{DistributionPointName, GeneralName, ParsedExtension}; + use x509_parser::oid_registry::{ + OID_PKIX_AUTHORITY_INFO_ACCESS, OID_X509_EXT_CRL_DISTRIBUTION_POINTS, + }; + + let (_, cert) = x509_parser::parse_x509_certificate(der) + .map_err(|error| pem_error(format!("failed to parse certificate: {error}")))?; + let mut decoded = DecodedCertificate { + issuer: decode_name(cert.issuer()), + subject: decode_name(cert.subject()), + not_after: format_certificate_time(&cert.validity().not_after), + not_before: format_certificate_time(&cert.validity().not_before), + serial_number: { + let mut serial = cert.serial.to_str_radix(16).to_uppercase(); + if serial.len() % 2 != 0 { + serial.insert(0, '0'); + } + serial + }, + version: cert.version().0 as i32 + 1, + ocsp: Vec::new(), + ca_issuers: Vec::new(), + crl_distribution_points: Vec::new(), + subject_alt_names: Vec::new(), + }; + + if let Ok(extensions) = cert.tbs_certificate.extensions_map() { + if let Some(extension) = extensions.get(&OID_PKIX_AUTHORITY_INFO_ACCESS) + && let ParsedExtension::AuthorityInfoAccess(access) = extension.parsed_extension() + { + for description in &access.accessdescs { + if let GeneralName::URI(uri) = &description.access_location { + match description.access_method.to_id_string().as_str() { + "1.3.6.1.5.5.7.48.1" => decoded.ocsp.push((*uri).to_string()), + "1.3.6.1.5.5.7.48.2" => decoded.ca_issuers.push((*uri).to_string()), + _ => {} + } + } + } + } + if let Some(extension) = extensions.get(&OID_X509_EXT_CRL_DISTRIBUTION_POINTS) + && let ParsedExtension::CRLDistributionPoints(points) = extension.parsed_extension() + { + for point in &points.points { + if let Some(DistributionPointName::FullName(names)) = &point.distribution_point { + for name in names { + if let GeneralName::URI(uri) = name { + decoded.crl_distribution_points.push((*uri).to_string()); + } + } + } + } + } + } + + if let Ok(Some(extension)) = cert.subject_alternative_name() { + for name in &extension.value.general_names { + let entry = match name { + GeneralName::DNSName(value) => SubjectAlternativeName { + kind: "DNS", + value: (*value).to_string(), + directory_name: Vec::new(), + }, + GeneralName::IPAddress(value) => SubjectAlternativeName { + kind: "IP Address", + value: format_ip_address(value), + directory_name: Vec::new(), + }, + GeneralName::RFC822Name(value) => SubjectAlternativeName { + kind: "email", + value: (*value).to_string(), + directory_name: Vec::new(), + }, + GeneralName::URI(value) => SubjectAlternativeName { + kind: "URI", + value: (*value).to_string(), + directory_name: Vec::new(), + }, + GeneralName::OtherName(_, _) => SubjectAlternativeName { + kind: "othername", + value: "".to_string(), + directory_name: Vec::new(), + }, + GeneralName::DirectoryName(value) => SubjectAlternativeName { + kind: "DirName", + value: String::new(), + directory_name: decode_name(value), + }, + GeneralName::RegisteredID(value) => SubjectAlternativeName { + kind: "Registered ID", + value: value.to_id_string(), + directory_name: Vec::new(), + }, + _ => continue, + }; + decoded.subject_alt_names.push(entry); + } + } + Ok(decoded) +} + +#[inline(never)] +pub fn certificate_decode_der(der: &[u8]) -> NativeResult<*mut DecodedCertificate> { + decode_certificate(der).map(|cert| Box::into_raw(Box::new(cert))) +} + +#[inline(never)] +pub fn certificate_decode_file(path: &str) -> NativeResult<*mut DecodedCertificate> { + let data = std::fs::read(path).map_err(io_error)?; + let certs = read_pem_certificates(&data)?; + certificate_decode_der(certs[0].as_ref()) +} + +/// # Safety +/// `cert` must be null or a live pointer returned by a certificate decoder. +#[inline(never)] +pub unsafe fn certificate_free(cert: *mut DecodedCertificate) { + if !cert.is_null() { + unsafe { drop(Box::from_raw(cert)) }; + } +} + +macro_rules! certificate_string { + ($name:ident, $field:ident) => { + #[inline(never)] + pub unsafe fn $name(cert: *const DecodedCertificate) -> String { + unsafe { (*cert).$field.clone() } + } + }; +} + +certificate_string!(certificate_not_after, not_after); +certificate_string!(certificate_not_before, not_before); +certificate_string!(certificate_serial_number, serial_number); + +#[inline(never)] +pub unsafe fn certificate_version(cert: *const DecodedCertificate) -> i32 { + unsafe { (*cert).version } +} + +fn decoded_name(cert: &DecodedCertificate, subject: bool) -> &DistinguishedName { + if subject { &cert.subject } else { &cert.issuer } +} + +#[inline(never)] +pub unsafe fn certificate_name_rdn_count(cert: *const DecodedCertificate, subject: bool) -> usize { + decoded_name(unsafe { &*cert }, subject).len() +} + +#[inline(never)] +pub unsafe fn certificate_name_attribute_count( + cert: *const DecodedCertificate, + subject: bool, + rdn: usize, +) -> usize { + decoded_name(unsafe { &*cert }, subject)[rdn].len() +} + +#[inline(never)] +pub unsafe fn certificate_name_attribute_key( + cert: *const DecodedCertificate, + subject: bool, + rdn: usize, + attribute: usize, +) -> String { + decoded_name(unsafe { &*cert }, subject)[rdn][attribute] + .0 + .clone() +} + +#[inline(never)] +pub unsafe fn certificate_name_attribute_value( + cert: *const DecodedCertificate, + subject: bool, + rdn: usize, + attribute: usize, +) -> String { + decoded_name(unsafe { &*cert }, subject)[rdn][attribute] + .1 + .clone() +} + +fn certificate_urls(cert: &DecodedCertificate, kind: i32) -> &Vec { + match kind { + 0 => &cert.ocsp, + 1 => &cert.ca_issuers, + _ => &cert.crl_distribution_points, + } +} + +#[inline(never)] +pub unsafe fn certificate_url_count(cert: *const DecodedCertificate, kind: i32) -> usize { + certificate_urls(unsafe { &*cert }, kind).len() +} + +#[inline(never)] +pub unsafe fn certificate_url(cert: *const DecodedCertificate, kind: i32, index: usize) -> String { + certificate_urls(unsafe { &*cert }, kind)[index].clone() +} + +#[inline(never)] +pub unsafe fn certificate_san_count(cert: *const DecodedCertificate) -> usize { + unsafe { (*cert).subject_alt_names.len() } +} + +fn decoded_san(cert: &DecodedCertificate, index: usize) -> &SubjectAlternativeName { + &cert.subject_alt_names[index] +} + +#[inline(never)] +pub unsafe fn certificate_san_kind(cert: *const DecodedCertificate, index: usize) -> &'static str { + decoded_san(unsafe { &*cert }, index).kind +} + +#[inline(never)] +pub unsafe fn certificate_san_value(cert: *const DecodedCertificate, index: usize) -> String { + decoded_san(unsafe { &*cert }, index).value.clone() +} + +#[inline(never)] +pub unsafe fn certificate_san_directory_rdn_count( + cert: *const DecodedCertificate, + index: usize, +) -> usize { + decoded_san(unsafe { &*cert }, index).directory_name.len() +} + +#[inline(never)] +pub unsafe fn certificate_san_directory_attribute_count( + cert: *const DecodedCertificate, + index: usize, + rdn: usize, +) -> usize { + decoded_san(unsafe { &*cert }, index).directory_name[rdn].len() +} + +#[inline(never)] +pub unsafe fn certificate_san_directory_attribute_key( + cert: *const DecodedCertificate, + index: usize, + rdn: usize, + attribute: usize, +) -> String { + decoded_san(unsafe { &*cert }, index).directory_name[rdn][attribute] + .0 + .clone() +} + +#[inline(never)] +pub unsafe fn certificate_san_directory_attribute_value( + cert: *const DecodedCertificate, + index: usize, + rdn: usize, + attribute: usize, +) -> String { + decoded_san(unsafe { &*cert }, index).directory_name[rdn][attribute] + .1 + .clone() +} + +#[derive(Clone, Copy)] +struct CipherInfo { + name: &'static str, + protocol: &'static str, + bits: i32, + aead: bool, + symmetric: &'static str, + digest: &'static str, + kea: &'static str, + auth: &'static str, +} + +const CIPHERS: &[CipherInfo] = &[ + CipherInfo { + name: "TLS_AES_128_GCM_SHA256", + protocol: "TLSv1.3", + bits: 128, + aead: true, + symmetric: "aes-128-gcm", + digest: "sha256", + kea: "kx-any", + auth: "auth-any", + }, + CipherInfo { + name: "TLS_AES_256_GCM_SHA384", + protocol: "TLSv1.3", + bits: 256, + aead: true, + symmetric: "aes-256-gcm", + digest: "sha384", + kea: "kx-any", + auth: "auth-any", + }, + CipherInfo { + name: "TLS_CHACHA20_POLY1305_SHA256", + protocol: "TLSv1.3", + bits: 256, + aead: true, + symmetric: "chacha20-poly1305", + digest: "sha256", + kea: "kx-any", + auth: "auth-any", + }, + CipherInfo { + name: "ECDHE-ECDSA-AES128-GCM-SHA256", + protocol: "TLSv1.2", + bits: 128, + aead: true, + symmetric: "aes-128-gcm", + digest: "sha256", + kea: "kx-ecdhe", + auth: "auth-ecdsa", + }, + CipherInfo { + name: "ECDHE-ECDSA-AES256-GCM-SHA384", + protocol: "TLSv1.2", + bits: 256, + aead: true, + symmetric: "aes-256-gcm", + digest: "sha384", + kea: "kx-ecdhe", + auth: "auth-ecdsa", + }, + CipherInfo { + name: "ECDHE-RSA-AES128-GCM-SHA256", + protocol: "TLSv1.2", + bits: 128, + aead: true, + symmetric: "aes-128-gcm", + digest: "sha256", + kea: "kx-ecdhe", + auth: "auth-rsa", + }, + CipherInfo { + name: "ECDHE-RSA-AES256-GCM-SHA384", + protocol: "TLSv1.2", + bits: 256, + aead: true, + symmetric: "aes-256-gcm", + digest: "sha384", + kea: "kx-ecdhe", + auth: "auth-rsa", + }, +]; + +#[inline(never)] +pub fn cipher_count() -> usize { + CIPHERS.len() +} +#[inline(never)] +pub fn cipher_name(index: usize) -> &'static str { + CIPHERS[index].name +} +#[inline(never)] +pub fn cipher_protocol(index: usize) -> &'static str { + CIPHERS[index].protocol +} +#[inline(never)] +pub fn cipher_bits(index: usize) -> i32 { + CIPHERS[index].bits +} +#[inline(never)] +pub fn cipher_aead(index: usize) -> bool { + CIPHERS[index].aead +} +#[inline(never)] +pub fn cipher_symmetric(index: usize) -> &'static str { + CIPHERS[index].symmetric +} +#[inline(never)] +pub fn cipher_digest(index: usize) -> &'static str { + CIPHERS[index].digest +} +#[inline(never)] +pub fn cipher_kea(index: usize) -> &'static str { + CIPHERS[index].kea +} +#[inline(never)] +pub fn cipher_auth(index: usize) -> &'static str { + CIPHERS[index].auth +} + +#[inline(never)] +pub fn validate_cipher_string(pattern: &str) -> Result<(), &'static str> { + parse_cipher_string(pattern).map(|_| ()) +} + +fn cipher_pattern_matches(suite: rustls::SupportedCipherSuite, pattern: &str) -> bool { + if suite.tls13().is_some() { + return false; + } + let name = format!("{:?}", suite.suite()); + match pattern { + "ALL" | "DEFAULT" | "HIGH" => true, + "AES128" => name.contains("AES_128"), + "AES256" => name.contains("AES_256"), + "AESGCM" => name.contains("AES") && name.contains("GCM"), + "CHACHA20" => name.contains("CHACHA20"), + "ECDHE" | "KECDHE" => name.contains("ECDHE"), + "ECDSA" | "AECDSA" => name.contains("ECDSA"), + "RSA" | "ARSA" | "KRSA" => name.contains("RSA"), + "NULL" | "ANULL" | "ENULL" => false, + _ => { + let compact_name = name.replace('_', "").replace('-', ""); + let compact_pattern = pattern.replace('_', "").replace('-', ""); + compact_name.contains(&compact_pattern) + } + } +} + +fn parse_cipher_string(pattern: &str) -> Result, &'static str> { + ensure_provider(); + let provider = + rustls::crypto::CryptoProvider::get_default().expect("the _ssl provider is installed"); + let mut selected = Vec::new(); + let mut exclusions = Vec::new(); + for raw in pattern.split(':') { + let token = raw.trim().to_ascii_uppercase(); + if token.is_empty() || token.starts_with('@') || token.starts_with('+') { + continue; + } + if let Some(excluded) = token.strip_prefix('!') { + exclusions.push(excluded.to_string()); + continue; + } + let parts: Vec<&str> = token.split('+').collect(); + for suite in &provider.cipher_suites { + if parts + .iter() + .all(|part| cipher_pattern_matches(*suite, part)) + && !selected + .iter() + .any(|known: &rustls::SupportedCipherSuite| known.suite() == suite.suite()) + { + selected.push(*suite); + } + } + } + selected.retain(|suite| { + !exclusions + .iter() + .any(|pattern| cipher_pattern_matches(*suite, pattern)) + }); + if selected.is_empty() { + Err("No cipher can be selected") + } else { + Ok(selected) + } +} + +/// Apply OpenSSL's TLS <= 1.2 cipher-list selection. TLS 1.3 suites remain +/// provider defaults, matching `SSL_CTX_set_cipher_list` semantics. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_set_cipher_list( + context: *mut Context, + pattern: &str, +) -> Result<(), &'static str> { + unsafe { (*context).cipher_suites = Some(parse_cipher_string(pattern)?) }; + Ok(()) +} + +/// Restrict key exchange to the curve selected by SSLContext.set_ecdh_curve. +/// OpenSSL stores this choice directly on SSL_CTX; keep the equivalent state +/// on our native Context and apply it to each immutable rustls provider built +/// from that context. +/// +/// # Safety +/// `context` must point to a live [`Context`]. +#[inline(never)] +pub unsafe fn context_set_ecdh_curve( + context: *mut Context, + curve: &str, +) -> Result<(), &'static str> { + let curve = match curve { + "prime256v1" => EcdhCurve::Secp256r1, + "secp384r1" => EcdhCurve::Secp384r1, + "X25519" => EcdhCurve::X25519, + _ => return Err("unknown elliptic curve name"), + }; + unsafe { (*context).ecdh_curve = Some(curve) }; + Ok(()) +} + +#[derive(Debug)] +struct NoCertificateVerification; + +impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + _ocsp_response: &[u8], + _now: rustls::pki_types::UnixTime, + ) -> Result { + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + let provider = + rustls::crypto::CryptoProvider::get_default().expect("the _ssl provider is installed"); + rustls::crypto::verify_tls12_signature( + message, + cert, + signature, + &provider.signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + let provider = + rustls::crypto::CryptoProvider::get_default().expect("the _ssl provider is installed"); + rustls::crypto::verify_tls13_signature( + message, + cert, + signature, + &provider.signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::CryptoProvider::get_default() + .expect("the _ssl provider is installed") + .signature_verification_algorithms + .supported_schemes() + } +} + +/// Keep WebPKI's chain, time, purpose, and signature validation while making +/// Python's `SSLContext.check_hostname` an independent policy switch. +/// +/// rustls 0.23's WebPkiServerVerifier validates the complete chain before its +/// final server-name check. We deliberately supply an unrelated valid DNS +/// name and suppress only the two typed name-mismatch outcomes. Unlike +/// RustPython's certificate-name extraction workaround, this neither parses +/// SAN/CN a second time nor invents wildcard normalization rules. +#[derive(Debug)] +struct ChainOnlyServerVerifier { + inner: Arc, +} + +#[derive(Debug)] +struct PolicyServerVerifier { + inner: Arc, + require_authority_key_identifier: bool, + require_crl: bool, + has_crl: bool, +} + +impl rustls::client::danger::ServerCertVerifier for PolicyServerVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &rustls::pki_types::ServerName<'_>, + ocsp_response: &[u8], + now: rustls::pki_types::UnixTime, + ) -> Result { + if self.require_crl && !self.has_crl { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::UnknownRevocationStatus, + )); + } + if self.require_authority_key_identifier { + let (_, certificate) = x509_parser::parse_x509_certificate(end_entity.as_ref()) + .map_err(|_| { + rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding) + })?; + let has_aki = certificate + .extensions() + .iter() + .any(|extension| extension.oid.to_id_string() == "2.5.29.35"); + if !has_aki { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::ApplicationVerificationFailure, + )); + } + } + self.inner + .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, signature) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, signature) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + +impl rustls::client::danger::ServerCertVerifier for ChainOnlyServerVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + _server_name: &rustls::pki_types::ServerName<'_>, + ocsp_response: &[u8], + now: rustls::pki_types::UnixTime, + ) -> Result { + let unrelated_name = rustls::pki_types::ServerName::try_from("pyre.invalid") + .expect("the fixed chain-only verifier name is valid"); + match self.inner.verify_server_cert( + end_entity, + intermediates, + &unrelated_name, + ocsp_response, + now, + ) { + Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidForName + | rustls::CertificateError::NotValidForNameContext { .. }, + )) => Ok(rustls::client::danger::ServerCertVerified::assertion()), + result => result, + } + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, signature) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, signature) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + +fn is_capath_hash_name(name: &std::ffi::OsStr) -> bool { + let Some(name) = name.to_str() else { + return false; + }; + let Some((hash, suffix)) = name.split_once('.') else { + return false; + }; + hash.len() == 8 + && hash.bytes().all(|byte| byte.is_ascii_hexdigit()) + && !suffix.is_empty() + && suffix.bytes().all(|byte| byte.is_ascii_digit()) +} + +fn capath_certificates(context: &Context) -> Vec> { + let mut paths = Vec::new(); + for directory in &context.capaths { + let Ok(entries) = std::fs::read_dir(directory) else { + continue; + }; + for entry in entries.flatten() { + if is_capath_hash_name(&entry.file_name()) { + paths.push(entry.path()); + } + } + } + paths.sort(); + + let mut result = Vec::new(); + for path in paths { + let Ok(data) = std::fs::read(path) else { + continue; + }; + let Ok(certificates) = read_pem_certificates(&data) else { + continue; + }; + for certificate in certificates { + let der = certificate.as_ref().to_vec(); + if !result.iter().any(|known| known == &der) { + result.push(der); + } + } + } + result +} + +fn provider_for_context(context: &Context) -> Arc { + let mut provider = rustls::crypto::CryptoProvider::get_default() + .expect("the _ssl provider is installed") + .as_ref() + .clone(); + if let Some(selected) = &context.cipher_suites { + provider.cipher_suites.retain(|suite| { + suite.tls13().is_some() + || selected + .iter() + .any(|chosen| chosen.suite() == suite.suite()) + }); + } + if let Some(curve) = context.ecdh_curve { + provider.kx_groups = vec![match curve { + EcdhCurve::Secp256r1 => rustls::crypto::aws_lc_rs::kx_group::SECP256R1, + EcdhCurve::Secp384r1 => rustls::crypto::aws_lc_rs::kx_group::SECP384R1, + EcdhCurve::X25519 => rustls::crypto::aws_lc_rs::kx_group::X25519, + }]; + } + Arc::new(provider) +} + +#[derive(Debug)] +struct MultiCertResolver { + keys: Vec>, +} + +impl MultiCertResolver { + fn choose(&self, schemes: &[rustls::SignatureScheme]) -> Option> { + self.keys + .iter() + .find(|key| key.key.choose_scheme(schemes).is_some()) + .cloned() + } +} + +impl rustls::server::ResolvesServerCert for MultiCertResolver { + fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option> { + self.choose(client_hello.signature_schemes()) + } +} + +impl rustls::client::ResolvesClientCert for MultiCertResolver { + fn resolve( + &self, + _root_hint_subjects: &[&[u8]], + signature_schemes: &[rustls::SignatureScheme], + ) -> Option> { + self.choose(signature_schemes) + } + + fn has_certs(&self) -> bool { + !self.keys.is_empty() + } +} + +/// Per-connection rustls store. CPython only resumes a client connection when +/// an SSLSession is supplied explicitly, so unlike rustls' default config this +/// cache is not shared implicitly by every connection from one context. +#[derive(Debug)] +struct CapturingClientSessionStore { + inner: rustls::client::ClientSessionMemoryCache, + latest_tls12: Mutex< + Option<( + rustls::pki_types::ServerName<'static>, + rustls::client::Tls12ClientSessionValue, + )>, + >, + public_id: Mutex>>, + creation_time: Mutex>, +} + +impl CapturingClientSessionStore { + fn new() -> Self { + Self { + inner: rustls::client::ClientSessionMemoryCache::new(8), + latest_tls12: Mutex::new(None), + public_id: Mutex::new(None), + creation_time: Mutex::new(None), + } + } + + fn seed(&self, session: &NativeSession) { + self.inner + .set_tls12_session(session.server_name.clone(), session.value.clone()); + *self + .latest_tls12 + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = + Some((session.server_name.clone(), session.value.clone())); + *self + .public_id + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(session.id.clone()); + *self + .creation_time + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some(session.creation_time); + } + + fn ensure_metadata(&self) { + let mut id = self + .public_id + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if id.is_none() { + let mut bytes = vec![0u8; 32]; + let provider = rustls::crypto::CryptoProvider::get_default() + .expect("the _ssl provider is installed"); + if provider.secure_random.fill(&mut bytes).is_ok() { + *id = Some(bytes); + } + } + drop(id); + let mut created = self + .creation_time + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if created.is_none() { + *created = Some( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(), + ); + } + } + + fn snapshot( + &self, + context_identity: usize, + config: Arc, + ) -> Option { + let (server_name, value) = self + .latest_tls12 + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone()?; + self.ensure_metadata(); + Some(NativeSession { + context_identity, + config, + server_name, + value, + id: self + .public_id + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone()?, + creation_time: self + .creation_time + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .unwrap_or(0), + timeout: 43_200, + }) + } +} + +impl rustls::client::ClientSessionStore for CapturingClientSessionStore { + fn set_kx_hint( + &self, + server_name: rustls::pki_types::ServerName<'static>, + group: rustls::NamedGroup, + ) { + self.inner.set_kx_hint(server_name, group); + } + + fn kx_hint( + &self, + server_name: &rustls::pki_types::ServerName<'_>, + ) -> Option { + self.inner.kx_hint(server_name) + } + + fn set_tls12_session( + &self, + server_name: rustls::pki_types::ServerName<'static>, + value: rustls::client::Tls12ClientSessionValue, + ) { + self.inner + .set_tls12_session(server_name.clone(), value.clone()); + *self + .latest_tls12 + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some((server_name, value)); + self.ensure_metadata(); + } + + fn tls12_session( + &self, + server_name: &rustls::pki_types::ServerName<'_>, + ) -> Option { + // Keep the one TLS 1.2 value directly on this per-connection store, + // matching ClientSessionStore's documented cardinality. The inner + // cache remains responsible for KX hints and TLS 1.3 ticket queues. + let session = self + .latest_tls12 + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .as_ref() + .filter(|(known_name, _)| known_name == server_name) + .map(|(_, value)| value.clone()); + session + } + + fn remove_tls12_session(&self, server_name: &rustls::pki_types::ServerName<'static>) { + self.inner.remove_tls12_session(server_name); + let mut latest = self + .latest_tls12 + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if latest + .as_ref() + .is_some_and(|(known_name, _)| known_name == server_name) + { + *latest = None; + } + } + + fn insert_tls13_ticket( + &self, + server_name: rustls::pki_types::ServerName<'static>, + value: rustls::client::Tls13ClientSessionValue, + ) { + self.inner.insert_tls13_ticket(server_name, value); + } + + fn take_tls13_ticket( + &self, + server_name: &rustls::pki_types::ServerName<'static>, + ) -> Option { + self.inner.take_tls13_ticket(server_name) + } +} + +pub struct NativeSession { + context_identity: usize, + config: Arc, + server_name: rustls::pki_types::ServerName<'static>, + value: rustls::client::Tls12ClientSessionValue, + id: Vec, + creation_time: u64, + timeout: u64, +} + +#[inline(never)] +pub unsafe fn session_clone(session: *const NativeSession) -> *mut NativeSession { + if session.is_null() { + return std::ptr::null_mut(); + } + let session = unsafe { &*session }; + Box::into_raw(Box::new(NativeSession { + context_identity: session.context_identity, + config: session.config.clone(), + server_name: session.server_name.clone(), + value: session.value.clone(), + id: session.id.clone(), + creation_time: session.creation_time, + timeout: session.timeout, + })) +} + +#[inline(never)] +pub unsafe fn session_free(session: *mut NativeSession) { + if !session.is_null() { + unsafe { drop(Box::from_raw(session)) }; + } +} + +#[inline(never)] +pub unsafe fn session_context_identity(session: *const NativeSession) -> usize { + unsafe { (&*session).context_identity } +} + +#[inline(never)] +pub unsafe fn session_id(session: *const NativeSession) -> Vec { + unsafe { (&*session).id.clone() } +} + +#[inline(never)] +pub unsafe fn session_creation_time(session: *const NativeSession) -> u64 { + unsafe { (&*session).creation_time } +} + +#[inline(never)] +pub unsafe fn session_timeout(session: *const NativeSession) -> u64 { + unsafe { (&*session).timeout } +} + +fn client_config(context: &Context) -> NativeResult<(rustls::ClientConfig, Vec>)> { + let builder = rustls::ClientConfig::builder_with_provider(provider_for_context(context)) + .with_protocol_versions(enabled_versions(context)?) + .map_err(|error| (0, format!("[SSL] invalid TLS configuration: {error}")))?; + let deferred_roots = capath_certificates(context); + let mut roots = context.roots.clone(); + for der in &deferred_roots { + // A hashed directory is intentionally tolerant of stale or malformed + // entries. Only usable X.509 anchors participate in rustls lookup. + let _ = roots.add(CertificateDer::from(der.clone())); + } + let wants_client_cert = if context.verify_mode == CERT_NONE { + builder + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoCertificateVerification)) + } else if !roots.is_empty() { + let mut verifier_builder = rustls::client::WebPkiServerVerifier::builder(Arc::new(roots)); + if !context.crls.is_empty() { + verifier_builder = verifier_builder.with_crls(context.crls.clone()); + } + let verifier = verifier_builder.build().map_err(|error| { + ( + 0, + format!("[SSL] cannot build certificate verifier: {error}"), + ) + })?; + let mut verifier: Arc = + Arc::new(PolicyServerVerifier { + inner: verifier, + require_authority_key_identifier: context.verify_flags & 32 != 0, + require_crl: context.verify_flags & 12 != 0, + has_crl: !context.crls.is_empty(), + }); + if !context.check_hostname { + verifier = Arc::new(ChainOnlyServerVerifier { inner: verifier }); + } + builder + .dangerous() + .with_custom_certificate_verifier(verifier) + } else { + builder.with_root_certificates(roots) + }; + let mut config = if !context.certified_keys.is_empty() { + wants_client_cert.with_client_cert_resolver(Arc::new(MultiCertResolver { + keys: context.certified_keys.clone(), + })) + } else { + wants_client_cert.with_no_client_auth() + }; + config.alpn_protocols = context.alpn_protocols.clone(); + Ok((config, deferred_roots)) +} + +fn server_config(context: &Context) -> NativeResult { + if context.certified_keys.is_empty() { + return Err(( + 0, + "[SSL] server-side connection requires a certificate and private key".to_string(), + )); + } + let builder = rustls::ServerConfig::builder_with_provider(provider_for_context(context)) + .with_protocol_versions(enabled_versions(context)?) + .map_err(|error| (0, format!("[SSL] invalid TLS configuration: {error}")))?; + let wants_server_cert = if context.verify_mode == CERT_NONE || context.roots.is_empty() { + builder.with_no_client_auth() + } else { + let verifier = + rustls::server::WebPkiClientVerifier::builder(Arc::new(context.roots.clone())); + let verifier = if context.verify_mode == CERT_OPTIONAL { + verifier.allow_unauthenticated() + } else { + verifier + } + .build() + .map_err(|error| (0, format!("[SSL] cannot build client verifier: {error}")))?; + builder.with_client_cert_verifier(verifier) + }; + let mut config = wants_server_cert.with_cert_resolver(Arc::new(MultiCertResolver { + keys: context.certified_keys.clone(), + })); + config.alpn_protocols = context.alpn_protocols.clone(); + config.session_storage = context.server_session_store.clone(); + config.ticketer = context.server_ticketer.clone(); + Ok(config) +} + +fn enabled_versions( + context: &Context, +) -> NativeResult<&'static [&'static rustls::SupportedProtocolVersion]> { + static TLS12_ONLY: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS12]; + static TLS13_ONLY: &[&rustls::SupportedProtocolVersion] = &[&rustls::version::TLS13]; + let minimum = context.minimum_version; + let maximum = context.maximum_version; + let tls12 = (minimum < 0 || minimum <= 0x303) + && (maximum < 0 || maximum >= 0x303) + && context.options & 0x0800_0000 == 0; + let tls13 = (minimum < 0 || minimum <= 0x304) + && (maximum < 0 || maximum >= 0x304) + && context.options & 0x2000_0000 == 0; + match (tls12, tls13) { + (true, true) => Ok(rustls::DEFAULT_VERSIONS), + (true, false) => Ok(TLS12_ONLY), + (false, true) => Ok(TLS13_ONLY), + (false, false) => Err((0, "[SSL] no protocols available".to_string())), + } +} + +pub const TLS_ERROR_SSL: i32 = 1; +pub const TLS_ERROR_WANT_READ: i32 = 2; +pub const TLS_ERROR_WANT_WRITE: i32 = 3; +pub const TLS_ERROR_ZERO_RETURN: i32 = 6; +pub const TLS_ERROR_EOF: i32 = 8; +/// Internal discriminator carrying an OpenSSL-compatible X509 verification +/// code to the interpreter without changing Python's public `errno` (which +/// remains SSL_ERROR_SSL == 1). +pub const TLS_ERROR_CERT_VERIFY_BASE: i32 = 1_000; + +pub type TlsResult = Result; + +/// One protocol-level event for CPython's private `_msg_callback` hook. +/// Record framing stays in the TLS engine; the interpreter only turns these +/// inert values into Python callback arguments. +pub struct TlsMessageEvent { + pub write: bool, + pub version: u16, + pub content_type: u16, + pub message_type: u16, + pub data: Vec, +} + +#[derive(Default)] +struct TlsRecordObserver { + records: Vec, + handshakes: Vec, + encrypted: bool, +} + +impl TlsRecordObserver { + fn observe(&mut self, bytes: &[u8], write: bool, events: &mut Vec) { + self.records.extend_from_slice(bytes); + loop { + if self.records.len() < 5 { + return; + } + let content_type = self.records[0]; + let version = u16::from_be_bytes([self.records[1], self.records[2]]); + let payload_len = u16::from_be_bytes([self.records[3], self.records[4]]) as usize; + if self.records.len() < 5 + payload_len { + return; + } + let record: Vec = self.records.drain(..5 + payload_len).collect(); + events.push(TlsMessageEvent { + write, + version, + content_type: 0x100, + message_type: content_type as u16, + data: record[..5].to_vec(), + }); + let payload = &record[5..]; + match content_type { + 20 => { + events.push(TlsMessageEvent { + write, + version, + content_type: 20, + message_type: 0x101, + data: payload.to_vec(), + }); + // In TLS 1.2, protocol messages following CCS are + // encrypted. TLS 1.3 uses application-data records for + // encrypted handshake traffic and never enters here. + self.encrypted = true; + self.handshakes.clear(); + } + 21 if payload.len() >= 2 => events.push(TlsMessageEvent { + write, + version, + content_type: 21, + message_type: payload[1] as u16, + data: payload.to_vec(), + }), + 22 if !self.encrypted => { + self.handshakes.extend_from_slice(payload); + loop { + if self.handshakes.len() < 4 { + break; + } + let message_len = ((self.handshakes[1] as usize) << 16) + | ((self.handshakes[2] as usize) << 8) + | self.handshakes[3] as usize; + if self.handshakes.len() < 4 + message_len { + break; + } + let message: Vec = self.handshakes.drain(..4 + message_len).collect(); + events.push(TlsMessageEvent { + write, + version, + content_type: 22, + message_type: message[0] as u16, + data: message, + }); + } + } + _ => {} + } + } + } +} + +fn rustls_error(error: impl std::fmt::Display) -> (i32, String) { + (TLS_ERROR_SSL, format!("[SSL] {error}")) +} + +#[allow(deprecated)] // rustls can still return the compatibility variant. +fn certificate_error_details(error: &rustls::CertificateError) -> (i32, &'static str) { + use rustls::CertificateError; + match error { + CertificateError::Expired | CertificateError::ExpiredContext { .. } => { + (10, "certificate has expired") + } + CertificateError::NotValidYet | CertificateError::NotValidYetContext { .. } => { + (9, "certificate is not yet valid") + } + CertificateError::Revoked => (23, "certificate revoked"), + CertificateError::UnknownIssuer => (20, "unable to get local issuer certificate"), + CertificateError::BadSignature => (7, "certificate signature failure"), + CertificateError::NotValidForName | CertificateError::NotValidForNameContext { .. } => { + (62, "hostname mismatch") + } + CertificateError::InvalidPurpose | CertificateError::InvalidPurposeContext { .. } => { + (26, "unsuitable certificate purpose") + } + CertificateError::BadEncoding => (5, "unable to decode certificate"), + CertificateError::UnhandledCriticalExtension => (34, "unhandled critical extension"), + CertificateError::UnknownRevocationStatus => (3, "unable to get certificate CRL"), + CertificateError::ExpiredRevocationList + | CertificateError::ExpiredRevocationListContext { .. } => (12, "CRL has expired"), + CertificateError::UnsupportedSignatureAlgorithm + | CertificateError::UnsupportedSignatureAlgorithmContext { .. } + | CertificateError::UnsupportedSignatureAlgorithmForPublicKeyContext { .. } => { + (7, "certificate signature failure") + } + CertificateError::InvalidOcspResponse => (50, "application verification failure"), + CertificateError::ApplicationVerificationFailure | CertificateError::Other(_) => { + (1, "certificate verify failed") + } + _ => (1, "certificate verify failed"), + } +} + +fn rustls_protocol_error(error: rustls::Error) -> (i32, String) { + if let rustls::Error::InvalidCertificate(certificate_error) = &error { + let (verify_code, verify_message) = certificate_error_details(certificate_error); + return ( + TLS_ERROR_CERT_VERIFY_BASE + verify_code, + format!("[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: {verify_message}"), + ); + } + if let rustls::Error::AlertReceived(alert) = &error { + let reason = match alert { + &rustls::AlertDescription::AccessDenied => "TLSV1_ALERT_ACCESS_DENIED", + &rustls::AlertDescription::HandshakeFailure => "SSLV3_ALERT_HANDSHAKE_FAILURE", + &rustls::AlertDescription::InternalError => "TLSV1_ALERT_INTERNAL_ERROR", + &rustls::AlertDescription::ProtocolVersion => "TLSV1_ALERT_PROTOCOL_VERSION", + &rustls::AlertDescription::UnknownCA => "TLSV1_ALERT_UNKNOWN_CA", + _ => "TLSV1_ALERT_UNKNOWN", + }; + return ( + TLS_ERROR_SSL, + format!("[SSL: {reason}] received fatal TLS alert"), + ); + } + if matches!( + error, + rustls::Error::PeerIncompatible(rustls::PeerIncompatible::NoCipherSuitesInCommon) + ) { + return ( + TLS_ERROR_SSL, + "[SSL: NO_SHARED_CIPHER] no shared cipher".to_string(), + ); + } + rustls_error(error) +} + +#[inline(never)] +pub fn certificate_verify_message(code: i32) -> &'static str { + match code { + 10 => "certificate has expired", + 9 => "certificate is not yet valid", + 23 => "certificate revoked", + 20 => "unable to get local issuer certificate", + 7 => "certificate signature failure", + 62 => "hostname mismatch", + 26 => "unsuitable certificate purpose", + 5 => "unable to decode certificate", + 34 => "unhandled critical extension", + 3 => "unable to get certificate CRL", + 12 => "CRL has expired", + 50 => "application verification failure", + _ => "certificate verify failed", + } +} + +pub struct TlsConnection { + inner: Option, + acceptor: Option, + accepted: Option, + pending_tls: Vec, + pending_tls_start: usize, + deferred_roots: Vec>, + verified_deferred_root: Option>, + incoming_observer: TlsRecordObserver, + outgoing_observer: TlsRecordObserver, + message_events: Vec, + pending_received_tls: Vec, + pending_received_tls_start: usize, + client_config: Option>, + client_session_store: Option>, + context_identity: usize, + server_context: *const Context, + server_hit_counted: bool, +} + +impl TlsConnection { + fn active_mut(&mut self) -> TlsResult<&mut rustls::Connection> { + self.inner.as_mut().ok_or_else(|| { + ( + TLS_ERROR_WANT_READ, + "TLS server handshake is waiting for ClientHello configuration".to_string(), + ) + }) + } + + fn fill_pending_tls(&mut self) -> TlsResult<()> { + if self.pending_tls_start == self.pending_tls.len() { + self.pending_tls.clear(); + self.pending_tls_start = 0; + } + let Some(inner) = self.inner.as_mut() else { + return Ok(()); + }; + while inner.wants_write() { + let before = self.pending_tls.len(); + inner + .write_tls(&mut self.pending_tls) + .map_err(rustls_error)?; + self.outgoing_observer.observe( + &self.pending_tls[before..], + true, + &mut self.message_events, + ); + if self.pending_tls.len() == before { + break; + } + } + Ok(()) + } + + fn compact_received_tls(&mut self) { + if self.pending_received_tls_start == self.pending_received_tls.len() { + self.pending_received_tls.clear(); + self.pending_received_tls_start = 0; + } else if self.pending_received_tls_start >= 16 * 1024 { + self.pending_received_tls + .drain(..self.pending_received_tls_start); + self.pending_received_tls_start = 0; + } + } + + /// Feed as much queued ciphertext as rustls can currently accept. Its + /// plaintext queue intentionally applies backpressure at 16 KiB, so an + /// unread tail remains owned by this connection until Python drains data. + fn process_received_tls(&mut self) -> TlsResult<()> { + loop { + if self.pending_received_tls_start == self.pending_received_tls.len() { + self.compact_received_tls(); + return Ok(()); + } + let Some(inner) = self.inner.as_mut() else { + return Ok(()); + }; + let mut cursor = + Cursor::new(&self.pending_received_tls[self.pending_received_tls_start..]); + let read = match inner.read_tls(&mut cursor) { + Ok(0) => return Ok(()), + Ok(read) => read, + Err(error) + if error.kind() == std::io::ErrorKind::Other + && error.to_string() == "received plaintext buffer full" => + { + return Ok(()); + } + Err(error) => return Err(rustls_error(error)), + }; + self.pending_received_tls_start += read; + inner.process_new_packets().map_err(rustls_protocol_error)?; + self.note_server_resumption(); + self.compact_received_tls(); + } + } + + fn note_server_resumption(&mut self) { + if self.server_hit_counted || self.server_context.is_null() { + return; + } + let Some(inner) = self.inner.as_ref() else { + return; + }; + if inner.is_handshaking() { + return; + } + if inner.handshake_kind() == Some(rustls::HandshakeKind::Resumed) { + unsafe { &*self.server_context } + .session_hits + .fetch_add(1, Ordering::Relaxed); + } + self.server_hit_counted = true; + } + + fn process_acceptor_tls(&mut self) -> TlsResult<()> { + loop { + if self.pending_received_tls_start == self.pending_received_tls.len() { + self.compact_received_tls(); + return Ok(()); + } + let acceptor = self.acceptor.as_mut().ok_or_else(|| { + ( + TLS_ERROR_SSL, + "[SSL] server acceptor is no longer available".to_string(), + ) + })?; + let mut cursor = + Cursor::new(&self.pending_received_tls[self.pending_received_tls_start..]); + let read = acceptor.read_tls(&mut cursor).map_err(rustls_error)?; + if read == 0 { + return Ok(()); + } + self.pending_received_tls_start += read; + match acceptor.accept() { + Ok(Some(accepted)) => { + self.accepted = Some(accepted); + self.acceptor = None; + self.compact_received_tls(); + return Ok(()); + } + Ok(None) => self.compact_received_tls(), + Err((error, mut alert)) => { + let before = self.pending_tls.len(); + let _ = alert.write_all(&mut self.pending_tls); + self.outgoing_observer.observe( + &self.pending_tls[before..], + true, + &mut self.message_events, + ); + self.acceptor = None; + return Err(rustls_protocol_error(error)); + } + } + } + } +} + +/// Create one rustls state machine. It has no knowledge of Python sockets or +/// BIO objects; the interpreter owns transport policy and explicitly moves TLS +/// records through the primitive functions below. +#[inline(never)] +pub unsafe fn connection_new( + context: *const Context, + server_side: bool, + server_hostname: Option<&str>, + session: *const NativeSession, +) -> NativeResult<*mut TlsConnection> { + ensure_provider(); + let context = unsafe { &*context }; + if server_side && context.protocol == PROTOCOL_TLS_CLIENT { + return Err(( + 0, + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context".to_string(), + )); + } + if !server_side && context.protocol == PROTOCOL_TLS_SERVER { + return Err(( + 0, + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context".to_string(), + )); + } + let mut retained_client_config = None; + let mut retained_client_store = None; + let (connection, acceptor, deferred_roots) = if server_side { + (None, Some(rustls::server::Acceptor::default()), Vec::new()) + } else { + let name = match server_hostname { + Some(hostname) => rustls::pki_types::ServerName::try_from(hostname.to_string()) + .map_err(|error| (0, format!("invalid server hostname: {error}")))?, + None => rustls::pki_types::ServerName::IpAddress( + std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST).into(), + ), + }; + let store = Arc::new(CapturingClientSessionStore::new()); + let (mut config, deferred_roots) = if session.is_null() { + client_config(context)? + } else { + let session = unsafe { &*session }; + if session.context_identity != context as *const Context as usize { + return Err((0, "Session refers to a different SSLContext.".to_string())); + } + if session.server_name == name { + store.seed(session); + } + ((*session.config).clone(), Vec::new()) + }; + config.resumption = rustls::client::Resumption::store(store.clone()); + let config = Arc::new(config); + retained_client_config = Some(config.clone()); + retained_client_store = Some(store); + ( + Some(rustls::Connection::Client( + rustls::ClientConnection::new(config, name).map_err(rustls_error)?, + )), + None, + deferred_roots, + ) + }; + Ok(Box::into_raw(Box::new(TlsConnection { + inner: connection, + acceptor, + accepted: None, + pending_tls: Vec::new(), + pending_tls_start: 0, + deferred_roots, + verified_deferred_root: None, + incoming_observer: TlsRecordObserver::default(), + outgoing_observer: TlsRecordObserver::default(), + message_events: Vec::new(), + pending_received_tls: Vec::new(), + pending_received_tls_start: 0, + client_config: retained_client_config, + client_session_store: retained_client_store, + context_identity: context as *const Context as usize, + server_context: std::ptr::null(), + server_hit_counted: false, + }))) +} + +/// # Safety +/// `connection` must be null or a live pointer returned by `connection_new`. +#[inline(never)] +pub unsafe fn connection_free(connection: *mut TlsConnection) { + if !connection.is_null() { + unsafe { drop(Box::from_raw(connection)) }; + } +} + +/// Feed encrypted TLS records into rustls and process every complete packet. +/// Returns the number of bytes accepted from `data`. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_receive_tls( + connection: *mut TlsConnection, + data: &[u8], +) -> TlsResult { + let connection = unsafe { &mut *connection }; + connection + .incoming_observer + .observe(data, false, &mut connection.message_events); + connection.pending_received_tls.extend_from_slice(data); + if connection.inner.is_none() { + if connection.accepted.is_some() { + return Ok(data.len()); + } + connection.process_acceptor_tls()?; + return Ok(data.len()); + } + + let was_handshaking = connection + .inner + .as_ref() + .is_some_and(|inner| inner.is_handshaking()); + connection.process_received_tls()?; + let inner = connection.inner.as_ref().expect("active connection"); + if was_handshaking && !inner.is_handshaking() { + connection.verified_deferred_root = + matching_deferred_root(inner, &connection.deferred_roots); + connection.deferred_roots.clear(); + } + Ok(data.len()) +} + +/// Whether a server-side connection has parsed ClientHello and is waiting for +/// the interpreter to run the Python SNI callback and choose a context. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_waiting_for_server_config(connection: *const TlsConnection) -> bool { + unsafe { (&*connection).accepted.is_some() } +} + +/// The SNI DNS name from the accepted ClientHello. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_server_name(connection: *const TlsConnection) -> Option { + unsafe { (&*connection).accepted.as_ref() } + .and_then(|accepted| accepted.client_hello().server_name().map(ToOwned::to_owned)) +} + +/// Resume an accepted server ClientHello with the context selected by the +/// Python SNI callback. +/// +/// # Safety +/// Both pointers must refer to live native values. +#[inline(never)] +pub unsafe fn connection_accept_server( + connection: *mut TlsConnection, + context: *const Context, +) -> TlsResult<()> { + let connection = unsafe { &mut *connection }; + let accepted = connection.accepted.take().ok_or_else(|| { + ( + TLS_ERROR_SSL, + "[SSL] no accepted ClientHello is waiting for configuration".to_string(), + ) + })?; + let mut config = server_config(unsafe { &*context })?; + if !config.alpn_protocols.is_empty() + && let Some(offered) = accepted.client_hello().alpn() + && !offered + .into_iter() + .any(|protocol| config.alpn_protocols.iter().any(|known| known == protocol)) + { + // OpenSSL completes a handshake without ALPN when both peers offered + // ALPN but their lists are disjoint. rustls defaults to a fatal + // no_application_protocol alert, so clear only this connection's + // immutable config in the disjoint case. + config.alpn_protocols.clear(); + } + let config = Arc::new(config); + match accepted.into_connection(config) { + Ok(server) => { + connection.inner = Some(rustls::Connection::Server(server)); + connection.server_context = context; + unsafe { &*context } + .accept_count + .fetch_add(1, Ordering::Relaxed); + connection.process_received_tls() + } + Err((error, mut alert)) => { + let _ = alert.write_all(&mut connection.pending_tls); + Err(rustls_protocol_error(error)) + } + } +} + +/// Reject an accepted ClientHello with a caller-selected fatal TLS alert. +/// Alerts at this stage are plaintext TLS records by protocol definition. +/// +/// # Safety +/// `connection` must point to a live connection waiting for server config. +#[inline(never)] +pub unsafe fn connection_reject_server( + connection: *mut TlsConnection, + alert_description: u8, +) -> TlsResult<()> { + let connection = unsafe { &mut *connection }; + if connection.accepted.take().is_none() { + return Err(( + TLS_ERROR_SSL, + "[SSL] no accepted ClientHello is waiting for rejection".to_string(), + )); + } + let alert = [21, 3, 3, 0, 2, 2, alert_description]; + connection.pending_tls.extend_from_slice(&alert); + connection + .outgoing_observer + .observe(&alert, true, &mut connection.message_events); + Ok(()) +} + +/// Drain protocol events observed since the previous call. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_take_message_events( + connection: *mut TlsConnection, +) -> Vec { + std::mem::take(unsafe { &mut (&mut *connection).message_events }) +} + +fn matching_deferred_root( + connection: &rustls::Connection, + candidates: &[Vec], +) -> Option> { + let peer_chain = connection.peer_certificates()?; + let tail = peer_chain.last()?; + let (_, tail) = x509_parser::parse_x509_certificate(tail.as_ref()).ok()?; + candidates.iter().find_map(|candidate| { + let (_, root) = x509_parser::parse_x509_certificate(candidate).ok()?; + (tail.issuer() == root.subject()).then(|| candidate.clone()) + }) +} + +/// Return, once, the trust anchor selected from an OpenSSL-style lazy CA +/// directory after a successful client handshake. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_take_verified_root(connection: *mut TlsConnection) -> Option> { + unsafe { (&mut *connection).verified_deferred_root.take() } +} + +/// Drain all currently generated encrypted TLS records. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_take_tls(connection: *mut TlsConnection) -> TlsResult> { + let connection = unsafe { &mut *connection }; + connection.fill_pending_tls()?; + let output = connection.pending_tls[connection.pending_tls_start..].to_vec(); + connection.pending_tls.clear(); + connection.pending_tls_start = 0; + Ok(output) +} + +/// Copy, but do not consume, generated TLS records. Socket transports use +/// this together with `connection_consume_tls` so partial non-blocking sends +/// cannot lose bytes already drained from rustls. +#[inline(never)] +pub unsafe fn connection_peek_tls(connection: *mut TlsConnection) -> TlsResult> { + let connection = unsafe { &mut *connection }; + connection.fill_pending_tls()?; + Ok(connection.pending_tls[connection.pending_tls_start..].to_vec()) +} + +#[inline(never)] +pub unsafe fn connection_consume_tls(connection: *mut TlsConnection, count: usize) { + let connection = unsafe { &mut *connection }; + connection.pending_tls_start = + (connection.pending_tls_start + count).min(connection.pending_tls.len()); +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_is_handshaking(connection: *const TlsConnection) -> bool { + unsafe { (&*connection).inner.as_ref() }.is_none_or(|inner| inner.is_handshaking()) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_wants_read(connection: *const TlsConnection) -> bool { + let connection = unsafe { &*connection }; + connection + .inner + .as_ref() + .map_or(connection.acceptor.is_some(), |inner| inner.wants_read()) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_write_plain( + connection: *mut TlsConnection, + data: &[u8], +) -> TlsResult { + use std::io::Write; + unsafe { (&mut *connection).active_mut()? } + .writer() + .write(data) + .map_err(rustls_error) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_read_plain( + connection: *mut TlsConnection, + size: usize, +) -> TlsResult> { + use std::io::Read; + let connection = unsafe { &mut *connection }; + connection.process_received_tls()?; + let mut output = vec![0; size]; + match connection.active_mut()?.reader().read(&mut output) { + // A clean close_notify is EOF at the Python stream layer. CPython's + // SSL_read wrapper returns b"" here; SSLZeroReturnError is reserved + // for lower-level error reporting paths, not ordinary recv(). + Ok(0) => Ok(Vec::new()), + Ok(read) => { + output.truncate(read); + Ok(output) + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => Err(( + TLS_ERROR_WANT_READ, + "The operation did not complete (read)".to_string(), + )), + Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => Err(( + TLS_ERROR_EOF, + "EOF occurred in violation of protocol".to_string(), + )), + Err(error) => Err(rustls_error(error)), + } +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_send_close_notify(connection: *mut TlsConnection) { + if let Some(inner) = unsafe { (&mut *connection).inner.as_mut() } { + inner.send_close_notify(); + } +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_pending_plaintext(connection: *mut TlsConnection) -> usize { + unsafe { (&mut *connection).inner.as_mut() } + .and_then(|inner| inner.process_new_packets().ok()) + .map(|state| state.plaintext_bytes_to_read()) + .unwrap_or(0) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_peer_closed(connection: *mut TlsConnection) -> bool { + unsafe { (&mut *connection).inner.as_mut() } + .and_then(|inner| inner.process_new_packets().ok()) + .map(|state| state.peer_has_closed()) + .unwrap_or(false) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_alpn(connection: *const TlsConnection) -> Option> { + unsafe { (&*connection).inner.as_ref() } + .and_then(|inner| inner.alpn_protocol()) + .map(ToOwned::to_owned) +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_version(connection: *const TlsConnection) -> Option<&'static str> { + match unsafe { (&*connection).inner.as_ref() }.and_then(|inner| inner.protocol_version()) { + Some(rustls::ProtocolVersion::TLSv1_2) => Some("TLSv1.2"), + Some(rustls::ProtocolVersion::TLSv1_3) => Some("TLSv1.3"), + _ => None, + } +} + +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_peer_certificate(connection: *const TlsConnection) -> Option> { + unsafe { (&*connection).inner.as_ref() } + .and_then(|inner| inner.peer_certificates()) + .and_then(|certs| certs.first()) + .map(|cert| cert.as_ref().to_vec()) +} + +#[inline(never)] +pub unsafe fn connection_session_reused(connection: *const TlsConnection) -> Option { + let inner = unsafe { (&*connection).inner.as_ref() }?; + if inner.is_handshaking() { + return None; + } + Some(inner.handshake_kind() == Some(rustls::HandshakeKind::Resumed)) +} + +/// Snapshot the actual opaque rustls TLS 1.2 resumption value captured for +/// this connection. The returned session keeps its ClientConfig alive so the +/// verifier/credential identity checks performed by rustls remain valid. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_session(connection: *const TlsConnection) -> *mut NativeSession { + let connection = unsafe { &*connection }; + let Some(inner) = connection.inner.as_ref() else { + return std::ptr::null_mut(); + }; + if inner.is_handshaking() || inner.protocol_version() != Some(rustls::ProtocolVersion::TLSv1_2) + { + return std::ptr::null_mut(); + } + let (Some(store), Some(config)) = ( + connection.client_session_store.as_ref(), + connection.client_config.as_ref(), + ) else { + return std::ptr::null_mut(); + }; + store + .snapshot(connection.context_identity, config.clone()) + .map(|session| Box::into_raw(Box::new(session))) + .unwrap_or(std::ptr::null_mut()) +} + +/// Return stable channel-binding bytes shared by both peers for TLS 1.2. +/// rustls does not expose the Finished verify_data used by RFC 5929's +/// historical `tls-unique` construction, so use its standard exporter with a +/// private compatibility label. This preserves the binding's required peer +/// equality and per-session uniqueness without exposing key material. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_tls_unique(connection: *const TlsConnection) -> Option> { + let inner = unsafe { (&*connection).inner.as_ref() }?; + if inner.is_handshaking() || inner.protocol_version()? != rustls::ProtocolVersion::TLSv1_2 { + return None; + } + let mut output = vec![0u8; 12]; + inner + .export_keying_material(&mut output, b"EXPORTER-pyre-tls-unique", None) + .ok()?; + Some(output) +} + +fn openssl_cipher_name(suite: rustls::SupportedCipherSuite) -> String { + let name = format!("{:?}", suite.suite()); + if suite.tls13().is_some() { + // rustls' enum uses a `TLS13_` disambiguator while IANA/OpenSSL call + // these suites `TLS_AES_*` and `TLS_CHACHA20_*`. + return name + .strip_prefix("TLS13_") + .map(|suffix| format!("TLS_{suffix}")) + .unwrap_or(name); + } + name.strip_prefix("TLS_") + .unwrap_or(&name) + .replace("_WITH_", "-") + .replace("AES_128", "AES128") + .replace("AES_256", "AES256") + .replace('_', "-") +} + +/// Negotiated OpenSSL-style cipher name and effective key size. +/// +/// # Safety +/// `connection` must point to a live connection. +#[inline(never)] +pub unsafe fn connection_cipher(connection: *const TlsConnection) -> Option<(String, i32)> { + let suite = unsafe { (&*connection).inner.as_ref() }?.negotiated_cipher_suite()?; + let name = openssl_cipher_name(suite); + let bits = if name.contains("AES128") || name.contains("AES_128") { + 128 + } else { + 256 + }; + Some((name, bits)) +} diff --git a/pyre/pyre-object/src/pyobject.rs b/pyre/pyre-object/src/pyobject.rs index 6378d5d7d65..efd16c10b0d 100644 --- a/pyre/pyre-object/src/pyobject.rs +++ b/pyre/pyre-object/src/pyobject.rs @@ -656,6 +656,19 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ // on wasm32, so its object-hierarchy slot is too. #[cfg(not(target_arch = "wasm32"))] (169, Some(0)), + // rustls `_ssl` context, MemoryBIO, and session native payloads. These + // extend the append-only native rclass tail; wasm omits the host TLS + // module and therefore the hierarchy entries as well. + #[cfg(not(target_arch = "wasm32"))] + (170, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (171, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (172, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (173, Some(0)), + #[cfg(not(target_arch = "wasm32"))] + (174, Some(0)), ]; /// Compute subclass IDs from [`SUBCLASS_RANGE_HIERARCHY`] and write every From 546bc429e060274e06079fe69505480a1f9eff9d Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 11:19:46 +0900 Subject: [PATCH 2/3] Honor explicit TLS trust anchors --- pyre/pyre-native/src/ssl.rs | 130 +++++++++++++++++++++++++++++++++++- 1 file changed, 129 insertions(+), 1 deletion(-) diff --git a/pyre/pyre-native/src/ssl.rs b/pyre/pyre-native/src/ssl.rs index 39cbb4f1e23..1a7e4e0dc47 100644 --- a/pyre/pyre-native/src/ssl.rs +++ b/pyre/pyre-native/src/ssl.rs @@ -1253,6 +1253,111 @@ struct PolicyServerVerifier { has_crl: bool, } +/// Preserve OpenSSL's explicit end-entity trust semantics on top of WebPKI. +/// +/// RustPython's `PartialChainVerifier` performs the same fallback: a +/// self-issued leaf explicitly loaded into the trust store is trusted without +/// `VERIFY_X509_PARTIAL_CHAIN`; a non-self-issued leaf additionally requires +/// that flag. The exact DER match is the trust decision, but certificate +/// time, purpose, and server-name policy still apply. +#[derive(Debug)] +struct ExplicitEndEntityVerifier { + inner: Arc, + trusted_der: Vec>, + verify_flags: i32, +} + +impl rustls::client::danger::ServerCertVerifier for ExplicitEndEntityVerifier { + fn verify_server_cert( + &self, + end_entity: &CertificateDer<'_>, + intermediates: &[CertificateDer<'_>], + server_name: &rustls::pki_types::ServerName<'_>, + ocsp_response: &[u8], + now: rustls::pki_types::UnixTime, + ) -> Result { + let original_error = match self.inner.verify_server_cert( + end_entity, + intermediates, + server_name, + ocsp_response, + now, + ) { + Ok(verified) => return Ok(verified), + Err(error) => error, + }; + + let exact_match = self + .trusted_der + .iter() + .any(|trusted| trusted.as_slice() == end_entity.as_ref()); + if !exact_match { + return Err(original_error); + } + + let (_, certificate) = + x509_parser::parse_x509_certificate(end_entity.as_ref()).map_err(|_| { + rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding) + })?; + let self_issued = certificate.subject() == certificate.issuer(); + const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000; + if !self_issued && self.verify_flags & VERIFY_X509_PARTIAL_CHAIN == 0 { + return Err(original_error); + } + + let now = i64::try_from(now.as_secs()).map_err(|_| { + rustls::Error::InvalidCertificate( + rustls::CertificateError::ApplicationVerificationFailure, + ) + })?; + if now < certificate.validity().not_before.timestamp() { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::NotValidYet, + )); + } + if now > certificate.validity().not_after.timestamp() { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::Expired, + )); + } + if certificate + .extended_key_usage() + .map_err(|_| rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding))? + .is_some_and(|usage| !usage.value.any && !usage.value.server_auth) + { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::InvalidPurpose, + )); + } + + let certificate = rustls::server::ParsedCertificate::try_from(end_entity)?; + rustls::client::verify_server_name(&certificate, server_name)?; + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls12_signature(message, cert, signature) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + signature: &rustls::DigitallySignedStruct, + ) -> Result { + self.inner.verify_tls13_signature(message, cert, signature) + } + + fn supported_verify_schemes(&self) -> Vec { + self.inner.supported_verify_schemes() + } +} + impl rustls::client::danger::ServerCertVerifier for PolicyServerVerifier { fn verify_server_cert( &self, @@ -1276,11 +1381,26 @@ impl rustls::client::danger::ServerCertVerifier for PolicyServerVerifier { .extensions() .iter() .any(|extension| extension.oid.to_id_string() == "2.5.29.35"); - if !has_aki { + if certificate.subject() != certificate.issuer() && !has_aki { return Err(rustls::Error::InvalidCertificate( rustls::CertificateError::ApplicationVerificationFailure, )); } + for intermediate in intermediates { + let (_, certificate) = x509_parser::parse_x509_certificate(intermediate.as_ref()) + .map_err(|_| { + rustls::Error::InvalidCertificate(rustls::CertificateError::BadEncoding) + })?; + let has_aki = certificate + .extensions() + .iter() + .any(|extension| extension.oid.to_id_string() == "2.5.29.35"); + if !has_aki { + return Err(rustls::Error::InvalidCertificate( + rustls::CertificateError::ApplicationVerificationFailure, + )); + } + } } self.inner .verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now) @@ -1720,6 +1840,14 @@ fn client_config(context: &Context) -> NativeResult<(rustls::ClientConfig, Vec = + Arc::new(ExplicitEndEntityVerifier { + inner: verifier, + trusted_der, + verify_flags: context.verify_flags, + }); let mut verifier: Arc = Arc::new(PolicyServerVerifier { inner: verifier, From 4462a84f3dc6ee9ac7ec9a026a1620025a99693a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 12 Aug 2026 19:09:12 +0900 Subject: [PATCH 3/3] Fix sandbox SSL JIT integration --- pyre/pyre-interpreter/src/lib.rs | 19 ++++++++++++++++ pyre/pyre-interpreter/src/module/_ssl/mod.rs | 4 +++- pyre/pyre-interpreter/src/typedef.rs | 8 +++---- pyre/pyre-jit/Cargo.toml | 4 ++++ pyre/pyre-jit/src/eval.rs | 18 +++++++-------- pyre/pyre-object/src/pyobject.rs | 23 +++++++++++++++----- pyre/pyrex/Cargo.toml | 2 +- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/pyre/pyre-interpreter/src/lib.rs b/pyre/pyre-interpreter/src/lib.rs index 356d1f71ebd..f6e15c33209 100644 --- a/pyre/pyre-interpreter/src/lib.rs +++ b/pyre/pyre-interpreter/src/lib.rs @@ -1140,6 +1140,25 @@ pub fn all_subclass_range_aliases() -> Vec &'static [(u32, Option)] { + let hierarchy = pyre_object::pyobject::SUBCLASS_RANGE_HIERARCHY; + #[cfg(all(not(target_arch = "wasm32"), feature = "sandbox"))] + { + const SSL_HIERARCHY_SLOTS: usize = 5; + &hierarchy[..hierarchy.len() - SSL_HIERARCHY_SLOTS] + } + #[cfg(not(all(not(target_arch = "wasm32"), feature = "sandbox")))] + { + hierarchy + } +} + // ── Print / stderr hooks for wasm (fd-1 / fd-2 capture) ── // // An embedder installs these to receive everything the interpreter writes to diff --git a/pyre/pyre-interpreter/src/module/_ssl/mod.rs b/pyre/pyre-interpreter/src/module/_ssl/mod.rs index 2dc769ea261..06d3490ec36 100644 --- a/pyre/pyre-interpreter/src/module/_ssl/mod.rs +++ b/pyre/pyre-interpreter/src/module/_ssl/mod.rs @@ -2242,7 +2242,9 @@ crate::py_module! { crate::builtins::exc_os_error_new, crate::builtins::lookup_exc_class("_ssl.SSLError").expect("SSLError installed"), ), - "OPENSSL_VERSION" => w_str_new("AWS-LC (rustls/0.23)"), + // OpenSSL-shaped compatibility fields are required by `ssl.py` and + // consumers such as urllib3, while the suffix names the real backend. + "OPENSSL_VERSION" => w_str_new("OpenSSL 3.0.0-compatible (AWS-LC/rustls 0.23)"), "OPENSSL_VERSION_NUMBER" => w_int_new(0x3000_0000), "OPENSSL_VERSION_INFO" => w_tuple_new(vec![w_int_new(3), w_int_new(0), w_int_new(0), w_int_new(0), w_int_new(15)]), "_OPENSSL_API_VERSION" => w_tuple_new(vec![w_int_new(3), w_int_new(0), w_int_new(0), w_int_new(0), w_int_new(15)]), diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index 2cbc4c38e41..d3f6efea5a3 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -274,10 +274,10 @@ pub fn init_typeobjects() { // aliases from a later redundant write. let object_aliases = pyre_object::pyobject::all_subclass_range_aliases(); let interpreter_aliases = crate::all_subclass_range_aliases(); - pyre_object::pyobject::compute_subclass_ranges_from(&[ - &object_aliases, - &interpreter_aliases, - ]); + pyre_object::pyobject::compute_subclass_ranges_from_hierarchy( + crate::active_subclass_range_hierarchy(), + &[&object_aliases, &interpreter_aliases], + ); pyre_object::pyobject::mark_subclass_ranges_initialized(); let mut reg: HashMap = HashMap::new(); diff --git a/pyre/pyre-jit/Cargo.toml b/pyre/pyre-jit/Cargo.toml index c5e10b65d04..933a031b5ae 100644 --- a/pyre/pyre-jit/Cargo.toml +++ b/pyre/pyre-jit/Cargo.toml @@ -21,6 +21,10 @@ mir-frontend = ["majit-translate/mir-frontend"] # exercised eagerly. majit-gc is compiled once per build, so this single # edge enables the stress mode for the whole pyre-jit dependency closure. gc_stress = ["majit-gc/gc_stress"] +# Keep JIT-side module references in lockstep with the interpreter's sandbox +# surface. In particular, native networking modules such as `_ssl` are +# compiled out of pyre-interpreter under this feature. +sandbox = ["pyre-interpreter/sandbox"] # Select the host binding for the wasm backend (only meaningful on # wasm32). `wasm-web` instantiates JIT traces through the browser # `WebAssembly` API; `wasm-host` through plain wasm imports a native diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index 6d685d6d61f..2dcffd2978e 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -690,28 +690,28 @@ unsafe fn hashlib_hmac_destructor(obj_addr: usize) { } } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn ssl_context_destructor(obj_addr: usize) { unsafe { pyre_interpreter::module::_ssl::w_ssl_context_dealloc(obj_addr as pyre_object::PyObjectRef) }; } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn memory_bio_destructor(obj_addr: usize) { unsafe { pyre_interpreter::module::_ssl::w_memory_bio_dealloc(obj_addr as pyre_object::PyObjectRef) }; } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn ssl_session_destructor(obj_addr: usize) { unsafe { pyre_interpreter::module::_ssl::w_ssl_session_dealloc(obj_addr as pyre_object::PyObjectRef) }; } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn ssl_socket_destructor(obj_addr: usize) { unsafe { pyre_interpreter::module::_ssl::w_ssl_socket_dealloc(obj_addr as pyre_object::PyObjectRef) @@ -775,7 +775,7 @@ unsafe fn random_object_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut maj /// `_ssl._SSLContext` has the native-layout mapdict prefix plus the three /// Python callback/path references owned by the context wrapper. -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn ssl_context_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { unsafe { object_object_custom_trace(obj_addr, f) }; let context = unsafe { &mut *(obj_addr as *mut pyre_interpreter::module::_ssl::W_SSLContext) }; @@ -786,14 +786,14 @@ unsafe fn ssl_context_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit /// `ssl.MemoryBIO` is subclassable and therefore carries mapdict storage even /// though its rustls transport state contains no Python references. -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn memory_bio_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { unsafe { object_object_custom_trace(obj_addr, f) }; } /// `_ssl._SSLSocket` owns its context, transport endpoints, cached unbound /// socket methods, public owner, and hostname directly on the typed object. -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] unsafe fn ssl_socket_custom_trace(obj_addr: usize, f: &mut dyn FnMut(*mut majit_ir::GcRef)) { let socket = unsafe { &mut *(obj_addr as *mut pyre_interpreter::module::_ssl::W_SSLSocket) }; f(std::ptr::addr_of_mut!(socket.ob.w_class) as *mut majit_ir::GcRef); @@ -3624,7 +3624,7 @@ fn build_gc() -> Box { // MemoryBIO are subclassable native layouts, so their marker walks the // mapdict prefix; Context additionally owns Python callbacks/path values. // Their sweep destructors release the opaque rustls allocations. - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "sandbox")))] { let context_descr = ::DESCRIPTOR; @@ -3970,7 +3970,7 @@ fn build_gc() -> Box { .collect(); assert_eq!( actual_hierarchy, - pyre_object::pyobject::SUBCLASS_RANGE_HIERARCHY, + pyre_interpreter::active_subclass_range_hierarchy(), "GC rclass.OBJECT registration order must match the shared subclass-range census", ); gc.freeze_types(); diff --git a/pyre/pyre-object/src/pyobject.rs b/pyre/pyre-object/src/pyobject.rs index efd16c10b0d..5741bbec4ba 100644 --- a/pyre/pyre-object/src/pyobject.rs +++ b/pyre/pyre-object/src/pyobject.rs @@ -658,7 +658,8 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ (169, Some(0)), // rustls `_ssl` context, MemoryBIO, and session native payloads. These // extend the append-only native rclass tail; wasm omits the host TLS - // module and therefore the hierarchy entries as well. + // module and therefore the hierarchy entries as well. Sandbox filtering + // belongs to pyre-interpreter, which owns that module configuration. #[cfg(not(target_arch = "wasm32"))] (170, Some(0)), #[cfg(not(target_arch = "wasm32"))] @@ -687,18 +688,21 @@ pub const SUBCLASS_RANGE_HIERARCHY: &[(u32, Option)] = &[ /// `ll_isinstance` call (typically from `init_typeobjects` on the /// interpreter side). The later GC writeback consumes the same hierarchy, /// so either writer leaves byte-identical ranges. -pub fn compute_subclass_ranges_from(alias_chains: &[&[SubclassRangeAlias]]) { +pub fn compute_subclass_ranges_from_hierarchy( + hierarchy: &[(u32, Option)], + alias_chains: &[&[SubclassRangeAlias]], +) { #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum WitnessElement { Cdef(u32), Max, } - let slots = SUBCLASS_RANGE_HIERARCHY + let slots = hierarchy .last() .map_or(0, |(type_id, _)| *type_id as usize + 1); let mut witnesses: Vec>> = vec![None; slots]; - for &(type_id, parent) in SUBCLASS_RANGE_HIERARCHY { + for &(type_id, parent) in hierarchy { let mut witness = match parent { Some(parent_id) => witnesses[parent_id as usize] .clone() @@ -716,8 +720,8 @@ pub fn compute_subclass_ranges_from(alias_chains: &[&[SubclassRangeAlias]]) { is_max: bool, } - let mut peers = Vec::with_capacity(SUBCLASS_RANGE_HIERARCHY.len() * 2); - for &(type_id, _) in SUBCLASS_RANGE_HIERARCHY { + let mut peers = Vec::with_capacity(hierarchy.len() * 2); + for &(type_id, _) in hierarchy { let witness = witnesses[type_id as usize] .as_ref() .expect("every subclass-range typeid must have a witness"); @@ -765,6 +769,13 @@ pub fn compute_subclass_ranges_from(alias_chains: &[&[SubclassRangeAlias]]) { } } +/// Compute ranges from the complete object-model census. Configuration-aware +/// embedders should call [`compute_subclass_ranges_from_hierarchy`] with their +/// active hierarchy instead. +pub fn compute_subclass_ranges_from(alias_chains: &[&[SubclassRangeAlias]]) { + compute_subclass_ranges_from_hierarchy(SUBCLASS_RANGE_HIERARCHY, alias_chains); +} + /// Lazy first-caller-wins gate around `compute_subclass_ranges_from`. /// Pyre's interpreter-side `init_typeobjects` passes both object and /// interpreter alias slices so cross-crate types (e.g. `CODE_TYPE`, diff --git a/pyre/pyrex/Cargo.toml b/pyre/pyrex/Cargo.toml index f59b5c773b0..83bd1be14c3 100644 --- a/pyre/pyrex/Cargo.toml +++ b/pyre/pyrex/Cargo.toml @@ -40,7 +40,7 @@ mimalloc = ["dep:mimalloc"] # gc_stress = ["pyre-jit/gc_stress"] # Build the interpreter as an RPython-style sandbox client: every mediated OS # call routes through host_seam's marshalling trampoline (see pyre-interpreter). -sandbox = ["pyre-interpreter/sandbox"] +sandbox = ["pyre-interpreter/sandbox", "pyre-jit/sandbox"] # Backwards-compatible alias. The seccomp-bpf syscall allowlist is now installed # by default in any Linux `sandbox` build (see the install site in lib.rs and # pyre-sandbox::seccomp), so this adds nothing over `sandbox`; it is retained so