| // 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 once_cell::sync::Lazy; |
| |
| use crate::{ |
| abort_on_panic, |
| errors::{Error, TlsRetryReason}, |
| ffi::{sanitise_mut_slice, sanitize_slice}, |
| }; |
| |
| /// 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>>, |
| } |
| |
| /// Safety: `socket` field is a exclusively owned `Box<dyn AbstractSocket>` pointer, |
| /// and `AbstractSocket: Send + Sync`. |
| unsafe impl Send for RustBio {} |
| unsafe impl Sync for RustBio {} |
| |
| 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)) |
| } |
| |
| fn transform_result( |
| &mut self, |
| res: AbstractSocketResult, |
| reason_on_retry: TlsRetryReason, |
| ) -> IoStatus { |
| match res { |
| AbstractSocketResult::Ok(bytes) => IoStatus::Ok(bytes), |
| AbstractSocketResult::Retry => IoStatus::Retry(reason_on_retry), |
| AbstractSocketResult::EndOfStream => IoStatus::EndOfStream, |
| AbstractSocketResult::Err(e) => { |
| self.io_err = Some(e); |
| IoStatus::Err |
| } |
| } |
| } |
| } |
| |
| /// A 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) } |
| } |
| |
| /// I/O Status of the possibly pending operation. |
| #[derive(Debug, Clone, Copy)] |
| pub enum IoStatus { |
| /// Successfully performed I/O of bytes at certain size. |
| Ok(usize), |
| /// There is no more data to read or write. |
| EndOfStream, |
| /// I/O operation should be retried with the exactly same buffers when applicable. |
| Retry(TlsRetryReason), |
| /// There is no backing socket. |
| Empty, |
| /// I/O operation has failed. |
| Err, |
| } |
| |
| /// Result of operating an [`AbstractSocket`]. |
| pub enum AbstractSocketResult { |
| /// I/O completed by committing some amount of bytes. |
| Ok(usize), |
| /// I/O is pending completion; the I/O operation should be invoked again with the same parameter. |
| Retry, |
| /// I/O is impossible because the stream has ended. |
| EndOfStream, |
| /// I/O operation failed. |
| Err(Box<dyn core::error::Error + Send + Sync>), |
| } |
| |
| /// Abstract reader. |
| pub trait AbstractReader: Send + Sync + Unpin { |
| /// Read data from the socket. |
| fn read( |
| &mut self, |
| async_ctx: Option<&mut Context<'_>>, |
| buffer: &mut [u8], |
| ) -> AbstractSocketResult; |
| } |
| |
| /// Abstract writer. |
| pub trait AbstractWriter: Send + Sync + Unpin { |
| /// Write data to the socket. |
| 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(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, |
| len: c_int, |
| ) -> c_int { |
| 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(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 Some(buf) = (unsafe { |
| // Safety: `buffer` is valid for holding `len` bytes by BoringSSL invariants. |
| buffer.write_bytes(0, len); |
| // Safety: `buffer` and `len` are sanitised and initialised for the right memory region. |
| sanitise_mut_slice(buffer as *mut u8, len) |
| }) else { |
| return -1; |
| }; |
| let work = { |
| let Some(reader) = rust_bio.get_reader() else { |
| return -1; |
| }; |
| move || reader.read(async_ctx.as_mut(), buf) |
| }; |
| let res = abort_on_panic(work); |
| match rust_bio.transform_result(res, TlsRetryReason::WantRead) { |
| IoStatus::Ok(bytes) => { |
| if let Ok(bytes) = c_int::try_from(bytes) { |
| return bytes; |
| } |
| -1 |
| } |
| IoStatus::EndOfStream => { |
| rust_bio.read_eos = true; |
| -1 |
| } |
| IoStatus::Retry(_) => { |
| unsafe { |
| // Safety: `bio` is still valid now. |
| bssl_sys::BIO_set_retry_read(bio); |
| } |
| -1 |
| } |
| IoStatus::Empty | IoStatus::Err => -1, |
| } |
| } |
| |
| unsafe extern "C" fn rust_bio_write( |
| bio: *mut bssl_sys::BIO, |
| buffer: *const c_char, |
| len: c_int, |
| ) -> c_int { |
| 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 Ok(len) = usize::try_from(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 |
| }; |
| let Some(buf) = (unsafe { |
| // Safety: `buffer` and `len` are sanitised and initialised for the right memory region. |
| sanitize_slice(buffer as *mut u8, len) |
| }) else { |
| return -1; |
| }; |
| let work = { |
| let Some(writer) = rust_bio.get_writer() else { |
| return -1; |
| }; |
| move || writer.write(async_ctx.as_mut(), buf) |
| }; |
| let res = abort_on_panic(work); |
| match rust_bio.transform_result(res, TlsRetryReason::WantWrite) { |
| IoStatus::Ok(bytes) => { |
| if let Ok(bytes) = c_int::try_from(bytes) { |
| return bytes; |
| } |
| -1 |
| } |
| IoStatus::EndOfStream => { |
| rust_bio.write_eos = true; |
| 0 |
| } |
| IoStatus::Retry(_) => { |
| unsafe { |
| // Safety: `bio` is still valid now. |
| bssl_sys::BIO_set_retry_write(bio); |
| } |
| -1 |
| } |
| IoStatus::Empty | IoStatus::Err => -1, |
| } |
| } |
| |
| unsafe fn rust_bio_flush(bio: *mut bssl_sys::BIO) -> c_long { |
| 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 rust_bio.transform_result(res, TlsRetryReason::WantWrite) { |
| IoStatus::Ok(_) | IoStatus::Retry(_) => 1, |
| IoStatus::EndOfStream => { |
| rust_bio.write_eos = true; |
| 0 |
| } |
| IoStatus::Empty | IoStatus::Err => 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") |
| } |
| } |