| // Copyright 2026 The BoringSSL Authors | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // https://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! TLS I/O model | |
| use alloc::boxed::Box; | |
| use core::{ | |
| ffi::{ | |
| CStr, | |
| c_char, | |
| c_int, | |
| c_long, | |
| c_void, // | |
| }, | |
| fmt, | |
| ptr::NonNull, | |
| task::{ | |
| Context, | |
| Waker, // | |
| }, | |
| }; | |
| use bssl_crypto::FromFfiSlice; | |
| use once_cell::sync::Lazy; | |
| use crate::{ | |
| abort_on_panic, | |
| errors::{ | |
| Error, | |
| TlsRetryReason, // | |
| }, // | |
| }; | |
| #[cfg(feature = "std")] | |
| pub mod stdio; | |
| #[cfg(feature = "std")] | |
| pub mod sync_io; | |
| #[cfg(all(unix, feature = "std"))] | |
| pub mod unix; | |
| /// A wrapper around a `dyn AbstractSocket`, delegating BIO methods to the | |
| /// underlying `AbstractSocket` implementations. | |
| /// | |
| /// # Safety | |
| /// | |
| /// [`RustBio`] can only be accessed through exclusive ownership. | |
| // TODO(@xfding): switch to `derive(CoercePointee)` when it stabilises for flattening the layout. | |
| pub(crate) struct RustBio { | |
| // We may need to propagate the waker, but nothing more. | |
| waker: Option<Waker>, | |
| pub read_eos: bool, | |
| pub write_eos: bool, | |
| socket: Option<Box<dyn AbstractSocket>>, | |
| reader: Option<Box<dyn AbstractReader>>, | |
| writer: Option<Box<dyn AbstractWriter>>, | |
| io_err: Option<Box<dyn core::error::Error + Send + Sync>>, | |
| } | |
| fn _assert_rust_bio() | |
| where | |
| RustBio: Send + Unpin, | |
| { | |
| } | |
| /// Purely module-internal implementation details | |
| impl RustBio { | |
| fn init() -> Self { | |
| Self { | |
| waker: None, | |
| socket: None, | |
| reader: None, | |
| writer: None, | |
| read_eos: false, | |
| write_eos: false, | |
| io_err: None, | |
| } | |
| } | |
| pub fn attach_socket(&mut self, socket: Box<dyn AbstractSocket>) { | |
| self.socket = Some(socket); | |
| } | |
| pub fn attach_reader(&mut self, reader: Box<dyn AbstractReader>) { | |
| self.reader = Some(reader); | |
| } | |
| pub fn attach_writer(&mut self, writer: Box<dyn AbstractWriter>) { | |
| self.writer = Some(writer); | |
| } | |
| pub fn get_reader(&mut self) -> Option<&mut dyn AbstractReader> { | |
| if let Some(reader) = &mut self.reader { | |
| Some(&mut **reader) | |
| } else if let Some(socket) = &mut self.socket { | |
| Some(&mut **socket) | |
| } else { | |
| None | |
| } | |
| } | |
| pub fn get_writer(&mut self) -> Option<&mut dyn AbstractWriter> { | |
| if let Some(writer) = &mut self.writer { | |
| Some(&mut **writer) | |
| } else if let Some(socket) = &mut self.socket { | |
| Some(&mut **socket) | |
| } else { | |
| None | |
| } | |
| } | |
| } | |
| /// Crate-level `BIO` implementation details | |
| impl RustBio { | |
| pub fn set_waker(&mut self, waker: &Waker) { | |
| if let Some(other_waker) = &self.waker | |
| && waker.will_wake(other_waker) | |
| { | |
| return; | |
| } | |
| self.waker = Some(waker.clone()); | |
| } | |
| pub fn new_duplex<T: 'static + AbstractSocket + Sized>( | |
| socket: T, | |
| ) -> Result<RustBioHandle, Error> { | |
| let bio = unsafe { | |
| // Safety: the BIO_METH will be valid if this is the first call. | |
| bssl_sys::BIO_new(get_bio_method()) | |
| }; | |
| let bio = NonNull::new(bio).expect("allocation failure"); | |
| unsafe { | |
| // Safety: `bio` was constructed with our own BIO_METH, so the data must be valid | |
| // as a `RustBio` | |
| rust_bio_data_mut(bio.as_ptr()).attach_socket(Box::new(socket)); | |
| } | |
| Ok(RustBioHandle(bio)) | |
| } | |
| pub fn new_split<R, W>(reader: R, writer: W) -> Result<RustBioHandle, Error> | |
| where | |
| R: 'static + AbstractReader + Sized, | |
| W: 'static + AbstractWriter + Sized, | |
| { | |
| let bio = unsafe { | |
| // Safety: the BIO_METH will be valid if this is the first call. | |
| bssl_sys::BIO_new(get_bio_method()) | |
| }; | |
| let bio = NonNull::new(bio).expect("allocation failure"); | |
| let data = unsafe { | |
| // Safety: `bio` was constructed with our own BIO_METH, so the data must be valid | |
| // as a `RustBio` | |
| rust_bio_data_mut(bio.as_ptr()) | |
| }; | |
| data.attach_reader(Box::new(reader)); | |
| data.attach_writer(Box::new(writer)); | |
| Ok(RustBioHandle(bio)) | |
| } | |
| pub fn take_io_err(&mut self) -> Option<Box<dyn core::error::Error + Send + Sync>> { | |
| self.io_err.take() | |
| } | |
| } | |
| /// An exclusively owned handle to a BIO constructed by this crate. | |
| pub(crate) struct RustBioHandle(NonNull<bssl_sys::BIO>); | |
| impl RustBioHandle { | |
| pub fn ptr(&self) -> *mut bssl_sys::BIO { | |
| self.0.as_ptr() | |
| } | |
| pub fn as_mut(&mut self) -> &mut RustBio { | |
| unsafe { | |
| // Safety: `self` witnesses the validity of the handle. | |
| rust_bio_data_mut(self.ptr()) | |
| } | |
| } | |
| pub fn as_ref(&self) -> &RustBio { | |
| unsafe { | |
| // Safety: `self` witnesses the validity of the handle. | |
| rust_bio_data(self.ptr()) | |
| } | |
| } | |
| pub fn set_waker(&mut self, waker: &Waker) { | |
| self.as_mut().set_waker(waker); | |
| } | |
| } | |
| impl Drop for RustBioHandle { | |
| fn drop(&mut self) { | |
| unsafe { | |
| // Safety: the BIO handle should still be valid and was created by this crate. | |
| bssl_sys::BIO_free(self.0.as_ptr()); | |
| } | |
| } | |
| } | |
| /// Safety: caller must ensure that `bio` is created with `rust_bio_create` and outlives `'a` | |
| /// for exclusive access. | |
| unsafe fn rust_bio_data_mut<'a>(bio: *mut bssl_sys::BIO) -> &'a mut RustBio { | |
| let data = unsafe { | |
| // Safety: `bio` is still valid | |
| bssl_sys::BIO_get_data(bio) | |
| }; | |
| unsafe { &mut *(data as *mut RustBio) } | |
| } | |
| /// Safety: caller must ensure that `bio` is created with `rust_bio_create` and outlives `'a` | |
| /// for shared access. | |
| unsafe fn rust_bio_data<'a>(bio: *mut bssl_sys::BIO) -> &'a RustBio { | |
| let data = unsafe { | |
| // Safety: `bio` is still valid | |
| bssl_sys::BIO_get_data(bio) | |
| }; | |
| unsafe { &*(data as *const RustBio) } | |
| } | |
| /// TLS-level I/O Status of the possibly pending operation. | |
| #[derive(Debug, Clone, Copy)] | |
| pub enum IoStatus { | |
| /// Successfully performed I/O of bytes at certain size. | |
| /// `bssl-tls` uses `Ok(0)` to signal end-of-stream. | |
| Ok(usize), | |
| /// I/O operation should be retried with the exactly same buffers when applicable. | |
| Retry(TlsRetryReason), | |
| } | |
| /// Result of operating an [`AbstractSocket`]. | |
| pub enum AbstractSocketResult { | |
| /// I/O completed by committing some amount of bytes. | |
| /// Use `Ok(0)` to signal end-of-stream. | |
| Ok(usize), | |
| /// I/O is pending completion; the I/O operation should be invoked again with the same parameter. | |
| Retry, | |
| /// I/O operation failed. | |
| Err(Box<dyn core::error::Error + Send + Sync>), | |
| } | |
| /// Abstract reader. | |
| pub trait AbstractReader: Send { | |
| /// Read data from the socket. | |
| fn read( | |
| &mut self, | |
| async_ctx: Option<&mut Context<'_>>, | |
| buffer: &mut [u8], | |
| ) -> AbstractSocketResult; | |
| } | |
| /// Abstract writer. | |
| pub trait AbstractWriter: Send { | |
| /// Write data to the socket. | |
| /// | |
| /// If this function returns [`AbstractSocketResult::Ok(0)`], the library will translate it into | |
| /// a **transport error**. | |
| /// If this function returns [`AbstractSocketResult::Ok(n)`] with `n > buffer.len()`, the | |
| /// library will translate it into a **transport error**. | |
| fn write(&mut self, async_ctx: Option<&mut Context<'_>>, buffer: &[u8]) | |
| -> AbstractSocketResult; | |
| /// Flush the socket. | |
| fn flush(&mut self, async_ctx: Option<&mut Context<'_>>) -> AbstractSocketResult; | |
| } | |
| /// Abstract socket wrapper around Rust types that may support async I/O. | |
| pub trait AbstractSocket: AbstractReader + AbstractWriter {} | |
| // NOTE: this is not dead code, we are asserting that `dyn AbstractSocket` is a well-formed type, | |
| // or `AbstractSocket` is dyn-compatible specifically. | |
| fn _assert_dyn_compat() | |
| where | |
| dyn AbstractSocket:, | |
| { | |
| } | |
| fn get_bio_type() -> c_int { | |
| static BIO_TYPE: Lazy<c_int> = Lazy::new(|| { | |
| // Safety: this call does not have side-effect other than ID assignment | |
| unsafe { bssl_sys::BIO_get_new_index() } | |
| }); | |
| *BIO_TYPE | |
| } | |
| struct BioMethod(*mut bssl_sys::BIO_METHOD); | |
| /// Safety: once constructed this BIO vtable will stay immutable. | |
| unsafe impl Send for BioMethod {} | |
| /// Safety: once constructed this BIO vtable will stay immutable. | |
| unsafe impl Sync for BioMethod {} | |
| fn get_bio_method() -> *const bssl_sys::BIO_METHOD { | |
| static BIO_METHOD: Lazy<BioMethod> = Lazy::new(|| { | |
| let cstr = const { | |
| if let Ok(cstr) = CStr::from_bytes_with_nul(b"rust_bio\0") { | |
| cstr | |
| } else { | |
| // Compile-time assertion | |
| unreachable!() | |
| } | |
| }; | |
| let vtable = unsafe { | |
| // Safety: this call does not have side-effect other than allocation. | |
| bssl_sys::BIO_meth_new(get_bio_type(), cstr.as_ptr()) | |
| }; | |
| unsafe { | |
| // Safety: all the following calls are simple assignments to the vtable entries. | |
| bssl_sys::BIO_meth_set_read(vtable, Some(rust_bio_read)); | |
| bssl_sys::BIO_meth_set_write_ex(vtable, Some(rust_bio_write)); | |
| bssl_sys::BIO_meth_set_ctrl(vtable, Some(rust_bio_ctrl)); | |
| bssl_sys::BIO_meth_set_create(vtable, Some(rust_bio_create)); | |
| bssl_sys::BIO_meth_set_destroy(vtable, Some(rust_bio_destroy)); | |
| } | |
| BioMethod(vtable) | |
| }); | |
| BIO_METHOD.0 | |
| } | |
| unsafe extern "C" fn rust_bio_read( | |
| bio: *mut bssl_sys::BIO, | |
| buffer: *mut c_char, | |
| buf_len: c_int, | |
| ) -> c_int { | |
| unsafe { | |
| // Safety: `bio` is valid as witnessed by the C callback contract. | |
| bssl_sys::BIO_clear_retry_flags(bio); | |
| } | |
| debug_assert!(buf_len > 0, "BoringSSL BIO misbehaved"); | |
| let rust_bio = unsafe { | |
| // Safety: `bio` is still valid and so is the `RustBio` which we have exclusive access to. | |
| rust_bio_data_mut(bio) | |
| }; | |
| if rust_bio.read_eos { | |
| return 0; | |
| } | |
| let Ok(len) = usize::try_from(buf_len) else { | |
| return -1; | |
| }; | |
| let waker = rust_bio.waker.clone(); | |
| let mut async_ctx = if let Some(waker) = &waker { | |
| Some(Context::from_waker(waker)) | |
| } else { | |
| None | |
| }; | |
| // Zero the buffer now. | |
| // TODO(@xfding): maybe we want to have a buffer wrapper that tracks initialised region. | |
| let buf = unsafe { | |
| // Safety: `buffer` and `len` are sanitised and initialised for the right memory region. | |
| bssl_crypto::zeroize_mut_byteslice(buffer as *mut u8, len) | |
| }; | |
| let io_err = rust_bio.take_io_err(); | |
| let work = { | |
| let Some(reader) = rust_bio.get_reader() else { | |
| return -1; | |
| }; | |
| move || { | |
| let _ = io_err; | |
| reader.read(async_ctx.as_mut(), buf) | |
| } | |
| }; | |
| let res = abort_on_panic(work); | |
| match res { | |
| AbstractSocketResult::Ok(0) => { | |
| rust_bio.read_eos = true; | |
| 0 | |
| } | |
| AbstractSocketResult::Ok(bytes) => { | |
| // TODO: rewrite this after landing BIO_read_ex | |
| if let Ok(bytes) = c_int::try_from(bytes) | |
| && bytes <= buf_len | |
| { | |
| bytes | |
| } else { | |
| rust_bio.io_err = Some("AbstractReader misbehaved".into()); | |
| -1 | |
| } | |
| } | |
| AbstractSocketResult::Retry => { | |
| unsafe { | |
| // Safety: `bio` is still valid now. | |
| bssl_sys::BIO_set_retry_read(bio); | |
| } | |
| -1 | |
| } | |
| AbstractSocketResult::Err(e) => { | |
| rust_bio.io_err = Some(e); | |
| -1 | |
| } | |
| } | |
| } | |
| unsafe extern "C" fn rust_bio_write( | |
| bio: *mut bssl_sys::BIO, | |
| buffer: *const c_char, | |
| buf_len: usize, | |
| out_len: *mut usize, | |
| ) -> c_int { | |
| unsafe { | |
| // Safety: `bio` is valid as witnessed by the C callback contract. | |
| bssl_sys::BIO_clear_retry_flags(bio); | |
| } | |
| debug_assert!(buf_len > 0, "BoringSSL BIO misbehaved"); | |
| let rust_bio = unsafe { | |
| // Safety: `bio` is still valid and so is the `RustBio` which we have exclusive access to. | |
| rust_bio_data_mut(bio) | |
| }; | |
| if rust_bio.write_eos { | |
| return 0; | |
| } | |
| let waker = rust_bio.waker.clone(); | |
| let mut async_ctx = if let Some(waker) = &waker { | |
| Some(Context::from_waker(waker)) | |
| } else { | |
| None | |
| }; | |
| let buf = unsafe { | |
| // Safety: `buffer` is an allocation generated by BoringSSL of length `buf_len`. | |
| u8::from_ffi_ptr(buffer as *const u8, buf_len) | |
| }; | |
| // We must pre-emptively take out `io_err` so that it is not dropped outside the catch-unwind. | |
| let io_err = rust_bio.io_err.take(); | |
| let work = { | |
| let Some(writer) = rust_bio.get_writer() else { | |
| return 0; | |
| }; | |
| move || { | |
| let _ = io_err; | |
| writer.write(async_ctx.as_mut(), buf) | |
| } | |
| }; | |
| let res = abort_on_panic(work); | |
| match res { | |
| AbstractSocketResult::Ok(0) => { | |
| rust_bio.write_eos = true; | |
| rust_bio.io_err = Some("unexpected zero-length write".into()); | |
| 0 | |
| } | |
| AbstractSocketResult::Ok(bytes) => { | |
| if bytes <= buf_len { | |
| unsafe { | |
| // Safety: `out_len` must be valid per BoringSSL invariant. | |
| *out_len = bytes; | |
| } | |
| 1 | |
| } else { | |
| rust_bio.io_err = Some("AbstractWriter misbehaved".into()); | |
| 0 | |
| } | |
| } | |
| AbstractSocketResult::Retry => { | |
| unsafe { | |
| // Safety: `bio` is still valid now. | |
| bssl_sys::BIO_set_retry_write(bio); | |
| } | |
| 0 | |
| } | |
| AbstractSocketResult::Err(e) => { | |
| rust_bio.io_err = Some(e); | |
| 0 | |
| } | |
| } | |
| } | |
| unsafe fn rust_bio_flush(bio: *mut bssl_sys::BIO) -> c_long { | |
| unsafe { | |
| // Safety: `bio` is valid as witnessed by the caller contract. | |
| bssl_sys::BIO_clear_retry_flags(bio); | |
| } | |
| let rust_bio = unsafe { | |
| // Safety: `bio` is still valid | |
| rust_bio_data_mut(bio) | |
| }; | |
| if rust_bio.write_eos { | |
| return 0; | |
| } | |
| let waker = rust_bio.waker.clone(); | |
| let mut async_ctx = if let Some(waker) = &waker { | |
| Some(Context::from_waker(waker)) | |
| } else { | |
| None | |
| }; | |
| let work = { | |
| let Some(writer) = rust_bio.get_writer() else { | |
| return -1; | |
| }; | |
| move || writer.flush(async_ctx.as_mut()) | |
| }; | |
| let res = abort_on_panic(work); | |
| match res { | |
| AbstractSocketResult::Ok(_) => 1, | |
| AbstractSocketResult::Retry => { | |
| unsafe { | |
| // Safety: `bio` is still valid now. | |
| bssl_sys::BIO_set_retry_write(bio); | |
| } | |
| 0 | |
| } | |
| AbstractSocketResult::Err(e) => { | |
| rust_bio.io_err = Some(e); | |
| 0 | |
| } | |
| } | |
| } | |
| unsafe extern "C" fn rust_bio_ctrl( | |
| bio: *mut bssl_sys::BIO, | |
| ctrl: c_int, | |
| _: c_long, | |
| _: *mut c_void, | |
| ) -> c_long { | |
| match ctrl { | |
| bssl_sys::BIO_CTRL_FLUSH => unsafe { | |
| // Safety: `bio` is still valid. | |
| rust_bio_flush(bio) | |
| }, | |
| _ => 0, | |
| } | |
| } | |
| unsafe extern "C" fn rust_bio_create(bio: *mut bssl_sys::BIO) -> c_int { | |
| let data = Box::new(RustBio::init()); | |
| let data = Box::into_raw(data); | |
| unsafe { | |
| // Safety: both `bio` and `data` are still valid and exclusively owned. | |
| bssl_sys::BIO_set_data(bio, data as _); | |
| // Safety: it is now already safe to mark BIO initialised. | |
| bssl_sys::BIO_set_init(bio, 1); | |
| } | |
| 1 | |
| } | |
| unsafe extern "C" fn rust_bio_destroy(bio: *mut bssl_sys::BIO) -> c_int { | |
| let rust_bio = unsafe { | |
| // Safety: `bio` is still valid | |
| bssl_sys::BIO_get_data(bio) as *mut RustBio | |
| }; | |
| let rust_bio = unsafe { | |
| // Safety: `rust_bio` is created from `rust_bio_create` | |
| Box::from_raw(rust_bio) | |
| }; | |
| // Try to catch unwinding on the FFI boundary. | |
| // NOTE: it is not safe to drop the error value because its destructor can panic again. | |
| abort_on_panic(move || { | |
| let _ = rust_bio; | |
| }); | |
| unsafe { | |
| // Safety: `bio` is still valid, we just need to signal that it is inactive. | |
| bssl_sys::BIO_set_init(bio, 0); | |
| } | |
| 1 | |
| } | |
| /// Asynchronous methods were invoked outside `async` context | |
| #[derive(Debug)] | |
| pub struct NoAsyncContext; | |
| impl core::error::Error for NoAsyncContext {} | |
| impl fmt::Display for NoAsyncContext { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| f.write_str("async method is called outside async context") | |
| } | |
| } |