security: harden tls ffi
ober
1d6dd8f7562bfffe60dedf4f123ce6d667374b2f
--- a/docs/ffi-audit.md +++ b/docs/ffi-audit.md @@ -40,10 +40,10 @@ Latest summary from `tools/ffi-audit-report.ss`: (rust-file-count 29) (c-file-count 5) (no-mangle-exports 196) - (unsafe-sites 395) - (unsafe-sites-with-nearby-safety-comment 299) - (unsafe-sites-without-nearby-safety-comment 96) - (unsafe-comment-review-sites 96) + (unsafe-sites 369) + (unsafe-sites-with-nearby-safety-comment 303) + (unsafe-sites-without-nearby-safety-comment 66) + (unsafe-comment-review-sites 66) (export-review-sites 196)) (vendor (jsqlite (path "vendor/jsqlite") (status accepted-risk-cve-gated))) @@ -149,9 +149,12 @@ inputs and output-buffer validation. `secure_fs.rs` now documents descriptor-relative `openat`/`mkdirat`, fd duplication/ownership transfer, stat buffers, directory stream lifecycle, caller output slots, atomic output publication, and test-owned fd conversion -invariants around its secure filesystem capability boundary. The generated -inventory now reports 299 annotated native unsafe sites and 96 remaining unsafe -review sites. +invariants around its secure filesystem capability boundary. `tls.rs` now +funnels hostname, path, PEM, pin, and read/write buffers through checked C ABI +helpers, rejects null/nonzero and oversized buffers before slicing, rejects +negative accepted fds before adopting them as `TcpStream`, and documents socket +shutdown invariants used to break blocked TLS I/O. The generated inventory now +reports 303 annotated native unsafe sites and 66 remaining unsafe review sites. Remaining work before closing K3-P1-01: --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -146,7 +146,7 @@ never pattern-based: and the unsafe pattern unrepresentable — that's most of P0/P1 below. - **The FFI boundary is our `unsafe`.** 91 files under `lib/std/` mention `foreign-procedure`; the current [FFI audit](ffi-audit.md) tracks 196 - native `#[no_mangle]` sites and 395 Rust unsafe sites. That boundary is + native `#[no_mangle]` sites and 369 Rust unsafe sites. That boundary is where a K3-class adversary will concentrate, because it is the only place memory-corruption patterns *exist* in a Jerboa application. [`Philosophy.md`](Philosophy.md) Principle 4 and open tension #5 both @@ -268,7 +268,7 @@ Keep this section to baseline facts only. | Files under `lib/std/` mentioning `foreign-procedure` | **91** | `grep -rl foreign-procedure lib/std \| wc -l` | | Native `#[no_mangle]` sites in `jerboa-native-rs/src` | **196** | `make ffi-audit-report` | | Actual native C ABI exports after cfg filtering | **190** | `make native-export-review-check` | -| Rust unsafe sites in `jerboa-native-rs/src` | **395** | `make ffi-audit-report` | +| Rust unsafe sites in `jerboa-native-rs/src` | **369** | `make ffi-audit-report` | | Fuzz harnesses | 13 | `ls tests/fuzz/harness` | | Checked-in fuzz seed corpora | **0** | `tests/fuzz/` contains only `harness/` | | Security test files | 21 | Appendix A | @@ -693,9 +693,11 @@ the current inventory and remediation trail now live in [ffi-audit.md](ffi-audit and wraps integer and double column getters in panic containment. `secure_fs.rs` now documents descriptor-relative open/create/stat/list/read syscalls, fd ownership transfer, caller output slots, atomic output - publication, and test-owned fd conversion invariants. The generated report - now shows 299 annotated native unsafe sites and 96 remaining unsafe review - sites. + publication, and test-owned fd conversion invariants. `tls.rs` now validates + hostname, path, PEM, pin, and read/write buffers through shared helpers, + rejects invalid accepted fds before ownership transfer, and documents socket + shutdown invariants for blocked TLS I/O. The generated report now shows 303 + annotated native unsafe sites and 66 remaining unsafe review sites. Remaining work: continue unsafe invariant comments across the rest of `jerboa-native-rs`. @@ -1185,7 +1187,7 @@ Track these in `docs/status.md` per release: | Confined worker exists | facade, audit lifecycle, output caps, deadline, process-group kill, memory rlimit pre-exec path, explicit sandbox-axis refusal, egress proxy env wiring, and Linux syscall/ptrace seccomp pre-exec setup landed; Landlock path/net and Seatbelt/Capsicum parity pending | yes, tested (P0-02) | | Unclassified raw `read`/FASL/`load` sites | scanner-driven manifest classification gate closed 2026-07-27; trusted build, compiler, REPL/dev, and test surfaces are centrally justified by exact source line | 0 | | FFI bindings audited | scanner inventory and provisional verdicts in [ffi-audit.md](ffi-audit.md); native exports gated and justified | 100% with final verdicts | -| Un-annotated Rust `unsafe` blocks | 96 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | +| Un-annotated Rust `unsafe` blocks | 66 unsafe review sites without nearby `SAFETY:` comments in [ffi-audit.md](ffi-audit.md) | 0 | | Fuzz corpora / crash regressions | 0 / 0 | per-parser corpora + every crash a test | | Exploit-shaped regression tests | ~0 | ≥ 1 per historical finding | | Sandboxed-by-default app entry | no | yes (P0-06) | @@ -1200,7 +1202,7 @@ Track these in `docs/status.md` per release: ```bash # Posture facts used in this document (re-run to refresh): grep -rl 'foreign-procedure' lib/std | wc -l # 91 -make ffi-audit-report # native no_mangle 196; unsafe 395 +make ffi-audit-report # native no_mangle 196; unsafe 369 make native-export-review-check # actual exports 190 ls tests/fuzz/harness # 13 harnesses ls vendor/ # ChezScheme, ChezScheme-patches, jsqlite --- a/docs/reviews/2026-07-27-native-export-review.sexp +++ b/docs/reviews/2026-07-27-native-export-review.sexp @@ -163,22 +163,22 @@ (export (symbol "jerboa_sqlite_reset") (file "jerboa-native-rs/src/sqlite.rs") (line 711) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 54)))) (export (symbol "jerboa_sqlite_step") (file "jerboa-native-rs/src/sqlite.rs") (line 514) (scheme-callers 1 (caller (file "lib/std/db/sqlite-native.ss") (line 68)))) (export (symbol "jerboa_timing_safe_equal") (file "jerboa-native-rs/src/crypto.rs") (line 310) (scheme-callers 1 (caller (file "lib/std/crypto/native-rust.ss") (line 131)))) - (export (symbol "jerboa_tls_accept") (file "jerboa-native-rs/src/tls.rs") (line 807) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 86)))) - (export (symbol "jerboa_tls_close") (file "jerboa-native-rs/src/tls.rs") (line 1675) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 144)))) - (export (symbol "jerboa_tls_connect") (file "jerboa-native-rs/src/tls.rs") (line 154) (scheme-callers 5 (caller (file "lib/std/net/tls-rustls.ss") (line 92)) (caller (file "lib/std/net/tls-rustls.ss") (line 98)) (caller (file "lib/std/net/tls-rustls.ss") (line 107)) (caller (file "lib/std/net/tls-rustls.ss") (line 114)) (caller (file "lib/std/net/tls-rustls.ss") (line 121)))) - (export (symbol "jerboa_tls_connect_addr_timeout") (file "jerboa-native-rs/src/tls.rs") (line 293) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 107)))) - (export (symbol "jerboa_tls_connect_mtls") (file "jerboa-native-rs/src/tls.rs") (line 1135) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 121)))) - (export (symbol "jerboa_tls_connect_pinned") (file "jerboa-native-rs/src/tls.rs") (line 407) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 114)))) - (export (symbol "jerboa_tls_connect_timeout") (file "jerboa-native-rs/src/tls.rs") (line 161) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 98)))) - (export (symbol "jerboa_tls_flush") (file "jerboa-native-rs/src/tls.rs") (line 1640) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 139)))) - (export (symbol "jerboa_tls_get_fd") (file "jerboa-native-rs/src/tls.rs") (line 1768) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 161)))) - (export (symbol "jerboa_tls_read") (file "jerboa-native-rs/src/tls.rs") (line 1576) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 129)))) - (export (symbol "jerboa_tls_server_free") (file "jerboa-native-rs/src/tls.rs") (line 1692) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 80)))) - (export (symbol "jerboa_tls_server_new") (file "jerboa-native-rs/src/tls.rs") (line 635) (scheme-callers 2 (caller (file "lib/std/net/tls-rustls.ss") (line 67)) (caller (file "lib/std/net/tls-rustls.ss") (line 74)))) - (export (symbol "jerboa_tls_server_new_mtls") (file "jerboa-native-rs/src/tls.rs") (line 975) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 74)))) - (export (symbol "jerboa_tls_set_nonblock") (file "jerboa-native-rs/src/tls.rs") (line 1699) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 150)))) - (export (symbol "jerboa_tls_set_timeout") (file "jerboa-native-rs/src/tls.rs") (line 1724) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 155)))) - (export (symbol "jerboa_tls_write") (file "jerboa-native-rs/src/tls.rs") (line 1609) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 134)))) + (export (symbol "jerboa_tls_accept") (file "jerboa-native-rs/src/tls.rs") (line 812) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 86)))) + (export (symbol "jerboa_tls_close") (file "jerboa-native-rs/src/tls.rs") (line 1644) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 144)))) + (export (symbol "jerboa_tls_connect") (file "jerboa-native-rs/src/tls.rs") (line 199) (scheme-callers 5 (caller (file "lib/std/net/tls-rustls.ss") (line 92)) (caller (file "lib/std/net/tls-rustls.ss") (line 98)) (caller (file "lib/std/net/tls-rustls.ss") (line 107)) (caller (file "lib/std/net/tls-rustls.ss") (line 114)) (caller (file "lib/std/net/tls-rustls.ss") (line 121)))) + (export (symbol "jerboa_tls_connect_addr_timeout") (file "jerboa-native-rs/src/tls.rs") (line 330) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 107)))) + (export (symbol "jerboa_tls_connect_mtls") (file "jerboa-native-rs/src/tls.rs") (line 1131) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 121)))) + (export (symbol "jerboa_tls_connect_pinned") (file "jerboa-native-rs/src/tls.rs") (line 431) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 114)))) + (export (symbol "jerboa_tls_connect_timeout") (file "jerboa-native-rs/src/tls.rs") (line 206) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 98)))) + (export (symbol "jerboa_tls_flush") (file "jerboa-native-rs/src/tls.rs") (line 1609) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 139)))) + (export (symbol "jerboa_tls_get_fd") (file "jerboa-native-rs/src/tls.rs") (line 1735) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 161)))) + (export (symbol "jerboa_tls_read") (file "jerboa-native-rs/src/tls.rs") (line 1545) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 129)))) + (export (symbol "jerboa_tls_server_free") (file "jerboa-native-rs/src/tls.rs") (line 1659) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 80)))) + (export (symbol "jerboa_tls_server_new") (file "jerboa-native-rs/src/tls.rs") (line 653) (scheme-callers 2 (caller (file "lib/std/net/tls-rustls.ss") (line 67)) (caller (file "lib/std/net/tls-rustls.ss") (line 74)))) + (export (symbol "jerboa_tls_server_new_mtls") (file "jerboa-native-rs/src/tls.rs") (line 990) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 74)))) + (export (symbol "jerboa_tls_set_nonblock") (file "jerboa-native-rs/src/tls.rs") (line 1666) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 150)))) + (export (symbol "jerboa_tls_set_timeout") (file "jerboa-native-rs/src/tls.rs") (line 1691) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 155)))) + (export (symbol "jerboa_tls_write") (file "jerboa-native-rs/src/tls.rs") (line 1578) (scheme-callers 1 (caller (file "lib/std/net/tls-rustls.ss") (line 134)))) (export (symbol "jerboa_wasm_add_fuel") (file "jerboa-native-rs/src/wasm.rs") (line 398) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 118)))) (export (symbol "jerboa_wasm_call") (file "jerboa-native-rs/src/wasm.rs") (line 452) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 94)))) (export (symbol "jerboa_wasm_fuel_remaining") (file "jerboa-native-rs/src/wasm.rs") (line 424) (scheme-callers 1 (caller (file "lib/std/wasm/sandbox.ss") (line 124)))) --- a/docs/status.md +++ b/docs/status.md @@ -25,7 +25,7 @@ release artifacts are built as Jerboa multicall binaries with `jerboa`, | Area | Current state | Remaining work | |---|---|---| | Kimi security handoff | [kimi3-security-recommmendations.md](kimi3-security-recommmendations.md) is the backlog. Dated evidence and review manifests live under [reviews/](reviews/). | Keep new security evidence in dated review records and summarize the current release state here. | -| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, secure filesystem capability boundary in `secure_fs.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, regex-native ABI, process-control ABI, pcap ABI, inotify ABI, epoll/eventfd ABI, antidebug ABI, SOCKS5 server ABI, PostgreSQL native ABI, Landlock native ABI, wasmi sandbox ABI, SpiderMonkey WASM ABI, DuckDB native ABI, X.509 native ABI, and SQLite native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants, with the generated unannotated unsafe-site count at 96. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | +| FFI audit phase 5 | [ffi-audit.md](ffi-audit.md) records the scanner output, provisional Scheme binding verdicts, and native Rust export inventory. `make native-export-review-check` gates native export decisions. The worker-launch native path in `aproc.rs`/`seccomp.rs`, crypto FFI buffer path in `crypto.rs`, secure-memory region lifecycle in `secure_mem.rs`, secure filesystem capability boundary in `secure_fs.rs`, TLS native ABI in `tls.rs`, Ed25519/X25519 key-agreement buffers, compression buffers, HTTP parse/writev boundary, embed-crypto ABI, integrity ABI, regex-native ABI, process-control ABI, pcap ABI, inotify ABI, epoll/eventfd ABI, antidebug ABI, SOCKS5 server ABI, PostgreSQL native ABI, Landlock native ABI, wasmi sandbox ABI, SpiderMonkey WASM ABI, DuckDB native ABI, X.509 native ABI, and SQLite native ABI now have nearby `SAFETY:` comments or equivalent checked FFI invariants, with the generated unannotated unsafe-site count at 66. | Continue adding `SAFETY:` invariant comments near the remaining Rust unsafe sites. | | Native Rust exports | The native export review now has 190 exported functions: 183 tracked Scheme references and 7 retained standalone C/binary helpers. The previous 35 no-Scheme-reference removal candidates no longer have C ABI export markers. | Re-run `make native-export-review-check` whenever adding or removing native exports. | | Confined worker | `(std security worker)` provides the facade, audit lifecycle, output caps, deadlines, process-group kill, memory rlimit pre-exec setup, Linux syscall/ptrace seccomp pre-exec setup for requested axes, explicit sandbox-axis refusal, and egress proxy env wiring. | Install native Landlock path/net rules in the worker pre-exec path and keep Linux/macOS/FreeBSD parity tests current. | | Safe surface | Direct scripts default to the safe prelude; raw access requires `--unsafe-prelude` or `(jerboa prelude unsafe)`. | Continue moving risky APIs behind explicit unsafe imports as new modules land. | --- a/jerboa-native-rs/src/tls.rs +++ b/jerboa-native-rs/src/tls.rs @@ -11,6 +11,7 @@ use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerNam use rustls::{ClientConfig, ClientConnection, ServerConfig, ServerConnection, StreamOwned}; use crate::panic::{ffi_wrap, set_last_error}; +const MAX_C_ABI_SLICE_LEN: usize = isize::MAX as usize; // ============================================================ // Connection handle management @@ -84,6 +85,50 @@ fn next_handle() -> u64 { NEXT_HANDLE.fetch_add(1, std::sync::atomic::Ordering::Relaxed) } +fn ffi_bytes<'a>(ptr: *const u8, len: usize, label: &str) -> Result<&'a [u8], u64> { + if ptr.is_null() { + if len == 0 { + return Ok(&[]); + } + set_last_error(format!("null {label} pointer with nonzero length")); + return Err(0); + } + if len > MAX_C_ABI_SLICE_LEN { + set_last_error(format!("{label} length exceeds Rust slice limit")); + return Err(0); + } + // SAFETY: The C ABI caller supplies a readable buffer for the duration of + // this call; null nonempty and oversized buffers are rejected above. + Ok(unsafe { std::slice::from_raw_parts(ptr, len) }) +} + +fn ffi_utf8<'a>(ptr: *const u8, len: usize, label: &str) -> Result<&'a str, u64> { + match std::str::from_utf8(ffi_bytes(ptr, len, label)?) { + Ok(value) => Ok(value), + Err(_) => { + set_last_error(format!("invalid UTF-8 {label}")); + Err(0) + } + } +} + +fn ffi_output_mut<'a>(ptr: *mut u8, len: usize, label: &str) -> Result<&'a mut [u8], i32> { + if ptr.is_null() { + if len == 0 { + return Ok(&mut []); + } + set_last_error(format!("null {label} pointer with nonzero length")); + return Err(-1); + } + if len > MAX_C_ABI_SLICE_LEN { + set_last_error(format!("{label} length exceeds Rust slice limit")); + return Err(-1); + } + // SAFETY: The C ABI caller supplies a writable buffer for the duration of + // this call; null nonempty and oversized buffers are rejected above. + Ok(unsafe { std::slice::from_raw_parts_mut(ptr, len) }) +} + fn parse_cert_pem_reader<R: std::io::Read>( reader: R, empty_msg: &str, @@ -165,17 +210,9 @@ pub extern "C" fn jerboa_tls_connect_timeout( timeout_ms: u64, ) -> u64 { match std::panic::catch_unwind(|| { - if host.is_null() { - set_last_error("null host".to_string()); - return 0; - } - let host_str = unsafe { std::str::from_utf8(std::slice::from_raw_parts(host, host_len)) }; - let host_str = match host_str { - Ok(s) => s, - Err(_) => { - set_last_error("invalid UTF-8 hostname".to_string()); - return 0; - } + let host_str = match ffi_utf8(host, host_len, "TLS hostname") { + Ok(value) => value, + Err(code) => return code, }; // Build client config with webpki root certificates @@ -299,27 +336,14 @@ pub extern "C" fn jerboa_tls_connect_addr_timeout( timeout_ms: u64, ) -> u64 { match std::panic::catch_unwind(|| { - if server_host.is_null() || connect_host.is_null() { - set_last_error("null TLS server or connect host".to_string()); - return 0; - } - let server_host_str = match unsafe { - std::str::from_utf8(std::slice::from_raw_parts(server_host, server_host_len)) - } { + let server_host_str = match ffi_utf8(server_host, server_host_len, "TLS server name") { Ok(value) => value, - Err(_) => { - set_last_error("invalid UTF-8 TLS server name".to_string()); - return 0; - } + Err(code) => return code, }; - let connect_host_str = match unsafe { - std::str::from_utf8(std::slice::from_raw_parts(connect_host, connect_host_len)) - } { + let connect_host_str = match ffi_utf8(connect_host, connect_host_len, "TLS connect address") + { Ok(value) => value, - Err(_) => { - set_last_error("invalid UTF-8 connect address".to_string()); - return 0; - } + Err(code) => return code, }; let ip = match IpAddr::from_str(connect_host_str) { Ok(value) => value, @@ -412,25 +436,19 @@ pub extern "C" fn jerboa_tls_connect_pinned( pin_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if host.is_null() { - set_last_error("null host".to_string()); - return 0; - } - let host_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(host, host_len)) { - Ok(s) => s.to_string(), - Err(_) => { - set_last_error("invalid UTF-8 hostname".to_string()); - return 0; - } - } + let host_str = match ffi_utf8(host, host_len, "TLS hostname") { + Ok(value) => value.to_string(), + Err(code) => return code, }; - if pin_sha256.is_null() || pin_len != 32 { + if pin_len != 32 { set_last_error("certificate pin must be exactly 32 bytes".to_string()); return 0; } - let expected_pin = unsafe { std::slice::from_raw_parts(pin_sha256, pin_len) }.to_vec(); + let expected_pin = match ffi_bytes(pin_sha256, pin_len, "certificate pin") { + Ok(value) => value.to_vec(), + Err(code) => return code, + }; // Build config that skips CA verification (we verify via pin) let config = ClientConfig::builder() @@ -639,27 +657,13 @@ pub extern "C" fn jerboa_tls_server_new( key_path_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if cert_path.is_null() || key_path.is_null() { - set_last_error("null cert/key path".to_string()); - return 0; - } - let cert_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(cert_path, cert_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid cert path".to_string()); - return 0; - } - } + let cert_str = match ffi_utf8(cert_path, cert_path_len, "TLS cert path") { + Ok(value) => value, + Err(code) => return code, }; - let key_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(key_path, key_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid key path".to_string()); - return 0; - } - } + let key_str = match ffi_utf8(key_path, key_path_len, "TLS key path") { + Ok(value) => value, + Err(code) => return code, }; // Read cert chain @@ -740,13 +744,14 @@ pub extern "C" fn jerboa_tls_server_new_pem( key_pem_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if cert_pem.is_null() || key_pem.is_null() { - set_last_error("null cert/key PEM pointer".to_string()); - return 0; - } - - let cert_data = unsafe { std::slice::from_raw_parts(cert_pem, cert_pem_len) }; - let key_data = unsafe { std::slice::from_raw_parts(key_pem, key_pem_len) }; + let cert_data = match ffi_bytes(cert_pem, cert_pem_len, "TLS cert PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let key_data = match ffi_bytes(key_pem, key_pem_len, "TLS key PEM") { + Ok(value) => value, + Err(code) => return code, + }; let certs = match parse_cert_pem_slice( cert_data, @@ -806,6 +811,10 @@ pub extern "C" fn jerboa_tls_server_new_pem( #[no_mangle] pub extern "C" fn jerboa_tls_accept(server_ctx: u64, fd: i32) -> u64 { match std::panic::catch_unwind(|| { + if fd < 0 { + set_last_error("invalid accepted TCP fd".to_string()); + return 0; + } let config = { let ctxs = lock_unpoisoned(server_ctxs()); match ctxs.get(&server_ctx) { @@ -827,6 +836,8 @@ pub extern "C" fn jerboa_tls_accept(server_ctx: u64, fd: i32) -> u64 { // Wrap the raw fd in a TcpStream use std::os::unix::io::FromRawFd; + // SAFETY: the caller transfers ownership of a valid accepted TCP fd; + // `fd < 0` was rejected above, and `TcpStream` closes it on drop. let tcp = unsafe { TcpStream::from_raw_fd(fd) }; let stream = StreamOwned::new(conn, tcp); @@ -867,14 +878,18 @@ pub extern "C" fn jerboa_tls_server_new_mtls_pem( client_ca_pem_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if cert_pem.is_null() || key_pem.is_null() || client_ca_pem.is_null() { - set_last_error("null cert/key/ca PEM pointer".to_string()); - return 0; - } - - let cert_data = unsafe { std::slice::from_raw_parts(cert_pem, cert_pem_len) }; - let key_data = unsafe { std::slice::from_raw_parts(key_pem, key_pem_len) }; - let ca_data = unsafe { std::slice::from_raw_parts(client_ca_pem, client_ca_pem_len) }; + let cert_data = match ffi_bytes(cert_pem, cert_pem_len, "TLS cert PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let key_data = match ffi_bytes(key_pem, key_pem_len, "TLS key PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let ca_data = match ffi_bytes(client_ca_pem, client_ca_pem_len, "TLS client CA PEM") { + Ok(value) => value, + Err(code) => return code, + }; let certs = match parse_cert_pem_slice( cert_data, @@ -981,36 +996,17 @@ pub extern "C" fn jerboa_tls_server_new_mtls( client_ca_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if cert_path.is_null() || key_path.is_null() || client_ca_path.is_null() { - set_last_error("null cert/key/ca path".to_string()); - return 0; - } - let cert_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(cert_path, cert_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid cert path".to_string()); - return 0; - } - } + let cert_str = match ffi_utf8(cert_path, cert_path_len, "TLS cert path") { + Ok(value) => value, + Err(code) => return code, }; - let key_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(key_path, key_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid key path".to_string()); - return 0; - } - } + let key_str = match ffi_utf8(key_path, key_path_len, "TLS key path") { + Ok(value) => value, + Err(code) => return code, }; - let ca_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(client_ca_path, client_ca_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid CA path".to_string()); - return 0; - } - } + let ca_str = match ffi_utf8(client_ca_path, client_ca_len, "TLS client CA path") { + Ok(value) => value, + Err(code) => return code, }; // Read server cert chain @@ -1144,45 +1140,21 @@ pub extern "C" fn jerboa_tls_connect_mtls( ca_cert_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if host.is_null() || cert_path.is_null() || key_path.is_null() || ca_cert_path.is_null() { - set_last_error("null argument".to_string()); - return 0; - } - let host_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(host, host_len)) { - Ok(s) => s.to_string(), - Err(_) => { - set_last_error("invalid UTF-8 hostname".to_string()); - return 0; - } - } + let host_str = match ffi_utf8(host, host_len, "TLS hostname") { + Ok(value) => value.to_string(), + Err(code) => return code, }; - let cert_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(cert_path, cert_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid cert path".to_string()); - return 0; - } - } + let cert_str = match ffi_utf8(cert_path, cert_path_len, "TLS cert path") { + Ok(value) => value, + Err(code) => return code, }; - let key_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(key_path, key_path_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid key path".to_string()); - return 0; - } - } + let key_str = match ffi_utf8(key_path, key_path_len, "TLS key path") { + Ok(value) => value, + Err(code) => return code, }; - let ca_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(ca_cert_path, ca_cert_len)) { - Ok(s) => s, - Err(_) => { - set_last_error("invalid CA path".to_string()); - return 0; - } - } + let ca_str = match ffi_utf8(ca_cert_path, ca_cert_len, "TLS CA path") { + Ok(value) => value, + Err(code) => return code, }; // Read client cert chain @@ -1325,22 +1297,22 @@ pub extern "C" fn jerboa_tls_connect_mtls_pem_ca( ca_pem_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if host.is_null() || cert_pem.is_null() || key_pem.is_null() || ca_pem.is_null() { - set_last_error("null argument".to_string()); - return 0; - } - let host_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(host, host_len)) { - Ok(s) => s.to_string(), - Err(_) => { - set_last_error("invalid UTF-8 hostname".to_string()); - return 0; - } - } + let host_str = match ffi_utf8(host, host_len, "TLS hostname") { + Ok(value) => value.to_string(), + Err(code) => return code, + }; + let cert_data = match ffi_bytes(cert_pem, cert_pem_len, "TLS cert PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let key_data = match ffi_bytes(key_pem, key_pem_len, "TLS key PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let ca_data = match ffi_bytes(ca_pem, ca_pem_len, "TLS CA PEM") { + Ok(value) => value, + Err(code) => return code, }; - let cert_data = unsafe { std::slice::from_raw_parts(cert_pem, cert_pem_len) }; - let key_data = unsafe { std::slice::from_raw_parts(key_pem, key_pem_len) }; - let ca_data = unsafe { std::slice::from_raw_parts(ca_pem, ca_pem_len) }; let client_certs = match parse_cert_pem_slice( cert_data, @@ -1455,21 +1427,18 @@ pub extern "C" fn jerboa_tls_connect_mtls_mem( key_pem_len: usize, ) -> u64 { match std::panic::catch_unwind(|| { - if host.is_null() || cert_pem.is_null() || key_pem.is_null() { - set_last_error("null argument".to_string()); - return 0; - } - let host_str = unsafe { - match std::str::from_utf8(std::slice::from_raw_parts(host, host_len)) { - Ok(s) => s.to_string(), - Err(_) => { - set_last_error("invalid UTF-8 hostname".to_string()); - return 0; - } - } + let host_str = match ffi_utf8(host, host_len, "TLS hostname") { + Ok(value) => value.to_string(), + Err(code) => return code, + }; + let cert_data = match ffi_bytes(cert_pem, cert_pem_len, "TLS cert PEM") { + Ok(value) => value, + Err(code) => return code, + }; + let key_data = match ffi_bytes(key_pem, key_pem_len, "TLS key PEM") { + Ok(value) => value, + Err(code) => return code, }; - let cert_data = unsafe { std::slice::from_raw_parts(cert_pem, cert_pem_len) }; - let key_data = unsafe { std::slice::from_raw_parts(key_pem, key_pem_len) }; // Parse client certs from PEM bytes let client_certs = match parse_cert_pem_slice( @@ -1575,10 +1544,10 @@ pub extern "C" fn jerboa_tls_connect_mtls_mem( #[no_mangle] pub extern "C" fn jerboa_tls_read(handle: u64, buf: *mut u8, max_len: usize) -> i32 { ffi_wrap(|| { - if buf.is_null() { - return -1; - } - let out = unsafe { std::slice::from_raw_parts_mut(buf, max_len) }; + let out = match ffi_output_mut(buf, max_len, "TLS read buffer") { + Ok(value) => value, + Err(code) => return code, + }; let arc = match get_conn_arc(handle) { Some(a) => a, None => { @@ -1608,10 +1577,10 @@ pub extern "C" fn jerboa_tls_read(handle: u64, buf: *mut u8, max_len: usize) -> #[no_mangle] pub extern "C" fn jerboa_tls_write(handle: u64, buf: *const u8, len: usize) -> i32 { ffi_wrap(|| { - if buf.is_null() { - return -1; - } - let data = unsafe { std::slice::from_raw_parts(buf, len) }; + let data = match ffi_bytes(buf, len, "TLS write buffer") { + Ok(value) => value, + Err(_) => return -1, + }; let arc = match get_conn_arc(handle) { Some(a) => a, None => { @@ -1676,10 +1645,8 @@ pub extern "C" fn jerboa_tls_close(handle: u64) { let fd = get_conn_fd(handle); if let Some(fd) = fd { // SHUT_RDWR aborts any pending blocking I/O on this socket. - // Safe: fd is still owned by the StreamOwned inside the Arc, - // which we release immediately below by removing from the map. - // Even if the I/O thread is mid-read, shutdown is documented to - // be safe to call concurrently with read/write. + // The StreamOwned inside the Arc still owns `fd` until map removal. + // SAFETY: shutdown is safe concurrently with socket read/write. unsafe { libc::shutdown(fd, libc::SHUT_RDWR); }