diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdd2a5f15..5ee5fcc70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,6 +230,13 @@ jobs: # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # + # - `litebox_broker_transport` is allowed to have `std` access, + # since it owns hosted concrete broker transport implementations, + # including the current Unix-domain-socket control channel. + # + # - `litebox_broker_userland` is allowed to have `std` access, + # since it is the hosted userland broker executable. + # # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. # @@ -285,6 +292,8 @@ jobs: # can safely use std. find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ + -not -path './litebox_broker_transport/Cargo.toml' \ + -not -path './litebox_broker_userland/Cargo.toml' \ -not -path './litebox_platform_linux_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ -not -path './litebox_runner_linux_on_windows_userland/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 286b3b24e..01fde3376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1460,6 +1460,8 @@ dependencies = [ "buddy_system_allocator", "either", "hashbrown", + "litebox_broker_local", + "litebox_broker_protocol", "litebox_util_log", "rangemap", "ringbuf", @@ -1474,6 +1476,51 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_broker_core" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_host" +version = "0.1.0" +dependencies = [ + "litebox_broker_core", + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_local" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_protocol" +version = "0.1.0" + +[[package]] +name = "litebox_broker_transport" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_userland" +version = "0.1.0" +dependencies = [ + "clap", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", + "litebox_broker_transport", +] + [[package]] name = "litebox_common_linux" version = "0.1.0" @@ -1647,6 +1694,11 @@ dependencies = [ "glob", "libc", "litebox", + "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", + "litebox_broker_protocol", + "litebox_broker_transport", "litebox_common_linux", "litebox_platform_linux_userland", "litebox_platform_multiplex", diff --git a/Cargo.toml b/Cargo.toml index e30b3904a..6456081c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,13 @@ [workspace] resolver = "2" members = [ - "litebox", + "litebox", + "litebox_broker_local", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_host", + "litebox_broker_transport", + "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", @@ -29,6 +35,12 @@ members = [ ] default-members = [ "litebox", + "litebox_broker_local", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_host", + "litebox_broker_transport", + "litebox_broker_userland", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index cd9d68139..09ff7ad2c 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -34,6 +34,7 @@ fn ratchet_globals() -> Result<()> { ratchet( &[ ("dev_bench/", 1), + ("litebox_broker_core/", 1), ("litebox/", 9), ("litebox_platform_linux_kernel/", 6), ("litebox_platform_linux_userland/", 5), diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 44c0f35c7..6e8c3ecfe 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -20,6 +20,8 @@ buddy_system_allocator = { version = "0.11.0", default-features = false, feature # Depend on (currently unreleased) slabmalloc `main`, which contains some fixes on top of `0.11.0` slabmalloc = { git = "https://github.com/gz/rust-slabmalloc.git", rev = "19480b2e82704210abafe575fb9699184c1be110" } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.60.2", features = [ diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs new file mode 100644 index 000000000..2ffff3289 --- /dev/null +++ b/litebox/src/broker/error.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::ErrorCode; + +use crate::event::{counter::EventCounterError, polling::TryOpError}; + +/// Error returned by the deployment-provided broker control path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerControlError { + /// The broker control transport failed. + Transport, + /// The broker returned an operation error. + Broker(ErrorCode), + /// The broker returned a response shape that does not match the request. + UnexpectedResponse, +} + +/// Internal normalized error for broker-backed object adapters. +/// +/// This keeps protocol/control-channel failures separate from the public +/// object-specific API error exposed by each local-core facade. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum BrokerObjectError { + /// The deployment-provided broker control path failed. + Control, + /// The broker rejected the cached object handle, type, or rights. + InvalidObject, + /// The object operation would block in its current broker-side state. + WouldBlock, + /// The object or broker-side state cannot grow further. + ResourceExhausted, + /// The broker returned a response shape that does not match the request. + UnexpectedResponse, + /// The broker reported a non-recoverable or unsupported object error. + Internal, +} + +impl From for BrokerObjectError { + fn from(error: BrokerControlError) -> Self { + match error { + BrokerControlError::Transport => Self::Control, + BrokerControlError::Broker(error) => error.into(), + BrokerControlError::UnexpectedResponse => Self::UnexpectedResponse, + } + } +} + +impl From for BrokerObjectError { + fn from(error: ErrorCode) -> Self { + match error { + ErrorCode::InvalidRights + | ErrorCode::UnknownObject + | ErrorCode::WrongObjectType + | ErrorCode::StaleHandle => Self::InvalidObject, + ErrorCode::WouldBlock => Self::WouldBlock, + ErrorCode::ResourceExhausted => Self::ResourceExhausted, + _ => Self::Internal, + } + } +} + +pub(crate) fn map_broker_object_result( + result: Result, +) -> Result> { + match result { + Ok(value) => Ok(value), + Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(error.into())), + } +} + +impl From for EventCounterError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::WouldBlock => Self::WouldBlock, + BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, + BrokerObjectError::UnexpectedResponse => Self::UnexpectedResponse, + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::Internal => Self::Io, + } + } +} diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs new file mode 100644 index 000000000..e3edc541f --- /dev/null +++ b/litebox/src/broker/mod.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +use litebox_broker_local::{BrokerLocal, BrokerLocalError}; +use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; + +use crate::sync::{Mutex, RawSyncPrimitivesProvider}; + +pub(crate) mod error; +pub use error::BrokerControlError; + +/// Local-core access to the negotiated broker control channel. +/// +/// LiteBox owns broker-backed local objects and constructs broker protocol +/// requests. Deployment code owns endpoint selection and supplies the connected +/// transport behind this protocol-level boundary. +pub trait BrokerControl: Send + Sync { + /// Sends one active BrokerCore request and returns its response. + fn request( + &self, + request: CoreRequest, + ) -> core::result::Result; +} + +struct BrokerLocalControl { + local: Mutex>, +} + +impl BrokerLocalControl +where + Platform: RawSyncPrimitivesProvider, +{ + const fn new(local: BrokerLocal) -> Self { + Self { + local: Mutex::new(local), + } + } +} + +impl BrokerControl for BrokerLocalControl +where + Platform: RawSyncPrimitivesProvider, + T: LocalControlChannel + Send, +{ + fn request( + &self, + request: CoreRequest, + ) -> core::result::Result { + self.local + .lock() + .active_core_request(request) + .map_err(broker_control_error) + } +} + +fn broker_control_error(error: BrokerLocalError) -> BrokerControlError { + match error { + BrokerLocalError::Broker(error) => BrokerControlError::Broker(error), + BrokerLocalError::UnexpectedResponse(_) => BrokerControlError::UnexpectedResponse, + _ => BrokerControlError::Transport, + } +} + +pub(crate) fn control_from_local(local: BrokerLocal) -> Arc +where + Platform: RawSyncPrimitivesProvider, + T: LocalControlChannel + Send + 'static, +{ + Arc::new(BrokerLocalControl::::new(local)) +} + +pub(crate) struct BrokerState { + control: Option>, + _marker: core::marker::PhantomData, +} + +impl BrokerState { + pub(crate) fn new(control: Option>) -> Self { + Self { + control, + _marker: core::marker::PhantomData, + } + } + + pub(crate) fn control(&self) -> Option> { + self.control.clone() + } +} diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs new file mode 100644 index 000000000..c6bbfe447 --- /dev/null +++ b/litebox/src/event/counter.rs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +pub use litebox_broker_protocol::EventConsumeMode as EventCounterReadMode; +use litebox_broker_protocol::{ + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, + CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, + WaitEventRequest, WaitOutcome, +}; + +use crate::{ + LiteBox, + broker::{ + BrokerControl, + error::{BrokerObjectError, map_broker_object_result}, + }, + event::{ + Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, + wait::WaitContext, + }, + platform::TimeProvider, + sync::RawSyncPrimitivesProvider, +}; + +/// Errors returned by local-core event counters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventCounterError { + /// The requested operation is invalid for this event counter. + InvalidInput, + /// The operation would block. + WouldBlock, + /// The event counter cannot accept more state. + ResourceExhausted, + /// The backing authority or transport failed. + Io, + /// The backing authority returned a response shape that does not match the request. + UnexpectedResponse, + /// No backing authority is available for this event counter. + Unavailable, +} + +/// A local-core event counter object. +pub struct EventCounter { + broker: Arc, + handle: ObjectHandle, + pollee: Pollee, + blocking_operations_supported: bool, +} + +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + /// Creates a local-core event counter. + pub fn new(litebox: &LiteBox, initial_count: u64) -> Result { + let Some(broker) = litebox.broker_control() else { + return Err(EventCounterError::Unavailable); + }; + let response = broker + .request(CoreRequest::Event(EventRequest::Create( + CreateEventRequest::new(initial_count), + ))) + .map_err(BrokerObjectError::from) + .and_then(event_response_from_core) + .map_err(EventCounterError::from)?; + let EventResponse::Create(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse.into()); + }; + Ok(Self { + broker, + handle: response.handle, + pollee: Pollee::new(), + blocking_operations_supported: true, + }) + } + + /// Returns whether blocking reads and writes are supported. + pub fn supports_blocking_operations(&self) -> bool { + self.blocking_operations_supported + } + + /// Reads the event counter. + pub fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + mode: EventCounterReadMode, + ) -> Result> { + self.pollee.wait(cx, nonblock, Events::IN, || { + let response = map_broker_object_result(self.consume(mode))?; + if response.readiness.write_ready { + self.pollee.notify_observers(Events::OUT); + } + Ok(response.value) + }) + } + + /// Writes readiness credits to the event counter. + pub fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result> { + if value == u64::MAX { + return Err(TryOpError::Other(EventCounterError::InvalidInput)); + } + self.pollee.wait(cx, nonblock, Events::OUT, || { + let readiness = map_broker_object_result(self.add(value))?; + if value != 0 && readiness.read_ready { + self.pollee.notify_observers(Events::IN); + } + Ok(core::mem::size_of::()) + }) + } + + fn consume( + &self, + mode: EventCounterReadMode, + ) -> Result { + let response = self.request_event(EventRequest::Consume(ConsumeEventRequest::new( + self.handle, + mode, + )))?; + let EventResponse::Consume(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(response) + } + + fn add(&self, value: u64) -> Result { + let response = + self.request_event(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; + let EventResponse::Add(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(response.readiness) + } + + fn readiness_state(&self) -> Result { + let response = + self.request_event(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; + let EventResponse::Wait(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(match response.outcome { + WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness) => readiness, + _ => return Err(BrokerObjectError::UnexpectedResponse), + }) + } + + fn request_event(&self, request: EventRequest) -> Result { + self.broker + .request(CoreRequest::Event(request)) + .map_err(BrokerObjectError::from) + .and_then(event_response_from_core) + } +} + +impl IOPollable for EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + self.pollee.register_observer(observer, mask); + } + + fn check_io_events(&self) -> Events { + let Ok(readiness) = self.readiness_state() else { + return Events::empty(); + }; + let mut events = Events::empty(); + if readiness.read_ready { + events |= Events::IN; + } + if readiness.write_ready { + events |= Events::OUT; + } + events + } +} + +fn event_response_from_core(response: CoreResponse) -> Result { + match response { + CoreResponse::Event(response) => Ok(response), + _ => Err(BrokerObjectError::UnexpectedResponse), + } +} diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 24d5b6832..6089b6b08 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -3,6 +3,7 @@ //! Events related functionality +pub mod counter; pub mod observer; pub mod polling; pub mod wait; diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index f3d80997a..f01e028f9 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -39,3 +39,6 @@ mod utilities; // Public utilities that might be used in other LiteBox crates. pub mod utils; + +mod broker; +pub use broker::{BrokerControl, BrokerControlError}; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 2fb209c22..35cca9239 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -5,7 +5,11 @@ use alloc::sync::Arc; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::LocalControlChannel; + use crate::{ + broker::{self, BrokerControl, BrokerState}, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -30,6 +34,35 @@ impl LiteBox { /// If the `enforce_singleton_litebox_instance` compilation feature has been enabled, and more /// than one instance is made, will panic. pub fn new(platform: &'static Platform) -> Self { + Self::new_inner(platform, None) + } + + /// Create a new [`LiteBox`] instance with broker control installed. + pub fn new_with_broker_control( + platform: &'static Platform, + broker_control: Arc, + ) -> Self { + Self::new_inner(platform, Some(broker_control)) + } + + /// Create a new [`LiteBox`] instance with a negotiated broker-local control adapter installed. + pub fn new_with_broker_local( + platform: &'static Platform, + broker_local: BrokerLocal, + ) -> Self + where + T: LocalControlChannel + Send + 'static, + { + Self::new_inner( + platform, + Some(broker::control_from_local::(broker_local)), + ) + } + + fn new_inner( + platform: &'static Platform, + broker_control: Option>, + ) -> Self { // This check ensures that there is exactly one `LiteBox` instance in the process. // // LiteBox itself supports having multiple instances (and subsystems correctly make any @@ -65,6 +98,7 @@ impl LiteBox { crate::sync::lock_tracing::LockTracker::init(platform); let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); + let broker = BrokerState::new(broker_control); litebox_util_log::trace!("LiteBox instance initialized"); @@ -72,6 +106,7 @@ impl LiteBox { x: Arc::new(LiteBoxX { platform, descriptors, + broker, }), } } @@ -106,10 +141,15 @@ impl LiteBox { ) -> impl core::ops::DerefMut> + use<'_, Platform> { self.x.descriptors.write() } + + pub(crate) fn broker_control(&self) -> Option> { + self.x.broker.control() + } } /// The actual body of [`LiteBox`], containing any components that might be shared. pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, + broker: BrokerState, } diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml new file mode 100644 index 000000000..9b9894147 --- /dev/null +++ b/litebox_broker_core/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_core" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs new file mode 100644 index 000000000..8f9dceace --- /dev/null +++ b/litebox_broker_core/src/error.rs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// Broker authority error category. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum BrokerError { + /// Policy denied the operation. + PolicyDenied, + /// The referenced object does not exist. + UnknownObject, + /// The referenced object generation is stale. + StaleHandle, + /// The referenced object type does not match the operation. + WrongObjectType, + /// The caller lacks the required broker rights. + InvalidRights, + /// Broker-side resource exhaustion. + ResourceExhausted, + /// A broker core has already been created in this process. + BrokerCoreAlreadyExists, + /// The operation would block in the current object state. + WouldBlock, + /// The operation is not implemented by this BrokerCore. + UnsupportedOperation, + /// Policy returned a decision that does not match the authorized operation. + InvalidPolicyDecision, +} + +impl fmt::Display for BrokerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PolicyDenied => f.write_str("broker policy denied the operation"), + Self::UnknownObject => f.write_str("unknown broker object"), + Self::StaleHandle => f.write_str("stale broker handle"), + Self::WrongObjectType => f.write_str("wrong broker object type"), + Self::InvalidRights => f.write_str("invalid broker rights"), + Self::ResourceExhausted => f.write_str("broker resource exhausted"), + Self::BrokerCoreAlreadyExists => f.write_str("broker core already exists"), + Self::WouldBlock => f.write_str("broker operation would block"), + Self::UnsupportedOperation => f.write_str("unsupported broker operation"), + Self::InvalidPolicyDecision => f.write_str("invalid broker policy decision"), + } + } +} + +impl core::error::Error for BrokerError {} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs new file mode 100644 index 000000000..523489cea --- /dev/null +++ b/litebox_broker_core/src/event.rs @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::object::{ObjectId, ObjectKind}; +use crate::{BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, Result}; +use litebox_broker_protocol::{ + EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, +}; + +const MAX_EVENT_COUNT: u64 = u64::MAX - 1; + +impl BrokerCore { + /// Creates a broker-owned event object. + pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { + self.create_event_with_count(association, 0) + } + + /// Creates a broker-owned event object with initial readiness credits. + pub fn create_event_with_count( + &mut self, + association: &BrokerAssociation, + initial_count: u64, + ) -> Result { + if initial_count > MAX_EVENT_COUNT { + return Err(BrokerError::ResourceExhausted); + } + let rights = self.authorize_create_object(association, ObjectType::Event)?; + + self.insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new(initial_count)), + ObjectType::Event, + rights, + ) + } + + /// Checks whether an event wait would complete now. + /// + /// Blocking is intentionally outside BrokerCore for the first proof of + /// concept. Userland or kernel deployments can block on deployment-specific + /// wait primitives after BrokerCore authorizes and reports readiness state. + pub fn wait_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let state = Self::filter_readiness_for_rights( + self.event_state(authorized.object_id)?, + authorized.rights, + ); + Ok(if state.read_ready { + WaitOutcome::Ready(state) + } else { + WaitOutcome::WouldBlock(state) + }) + } + + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + value: u64, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + match &mut self.object_mut(authorized.object_id)?.kind { + ObjectKind::Event(event) => event + .add(value) + .map(|state| Self::filter_readiness_for_rights(state, authorized.rights)), + } + } + + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { + let authorized = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + match &mut self.object_mut(authorized.object_id)?.kind { + ObjectKind::Event(event) => event.consume(mode).map(|response| { + EventConsumption::new( + response.value, + Self::filter_readiness_for_rights(response.readiness, authorized.rights), + ) + }), + } + } + + fn filter_readiness_for_rights(state: ReadinessState, rights: ObjectRights) -> ReadinessState { + ReadinessState::new( + rights.contains(ObjectRights::WAIT) && state.read_ready, + rights.contains(ObjectRights::WRITE) && state.write_ready, + state.generation, + ) + } + + fn event_state(&self, object_id: ObjectId) -> Result { + match &self.object(object_id)?.kind { + ObjectKind::Event(event) => Ok(event.readiness_state()), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct EventObject { + count: u64, + readiness_generation: u64, +} + +impl EventObject { + pub(crate) const fn new(count: u64) -> Self { + Self { + count, + readiness_generation: 0, + } + } + + pub(crate) const fn readiness_state(self) -> ReadinessState { + ReadinessState::new( + self.count > 0, + self.count < MAX_EVENT_COUNT, + self.readiness_generation, + ) + } + + fn add(&mut self, value: u64) -> Result { + let new_count = self + .count + .checked_add(value) + .filter(|count| *count <= MAX_EVENT_COUNT) + .ok_or(BrokerError::WouldBlock)?; + let next_generation = self.next_generation()?; + self.count = new_count; + self.readiness_generation = next_generation; + Ok(self.readiness_state()) + } + + fn consume(&mut self, mode: EventConsumeMode) -> Result { + if self.count == 0 { + return Err(BrokerError::WouldBlock); + } + + let value = match mode { + EventConsumeMode::All => self.count, + EventConsumeMode::One => 1, + _ => return Err(BrokerError::UnsupportedOperation), + }; + let next_generation = self.next_generation()?; + self.count -= value; + self.readiness_generation = next_generation; + Ok(EventConsumption::new(value, self.readiness_state())) + } + + fn next_generation(&self) -> Result { + self.readiness_generation + .checked_add(1) + .ok_or(BrokerError::ResourceExhausted) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_readiness_state_only_reports_authorized_directions() { + let readiness = ReadinessState::new(true, true, 7); + + assert_eq!( + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WAIT), + ReadinessState::new(true, false, 7) + ); + assert_eq!( + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WRITE), + ReadinessState::new(false, true, 7) + ); + } + + #[test] + fn add_event_does_not_mutate_count_when_generation_is_exhausted() { + let mut event = EventObject { + count: 1, + readiness_generation: u64::MAX, + }; + + assert_eq!(event.add(1), Err(BrokerError::ResourceExhausted)); + assert_eq!(event.count, 1); + assert_eq!(event.readiness_generation, u64::MAX); + } + + #[test] + fn consume_event_does_not_mutate_count_when_generation_is_exhausted() { + let mut event = EventObject { + count: 1, + readiness_generation: u64::MAX, + }; + + assert_eq!( + event.consume(EventConsumeMode::One), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(event.count, 1); + assert_eq!(event.readiness_generation, u64::MAX); + } +} diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs new file mode 100644 index 000000000..5b26f26d0 --- /dev/null +++ b/litebox_broker_core/src/identity.rs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerCore, Result, allocate_id}; + +/// Caller identity information supplied by the broker entry layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but BrokerCore still accepts an explicit credential value so authenticated +/// servers or hosts can plumb identity through the same association-creation seam. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum CallerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + Unauthenticated, +} + +/// Broker-assigned guest process identity. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ProcessId(u64); + +impl ProcessId { + pub(crate) const fn new(raw: u64) -> Self { + Self(raw) + } +} + +/// Broker-owned authority token for one authenticated caller association. +/// +/// User mode does not choose this value. The broker entry layer authenticates +/// the caller, then BrokerCore assigns this identity for all operations received +/// on that association. +#[derive(Debug, PartialEq, Eq)] +pub struct BrokerAssociation { + /// Broker-assigned guest process identity. + process_id: ProcessId, + /// Broker-entry-authenticated caller credential for this association. + caller_credential: CallerCredential, +} + +impl BrokerAssociation { + /// Creates an authenticated association identity. + pub(crate) const fn new(process_id: ProcessId, caller_credential: CallerCredential) -> Self { + Self { + process_id, + caller_credential, + } + } + + pub(crate) const fn process_id(&self) -> ProcessId { + self.process_id + } + + /// Returns the broker-entry-authenticated caller credential for this association. + pub const fn caller_credential(&self) -> CallerCredential { + self.caller_credential + } +} + +impl BrokerCore { + /// Allocates broker authority state for one authenticated caller association. + pub fn create_association( + &mut self, + caller_credential: CallerCredential, + ) -> Result { + let process_id = allocate_id(&mut self.next_process_id)?; + let association = BrokerAssociation::new(ProcessId::new(process_id), caller_credential); + Ok(association) + } +} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs new file mode 100644 index 000000000..e1c54f5d5 --- /dev/null +++ b/litebox_broker_core/src/lib.rs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker authority core independent of protocol envelopes and channels. +//! +//! `litebox_broker_core` owns broker-side object identity, reference lifetime, +//! rights checks, reference generation checks, and policy calls. It may use +//! shared semantic DTOs from `litebox_broker_protocol` for values that both the +//! local core and broker understand, such as handles and readiness state. It +//! deliberately has no dependency on protocol envelopes, channel traits, wire +//! codecs, Unix sockets, shared-memory rings, kernel traps, or any other +//! channel implementation. + +#![no_std] + +extern crate alloc; +#[cfg(test)] +extern crate std; + +mod error; +mod event; +mod identity; +mod object; +mod policy; + +use alloc::collections::BTreeMap; +use core::sync::atomic::{AtomicBool, Ordering}; + +pub use error::BrokerError; +pub use identity::{BrokerAssociation, CallerCredential}; +use litebox_broker_protocol::ObjectReferenceId; +use object::{ObjectEntry, ObjectId, ObjectReference}; +pub use object::{ObjectRights, ObjectType}; +pub use policy::{ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, PolicyProfile}; + +/// BrokerCore result type. +pub type Result = core::result::Result; + +/// Resource limits for broker-owned authority state. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct BrokerCoreLimits { + /// Maximum live broker objects. + pub max_objects: usize, + /// Maximum live object references. + pub max_references: usize, +} + +impl BrokerCoreLimits { + /// Conservative default limits for initial broker deployments. + pub const DEFAULT: Self = Self { + max_objects: 4096, + max_references: 4096, + }; + + /// Creates a broker core limit set. + pub const fn new(max_objects: usize, max_references: usize) -> Self { + Self { + max_objects, + max_references, + } + } +} + +impl Default for BrokerCoreLimits { + fn default() -> Self { + Self::DEFAULT + } +} + +/// Channel-independent broker authority state. +/// +/// A broker process may construct only one broker core for its process +/// lifetime. Constructors return [`BrokerError::BrokerCoreAlreadyExists`] if a +/// core has already been constructed. +pub struct BrokerCore { + policy: PolicyEngine, + limits: BrokerCoreLimits, + next_process_id: u64, + next_object_id: u64, + next_reference_id: u64, + objects: BTreeMap, + references: BTreeMap, +} + +static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); + +impl BrokerCore { + /// Creates the broker core with the provided policy engine. + pub fn new(policy: PolicyEngine) -> Result { + Self::new_with_limits(policy, BrokerCoreLimits::DEFAULT) + } + + /// Creates the broker core with explicit authority-state limits. + pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Result { + BROKER_CORE_CREATED + .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) + .map_err(|_| BrokerError::BrokerCoreAlreadyExists)?; + + Ok(Self { + policy, + limits, + next_process_id: 1, + next_object_id: 1, + next_reference_id: 1, + objects: BTreeMap::new(), + references: BTreeMap::new(), + }) + } +} + +const EXHAUSTED_ID: u64 = 0; + +fn allocate_id(next_id: &mut u64) -> Result { + if *next_id == EXHAUSTED_ID { + return Err(BrokerError::ResourceExhausted); + } + + let id = *next_id; + *next_id = id.checked_add(1).unwrap_or(EXHAUSTED_ID); + Ok(id) +} diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs new file mode 100644 index 000000000..06cca10ff --- /dev/null +++ b/litebox_broker_core/src/object.rs @@ -0,0 +1,355 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::ops::BitOr; + +use crate::event::EventObject; +use crate::identity::{BrokerAssociation, ProcessId}; +use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; +use litebox_broker_protocol::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; + +/// Broker object type known to the authority core and policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ObjectType { + /// Broker-owned event object. + Event, +} + +/// Broker rights attached to an object reference. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +pub struct ObjectRights(u32); + +impl ObjectRights { + /// Empty rights set. + pub const NONE: Self = Self(0); + /// Right to wait for readiness. + pub const WAIT: Self = Self(1 << 0); + /// Right to mutate object state, such as adding event readiness credits. + pub const WRITE: Self = Self(1 << 1); + + /// Returns true when no rights are present. + pub const fn is_empty(self) -> bool { + self.0 == 0 + } + + /// Returns true when all `required` rights are present. + pub const fn contains(self, required: Self) -> bool { + (self.0 & required.0) == required.0 + } + + /// Returns the union of two rights sets. + #[must_use] + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } +} + +impl BitOr for ObjectRights { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + self.union(rhs) + } +} + +/// Broker-owned object identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ObjectId(u64); + +impl ObjectId { + /// Creates an object identifier from its raw value. + const fn new(raw: u64) -> Self { + Self(raw) + } +} + +const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectReference { + pub(crate) object_id: ObjectId, + pub(crate) reference_generation: ObjectReferenceGeneration, + pub(crate) owner: ProcessId, + pub(crate) object_type: ObjectType, + pub(crate) rights: ObjectRights, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectEntry { + pub(crate) kind: ObjectKind, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ObjectKind { + Event(EventObject), +} + +impl ObjectKind { + pub(crate) const fn object_type(self) -> ObjectType { + match self { + Self::Event(_) => ObjectType::Event, + } + } +} + +impl BrokerCore { + /// Inserts a broker object and mints its first owned reference. + /// + /// The current POC never reuses reference slots, so the reference + /// generation starts at the authority-owned first generation. Any future + /// reference-slot reuse path must bump the generation before reissuing a + /// slot so stale handles cannot validate against a recycled reference. + pub(crate) fn insert_object_with_reference( + &mut self, + association: &BrokerAssociation, + kind: ObjectKind, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result { + if self.objects.len() >= self.limits.max_objects + || self.references.len() >= self.limits.max_references + { + return Err(BrokerError::ResourceExhausted); + } + + let object_id = self.allocate_object_id()?; + let reference_id = self.allocate_reference_id()?; + let reference_generation = FIRST_REFERENCE_GENERATION; + + self.objects.insert(object_id, ObjectEntry { kind }); + self.references.insert( + reference_id, + ObjectReference { + object_id, + reference_generation, + owner: association.process_id(), + object_type, + rights, + }, + ); + + Ok(ObjectHandle::new(reference_id, reference_generation)) + } + + pub(crate) fn authorize_create_object( + &mut self, + association: &BrokerAssociation, + object_type: ObjectType, + ) -> Result { + match self.policy.authorize(PolicyOperation::create_object( + association.caller_credential(), + object_type, + ))? { + PolicyDecision::GrantObjectReference { rights } => Ok(rights), + _ => Err(BrokerError::InvalidPolicyDecision), + } + } + + pub(crate) fn authorize_use_object( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result { + let reference = self.validate_handle(association, handle, object_type, rights)?; + let object_id = reference.object_id; + let reference_rights = reference.rights; + match self.policy.authorize(PolicyOperation::use_object( + association.caller_credential(), + object_type, + rights, + ))? { + PolicyDecision::Authorized => Ok(AuthorizedObject { + object_id, + rights: reference_rights, + }), + _ => Err(BrokerError::InvalidPolicyDecision), + } + } + + pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { + self.objects + .get(&object_id) + .ok_or(BrokerError::UnknownObject) + } + + pub(crate) fn object_mut(&mut self, object_id: ObjectId) -> Result<&mut ObjectEntry> { + self.objects + .get_mut(&object_id) + .ok_or(BrokerError::UnknownObject) + } + + fn validate_handle( + &self, + association: &BrokerAssociation, + handle: ObjectHandle, + expected_type: ObjectType, + required_rights: ObjectRights, + ) -> Result { + let reference = self.reference_for_handle(association, handle)?; + if reference.object_type != expected_type { + return Err(BrokerError::WrongObjectType); + } + if !reference.rights.contains(required_rights) { + return Err(BrokerError::InvalidRights); + } + + let object = self + .objects + .get(&reference.object_id) + .ok_or(BrokerError::UnknownObject)?; + if object.kind.object_type() != expected_type { + return Err(BrokerError::WrongObjectType); + } + + Ok(*reference) + } + + fn allocate_object_id(&mut self) -> Result { + allocate_id(&mut self.next_object_id).map(ObjectId::new) + } + + fn allocate_reference_id(&mut self) -> Result { + allocate_id(&mut self.next_reference_id).map(ObjectReferenceId::new) + } +} + +impl BrokerCore { + /// Closes one object reference owned by an association. + /// + /// The underlying object is released when this was the last live reference. + pub fn close_object_reference( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result<()> { + let object_id = self.reference_for_handle(association, handle)?.object_id; + if !self.objects.contains_key(&object_id) { + return Err(BrokerError::UnknownObject); + } + + self.references.remove(&handle.reference_id); + self.drop_object_if_unreferenced(object_id); + Ok(()) + } + + /// Closes a broker association and releases references owned by it. + pub fn close_association(&mut self, association: BrokerAssociation) { + let process_id = association.process_id(); + self.references + .retain(|_, reference| reference.owner != process_id); + let references = &self.references; + self.objects.retain(|object_id, _| { + references + .values() + .any(|reference| reference.object_id == *object_id) + }); + } + + fn reference_for_handle( + &self, + association: &BrokerAssociation, + handle: ObjectHandle, + ) -> Result<&ObjectReference> { + let reference = self + .references + .get(&handle.reference_id) + .ok_or(BrokerError::UnknownObject)?; + if reference.owner != association.process_id() { + return Err(BrokerError::UnknownObject); + } + if reference.reference_generation != handle.reference_generation { + return Err(BrokerError::StaleHandle); + } + Ok(reference) + } + + fn drop_object_if_unreferenced(&mut self, object_id: ObjectId) { + if !self + .references + .values() + .any(|reference| reference.object_id == object_id) + { + self.objects.remove(&object_id); + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct AuthorizedObject { + pub(crate) object_id: ObjectId, + pub(crate) rights: ObjectRights, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BrokerError, CallerCredential, PolicyEngine}; + use litebox_broker_protocol::WaitOutcome; + + #[test] + fn allocator_issues_max_id_then_exhausts() { + let mut next_id = u64::MAX; + + assert_eq!(allocate_id(&mut next_id), Ok(u64::MAX)); + assert_eq!(next_id, 0); + assert_eq!( + allocate_id(&mut next_id), + Err(BrokerError::ResourceExhausted) + ); + } + + #[test] + fn object_reference_lifecycle_uses_public_core_constructor_once() { + let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); + let owner = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let other = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let handle = core.create_event(&owner).unwrap(); + + assert_eq!( + core.close_object_reference(&other, handle), + Err(BrokerError::UnknownObject) + ); + + let stale = ObjectHandle::new( + handle.reference_id, + ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), + ); + assert_eq!( + core.close_object_reference(&owner, stale), + Err(BrokerError::StaleHandle) + ); + assert!(matches!( + core.wait_event(&owner, handle), + Ok(WaitOutcome::WouldBlock(_)) + )); + + assert_eq!(core.close_object_reference(&owner, handle), Ok(())); + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + assert_eq!( + core.close_object_reference(&owner, handle), + Err(BrokerError::UnknownObject) + ); + + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let _handle = core.create_event(&association).unwrap(); + assert_eq!(core.references.len(), 1); + assert_eq!(core.objects.len(), 1); + + core.close_association(association); + + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + } +} diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs new file mode 100644 index 000000000..1c2ecdaeb --- /dev/null +++ b/litebox_broker_core/src/policy.rs @@ -0,0 +1,247 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerError, CallerCredential, ObjectRights, ObjectType}; + +/// Broker operation submitted to the policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyOperation { + /// Perform an operation on a broker-owned object type. + Object { + /// Broker-entry-authenticated credential for the caller. + caller_credential: CallerCredential, + /// Object type targeted by the operation. + object_type: ObjectType, + /// Operation requested for the object type. + operation: ObjectOperation, + }, +} + +/// Generic object operation submitted to the policy engine. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ObjectOperation { + /// Create a new broker-owned object. + Create, + /// Use an existing object handle with the requested rights. + Use { rights: ObjectRights }, +} + +/// Policy decision returned after authorizing a broker operation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyDecision { + /// Operation is authorized and does not grant new authority material. + Authorized, + /// Object creation is authorized with rights for the initial object reference. + GrantObjectReference { + /// Rights to attach to the newly minted object reference. + rights: ObjectRights, + }, +} + +impl PolicyOperation { + /// Creates a policy operation for creating a broker-owned object type. + pub const fn create_object( + caller_credential: CallerCredential, + object_type: ObjectType, + ) -> Self { + Self::Object { + caller_credential, + object_type, + operation: ObjectOperation::Create, + } + } + + /// Creates a policy operation for using a broker-owned object with rights. + pub const fn use_object( + caller_credential: CallerCredential, + object_type: ObjectType, + rights: ObjectRights, + ) -> Self { + Self::Object { + caller_credential, + object_type, + operation: ObjectOperation::Use { rights }, + } + } +} + +/// Configured broker policy. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum PolicyProfile { + /// Deny every operation. + DefaultDeny, + /// Allow the current event-object surface. + EventOnly { + /// Rights to attach to newly created event references. + event_reference_rights: ObjectRights, + /// Maximum event rights this policy may authorize for use requests. + event_use_rights: ObjectRights, + }, +} + +/// Broker policy decision and audit component. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PolicyEngine { + profile: PolicyProfile, +} + +impl PolicyEngine { + /// Creates a policy engine from a policy profile. + pub const fn new(profile: PolicyProfile) -> Self { + Self { profile } + } + + /// Creates a policy engine that denies every operation. + pub const fn default_deny() -> Self { + Self::new(PolicyProfile::DefaultDeny) + } + + /// Creates a policy engine that allows only the current event-object surface. + pub const fn event_only() -> Self { + Self::event_only_with_reference_rights(EVENT_REFERENCE_RIGHTS) + } + + /// Creates an event-only policy engine with explicit initial reference rights. + /// + /// Use authorization still allows the normal event-only rights; BrokerCore's + /// reference validation enforces the rights on each created reference. + pub const fn event_only_with_reference_rights(event_reference_rights: ObjectRights) -> Self { + Self::new(PolicyProfile::EventOnly { + event_reference_rights, + event_use_rights: EVENT_REFERENCE_RIGHTS, + }) + } + + /// Authorizes or denies a broker operation. + pub(crate) fn authorize( + &mut self, + operation: PolicyOperation, + ) -> Result { + match self.profile { + PolicyProfile::DefaultDeny => Err(BrokerError::PolicyDenied), + PolicyProfile::EventOnly { + event_reference_rights, + event_use_rights, + } => authorize_event_only(event_reference_rights, event_use_rights, operation), + } + } +} + +impl Default for PolicyEngine { + fn default() -> Self { + Self::default_deny() + } +} + +/// Policy profile that allows only the current event-object surface. +/// +/// The default event create operation grants `WAIT | WRITE` on the initial +/// reference. Use requests may ask for any non-empty subset of configured event +/// use rights; BrokerCore separately enforces each reference's actual rights. +const EVENT_REFERENCE_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); + +fn authorize_event_only( + event_reference_rights: ObjectRights, + event_use_rights: ObjectRights, + operation: PolicyOperation, +) -> Result { + match operation { + PolicyOperation::Object { + caller_credential: CallerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Create, + } => Ok(PolicyDecision::GrantObjectReference { + rights: event_reference_rights, + }), + PolicyOperation::Object { + caller_credential: CallerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Use { rights }, + } if !rights.is_empty() && event_use_rights.contains(rights) => { + Ok(PolicyDecision::Authorized) + } + PolicyOperation::Object { + object_type: ObjectType::Event, + .. + } => Err(BrokerError::PolicyDenied), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn event_only_policy_allows_only_current_event_surface() { + let mut policy = PolicyEngine::event_only(); + + assert_eq!( + policy.authorize(PolicyOperation::create_object( + CallerCredential::Unauthenticated, + ObjectType::Event + )), + Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT | ObjectRights::WRITE + }) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT | ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::NONE + )), + Err(BrokerError::PolicyDenied) + ); + } + + #[test] + fn explicit_event_reference_rights_do_not_narrow_event_use_policy() { + let mut policy = PolicyEngine::event_only_with_reference_rights(ObjectRights::WAIT); + + assert_eq!( + policy.authorize(PolicyOperation::create_object( + CallerCredential::Unauthenticated, + ObjectType::Event + )), + Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT + }) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + CallerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WRITE + )), + Ok(PolicyDecision::Authorized) + ); + } +} diff --git a/litebox_broker_host/Cargo.toml b/litebox_broker_host/Cargo.toml new file mode 100644 index 000000000..fdeb2b609 --- /dev/null +++ b/litebox_broker_host/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_host" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_host/src/error.rs b/litebox_broker_host/src/error.rs new file mode 100644 index 000000000..81a34d646 --- /dev/null +++ b/litebox_broker_host/src/error.rs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// Errors returned by a broker-host receive/send loop. +#[derive(Debug)] +#[non_exhaustive] +pub enum BrokerHostError { + /// The host could not authenticate the peer or allocate broker association state. + AssociationSetup, + /// The concrete channel failed. + Channel(E), +} + +impl fmt::Display for BrokerHostError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AssociationSetup => f.write_str("broker association setup failed"), + Self::Channel(error) => write!(f, "broker channel failed: {error}"), + } + } +} + +impl core::error::Error for BrokerHostError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::AssociationSetup => None, + Self::Channel(error) => Some(error), + } + } +} + +/// Broker-host receive/send loop result type. +pub type Result = core::result::Result>; diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs new file mode 100644 index 000000000..e0cc97d7f --- /dev/null +++ b/litebox_broker_host/src/lib.rs @@ -0,0 +1,460 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Channel-neutral broker-side protocol/core adapter. +//! +//! This crate wires `litebox_broker_core` to any implementation of the neutral +//! host-side control-channel trait. Concrete channels live in separate crates such as +//! `litebox_broker_transport`. + +#![no_std] + +#[cfg(test)] +extern crate std; + +use core::fmt; + +use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCredential}; +use litebox_broker_protocol::{ + AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, + CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, + INITIAL_PROTOCOL_VERSION, PeerCredential, ProtocolVersion, ReceivedBrokerRequest, + WaitEventResponse, +}; + +mod error; + +pub use error::{BrokerHostError, Result}; + +/// Protocol version this broker host implementation supports. +pub const HOST_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +/// Serves one broker connection over the provided connected control channel. +pub fn serve_connection( + core: &mut BrokerCore, + channel: &mut T, +) -> Result +where + T: HostControlChannel, +{ + let peer_credential = channel + .peer_credential() + .map_err(BrokerHostError::Channel)?; + let caller_credential = caller_credential_from_peer(peer_credential) + .map_err(|()| BrokerHostError::AssociationSetup)?; + let association = core + .create_association(caller_credential) + .map_err(|_error| BrokerHostError::AssociationSetup)?; + + let result = serve_request_loop(core, channel, &association); + core.close_association(association); + result +} + +fn serve_request_loop( + core: &mut BrokerCore, + channel: &mut T, + association: &BrokerAssociation, +) -> Result +where + T: HostControlChannel, +{ + let mut state = ConnectionState::AwaitingNegotiation; + loop { + let Some(received) = channel.recv_request().map_err(BrokerHostError::Channel)? else { + break; + }; + + let dispatch = handle_received_request(core, association, &mut state, received); + channel + .send_response(&dispatch.response) + .map_err(BrokerHostError::Channel)?; + if let DispatchOutcome::Close(reason) = dispatch.outcome { + return Ok(ConnectionTermination::BrokerClosed(reason)); + } + } + + Ok(ConnectionTermination::PeerClosed) +} + +fn caller_credential_from_peer( + peer_credential: PeerCredential, +) -> core::result::Result { + if peer_credential == PeerCredential::Unauthenticated { + Ok(CallerCredential::Unauthenticated) + } else { + Err(()) + } +} + +fn handle_received_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + state: &mut ConnectionState, + received: ReceivedBrokerRequest, +) -> BrokerDispatch { + match received { + ReceivedBrokerRequest::Request(request) => { + handle_request(core, association, state, request) + } + _ => handle_unknown_request(*state), + } +} + +fn handle_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + state: &mut ConnectionState, + request: BrokerRequest, +) -> BrokerDispatch { + match *state { + ConnectionState::AwaitingNegotiation => match request { + BrokerRequest::Negotiate { protocol_version } => { + negotiate_version(state, protocol_version) + } + _ => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + }, + ConnectionState::Active { + negotiated_protocol_version, + } => handle_active_request(core, association, negotiated_protocol_version, request), + } +} + +fn handle_active_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + _negotiated_protocol_version: ProtocolVersion, + request: BrokerRequest, +) -> BrokerDispatch { + match request { + BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + BrokerRequest::Core(request) => { + BrokerDispatch::continue_after(handle_core_request(core, association, request)) + } + _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), + } +} + +fn handle_core_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + request: CoreRequest, +) -> BrokerResponse { + match request { + CoreRequest::Event(request) => handle_event_request(core, association, request), + _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), + } +} + +fn handle_event_request( + core: &mut BrokerCore, + association: &BrokerAssociation, + request: EventRequest, +) -> BrokerResponse { + match request { + EventRequest::Create(request) => handle_core_result( + core.create_event_with_count(association, request.initial_count), + |handle| event_response(EventResponse::Create(CreateEventResponse::new(handle))), + ), + EventRequest::Wait(request) => { + handle_core_result(core.wait_event(association, request.handle), |outcome| { + event_response(EventResponse::Wait(WaitEventResponse::new(outcome))) + }) + } + EventRequest::Add(request) => handle_core_result( + core.add_event(association, request.handle, request.value), + |readiness| event_response(EventResponse::Add(AddEventResponse::new(readiness))), + ), + EventRequest::Consume(request) => handle_core_result( + core.consume_event(association, request.handle, request.mode), + |consumption| event_response(EventResponse::Consume(consumption)), + ), + _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), + } +} + +fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { + if state == ConnectionState::AwaitingNegotiation { + BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ) + } else { + BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)) + } +} + +fn negotiate_version( + state: &mut ConnectionState, + protocol_version: ProtocolVersion, +) -> BrokerDispatch { + if protocol_version.is_supported_by(HOST_PROTOCOL_VERSION) { + *state = ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; + BrokerDispatch::continue_after(BrokerResponse::Negotiated { + broker_protocol_version: HOST_PROTOCOL_VERSION, + }) + } else { + BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { + broker_protocol_version: HOST_PROTOCOL_VERSION, + }) + } +} + +fn handle_core_result( + result: litebox_broker_core::Result, + into_response: impl FnOnce(T) -> BrokerResponse, +) -> BrokerResponse { + match result { + Ok(value) => into_response(value), + Err(error) => BrokerResponse::Error(to_protocol_error(error)), + } +} + +const fn event_response(response: EventResponse) -> BrokerResponse { + BrokerResponse::Core(CoreResponse::Event(response)) +} + +fn to_protocol_error(error: BrokerError) -> ErrorCode { + match error { + BrokerError::PolicyDenied => ErrorCode::PolicyDenied, + BrokerError::UnknownObject => ErrorCode::UnknownObject, + BrokerError::StaleHandle => ErrorCode::StaleHandle, + BrokerError::WrongObjectType => ErrorCode::WrongObjectType, + BrokerError::InvalidRights => ErrorCode::InvalidRights, + BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, + BrokerError::WouldBlock => ErrorCode::WouldBlock, + BrokerError::UnsupportedOperation => ErrorCode::UnsupportedOperation, + _ => ErrorCode::Internal, + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active { + negotiated_protocol_version: ProtocolVersion, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct BrokerDispatch { + response: BrokerResponse, + outcome: DispatchOutcome, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DispatchOutcome { + Continue, + Close(CloseReason), +} + +impl BrokerDispatch { + const fn continue_after(response: BrokerResponse) -> Self { + Self { + response, + outcome: DispatchOutcome::Continue, + } + } + + const fn close_after(response: BrokerResponse, reason: CloseReason) -> Self { + Self { + response, + outcome: DispatchOutcome::Close(reason), + } + } +} + +/// Reason the broker host closed the connection after sending a response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CloseReason { + /// The peer violated the request sequencing state machine. + ProtocolViolation, +} + +impl fmt::Display for CloseReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ProtocolViolation => f.write_str("protocol violation"), + } + } +} + +/// Terminal outcome for a successfully served broker connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ConnectionTermination { + /// The peer cleanly closed the channel. + PeerClosed, + /// The host sent a terminal protocol response and closed the connection. + BrokerClosed(CloseReason), +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_core::PolicyEngine; + use litebox_broker_protocol::CreateEventRequest; + + #[test] + fn host_request_handling_uses_one_broker_core() { + let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); + + serve_connection_negotiates_routes_one_request_and_returns_peer_closed(&mut core); + serve_connection_closes_after_protocol_violation(&mut core); + serve_connection_returns_channel_error_when_response_send_fails(&mut core); + } + + fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( + core: &mut BrokerCore, + ) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ))), + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, + )))), + Ok(None), + ])); + + assert_eq!( + serve_connection(core, &mut channel).unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.responses[0], + BrokerResponse::Negotiated { + broker_protocol_version: HOST_PROTOCOL_VERSION + } + ); + let handle = match &channel.responses[1] { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + response.handle + } + response => panic!("unexpected response: {response:?}"), + }; + assert_ne!(handle.reference_id.get(), 0); + } + + fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, + )))), + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ))), + ])); + + assert_eq!( + serve_connection(core, &mut channel).unwrap(), + ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) + ); + assert_eq!( + channel.responses, + [BrokerResponse::Error(ErrorCode::ProtocolState)] + ); + assert_eq!(channel.requests.len(), 1); + } + + fn serve_connection_returns_channel_error_when_response_send_fails(core: &mut BrokerCore) { + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( + ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }), + ))])); + channel.send_error = Some(FakeChannelError::Send); + + match serve_connection(core, &mut channel) { + Err(BrokerHostError::Channel(FakeChannelError::Send)) => {} + result => panic!("unexpected serve result: {result:?}"), + } + assert!(channel.responses.is_empty()); + } + + const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) + } + + const fn event_create_request(initial_count: u64) -> BrokerRequest { + event_request(EventRequest::Create(CreateEventRequest::new(initial_count))) + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum FakeChannelError { + Send, + } + + impl fmt::Display for FakeChannelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Send => f.write_str("fake send error"), + } + } + } + + impl core::error::Error for FakeChannelError {} + + struct FakeHostControlChannel { + requests: + std::vec::Vec, FakeChannelError>>, + responses: std::vec::Vec, + send_error: Option, + } + + impl FakeHostControlChannel { + fn new( + requests: std::vec::Vec< + core::result::Result, FakeChannelError>, + >, + ) -> Self { + Self { + requests, + responses: std::vec::Vec::new(), + send_error: None, + } + } + } + + impl HostControlChannel for FakeHostControlChannel { + type Error = FakeChannelError; + + fn peer_credential(&self) -> core::result::Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_request( + &mut self, + ) -> core::result::Result, Self::Error> { + if self.requests.is_empty() { + Ok(None) + } else { + self.requests.remove(0) + } + } + + fn send_response( + &mut self, + response: &BrokerResponse, + ) -> core::result::Result<(), Self::Error> { + if let Some(error) = self.send_error { + return Err(error); + } + self.responses.push(response.clone()); + Ok(()) + } + } +} diff --git a/litebox_broker_local/Cargo.toml b/litebox_broker_local/Cargo.toml new file mode 100644 index 000000000..d3574f925 --- /dev/null +++ b/litebox_broker_local/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_local" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs new file mode 100644 index 000000000..45696b20d --- /dev/null +++ b/litebox_broker_local/src/error.rs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; + +/// Errors returned by the broker-local control adapter. +#[derive(Debug)] +#[non_exhaustive] +pub enum BrokerLocalError { + /// The control channel failed. + Channel(E), + /// An operation requiring an active broker session was called before negotiation. + NotNegotiated, + /// Negotiation was requested after the local adapter was already active. + AlreadyNegotiated, + /// The broker closed the channel before returning a response. + ChannelClosed, + /// The broker returned a response this local adapter does not understand. + UnknownResponse, + /// The broker accepted negotiation with a version that cannot serve the request. + IncompatibleNegotiation { + /// Protocol version requested by this local adapter. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, + /// This local adapter cannot speak the requested protocol version. + UnsupportedLocalVersion { + /// Protocol version requested by the caller. + requested: ProtocolVersion, + /// Protocol version supported by this local implementation. + local_protocol_version: ProtocolVersion, + }, + /// The active broker session cannot serve an operation requiring a newer version. + UnsupportedNegotiatedVersion { + /// Protocol version required by the operation. + required: ProtocolVersion, + /// Effective protocol version negotiated for this connection. + negotiated_protocol_version: ProtocolVersion, + }, + /// The broker does not support the requested protocol version. + UnsupportedVersion { + /// Protocol version requested by this local adapter. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, + /// The broker rejected the request. + Broker(ErrorCode), + /// The broker returned a response type that does not match the request. + UnexpectedResponse(BrokerResponse), +} + +impl fmt::Display for BrokerLocalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Channel(error) => write!(f, "broker channel failed: {error}"), + Self::NotNegotiated => { + write!( + f, + "broker local adapter has not negotiated protocol version" + ) + } + Self::AlreadyNegotiated => f.write_str("broker local adapter already negotiated"), + Self::ChannelClosed => write!(f, "broker closed the channel"), + Self::UnknownResponse => f.write_str("unknown broker response"), + Self::IncompatibleNegotiation { + requested, + broker_protocol_version, + } => write!( + f, + "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" + ), + Self::UnsupportedLocalVersion { + requested, + local_protocol_version, + } => write!( + f, + "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" + ), + Self::UnsupportedNegotiatedVersion { + required, + negotiated_protocol_version, + } => write!( + f, + "broker session protocol version {negotiated_protocol_version:?} does not support required version {required:?}" + ), + Self::UnsupportedVersion { + requested, + broker_protocol_version, + } => write!( + f, + "broker does not support requested protocol version {requested:?}; broker supports {broker_protocol_version:?}" + ), + Self::Broker(error) => write!(f, "broker rejected request: {error}"), + Self::UnexpectedResponse(response) => { + write!(f, "broker returned unexpected response: {response:?}") + } + } + } +} + +impl core::error::Error for BrokerLocalError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Channel(error) => Some(error), + Self::Broker(error) => Some(error), + Self::NotNegotiated + | Self::AlreadyNegotiated + | Self::ChannelClosed + | Self::UnknownResponse + | Self::IncompatibleNegotiation { .. } + | Self::UnsupportedLocalVersion { .. } + | Self::UnsupportedNegotiatedVersion { .. } + | Self::UnsupportedVersion { .. } + | Self::UnexpectedResponse(_) => None, + } + } +} + +/// Broker-local control adapter result type. +pub type Result = core::result::Result>; diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs new file mode 100644 index 000000000..7c07501c5 --- /dev/null +++ b/litebox_broker_local/src/event.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::{ + AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, + CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, + INITIAL_PROTOCOL_VERSION, LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, + WaitEventRequest, WaitOutcome, +}; + +use crate::{BrokerLocal, BrokerLocalError, Result}; + +const EVENT_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +impl BrokerLocal { + /// Creates a broker-owned event object. + pub fn create_event(&mut self) -> Result { + self.create_event_with_count(0) + } + + /// Creates a broker-owned event object with initial readiness credits. + pub fn create_event_with_count( + &mut self, + initial_count: u64, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Create( + CreateEventRequest::new(initial_count), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + Ok(response.handle) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Checks whether an event wait would complete now. + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( + handle, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { + Ok(response.outcome) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( + &mut self, + handle: ObjectHandle, + value: u64, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Add(AddEventRequest::new( + handle, value, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { + Ok(response.readiness) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { + self.ensure_event_protocol()?; + match self.request(event_request(EventRequest::Consume( + ConsumeEventRequest::new(handle, mode), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { + Ok(response) + } + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } +} + +const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) +} + +impl BrokerLocal { + fn ensure_event_protocol(&self) -> Result<(), T::Error> { + let negotiated = self.ensure_negotiated()?; + if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { + Ok(()) + } else { + Err(BrokerLocalError::UnsupportedNegotiatedVersion { + required: EVENT_PROTOCOL_VERSION, + negotiated_protocol_version: negotiated, + }) + } + } +} diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs new file mode 100644 index 000000000..610cd6512 --- /dev/null +++ b/litebox_broker_local/src/lib.rs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Typed broker-local control adapter for broker requests. +//! +//! The local control adapter owns request/response sequencing but does not own a channel. +//! Userland, kernel, or ring-buffer deployments can provide channels by +//! implementing [`litebox_broker_protocol::LocalControlChannel`]. + +#![no_std] + +#[cfg(test)] +extern crate std; + +mod error; +mod event; + +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, INITIAL_PROTOCOL_VERSION, + LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, +}; + +pub use error::{BrokerLocalError, Result}; + +/// Protocol version this broker-local implementation requests by default. +pub const LOCAL_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; + +/// Typed broker-local control adapter for broker operations. +pub struct BrokerLocal { + channel: T, + state: ConnectionState, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active { + negotiated_protocol_version: ProtocolVersion, + }, +} + +impl BrokerLocal { + /// Creates a broker-local control adapter over an already-connected control channel. + pub const fn new(channel: T) -> Self { + Self { + channel, + state: ConnectionState::AwaitingNegotiation, + } + } + + /// Returns the underlying control channel for deployment-specific configuration. + pub fn control_channel_mut(&mut self) -> &mut T { + &mut self.channel + } +} + +impl BrokerLocal { + /// Negotiates the default broker-local protocol version. + /// + /// Returns the effective protocol version this connection will speak. + pub fn negotiate(&mut self) -> Result { + self.negotiate_version(LOCAL_PROTOCOL_VERSION) + } + + /// Negotiates a caller-selected protocol version. + /// + /// Returns the effective protocol version this connection will speak. Feature + /// gating must use this effective version, not the broker's max-supported + /// version returned by the wire negotiation response. + pub fn negotiate_version( + &mut self, + protocol_version: ProtocolVersion, + ) -> Result { + if self.state != ConnectionState::AwaitingNegotiation { + return Err(BrokerLocalError::AlreadyNegotiated); + } + if !protocol_version.is_supported_by(LOCAL_PROTOCOL_VERSION) { + return Err(BrokerLocalError::UnsupportedLocalVersion { + requested: protocol_version, + local_protocol_version: LOCAL_PROTOCOL_VERSION, + }); + } + + let response = self.request(BrokerRequest::Negotiate { protocol_version })?; + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + if !protocol_version.is_supported_by(broker_protocol_version) { + return Err(BrokerLocalError::IncompatibleNegotiation { + requested: protocol_version, + broker_protocol_version, + }); + } + self.state = ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; + Ok(protocol_version) + } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => Err(BrokerLocalError::UnsupportedVersion { + requested: protocol_version, + broker_protocol_version, + }), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } + + /// Returns the effective protocol version this connection negotiated. + /// + /// Feature gating must use this effective version because the broker may + /// support a newer minor version than this local adapter requested. + pub fn negotiated_protocol_version(&self) -> Option { + match self.state { + ConnectionState::AwaitingNegotiation => None, + ConnectionState::Active { + negotiated_protocol_version, + } => Some(negotiated_protocol_version), + } + } + + pub(crate) fn ensure_negotiated(&self) -> Result { + match self.state { + ConnectionState::AwaitingNegotiation => Err(BrokerLocalError::NotNegotiated), + ConnectionState::Active { + negotiated_protocol_version, + } => Ok(negotiated_protocol_version), + } + } + + pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { + match self.raw_request(request)? { + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), + response => Ok(response), + } + } + + fn raw_request(&mut self, request: BrokerRequest) -> Result { + self.channel + .send_request(&request) + .map_err(BrokerLocalError::Channel)?; + match self + .channel + .recv_response() + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed)? + { + ReceivedBrokerResponse::Response(response) => Ok(response), + _ => Err(BrokerLocalError::UnknownResponse), + } + } + + /// Sends one BrokerCore request on an active connection. + pub fn active_core_request(&mut self, request: CoreRequest) -> Result { + self.ensure_negotiated()?; + match self.request(BrokerRequest::Core(request))? { + BrokerResponse::Core(response) => Ok(response), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::convert::Infallible; + use litebox_broker_protocol::ProtocolVersion; + + #[test] + fn event_operations_require_negotiation_without_sending() { + let channel = FakeControlChannel::new(None); + let mut local = BrokerLocal::new(channel); + + assert!(matches!( + local.create_event(), + Err(BrokerLocalError::NotNegotiated) + )); + assert_eq!(local.channel.sent_request, None); + } + + #[test] + fn negotiate_sends_default_version_and_activates_local_connection() { + let requested = LOCAL_PROTOCOL_VERSION; + let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { + broker_protocol_version: LOCAL_PROTOCOL_VERSION, + })); + let mut local = BrokerLocal::new(channel); + + assert_eq!(local.negotiate().unwrap(), requested); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest::Negotiate { + protocol_version: requested + }) + ); + assert_eq!(local.negotiated_protocol_version(), Some(requested)); + } + + #[test] + fn negotiate_version_rejects_locally_unsupported_version_without_sending() { + let too_new = ProtocolVersion::new( + LOCAL_PROTOCOL_VERSION.major, + LOCAL_PROTOCOL_VERSION.minor + 1, + ); + let channel = FakeControlChannel::new(None); + let mut local = BrokerLocal::new(channel); + + assert!(matches!( + local.negotiate_version(too_new), + Err(BrokerLocalError::UnsupportedLocalVersion { + requested, + local_protocol_version + }) if requested == too_new && local_protocol_version == LOCAL_PROTOCOL_VERSION + )); + assert_eq!(local.negotiated_protocol_version(), None); + assert_eq!(local.channel.sent_request, None); + } + + #[test] + fn active_core_request_wraps_request_and_unwraps_response() { + use litebox_broker_protocol::{ + CoreRequest, CoreResponse, EventRequest, EventResponse, ObjectHandle, + ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, WaitEventRequest, + WaitEventResponse, WaitOutcome, + }; + + let handle = + ObjectHandle::new(ObjectReferenceId::new(7), ObjectReferenceGeneration::new(1)); + let request = CoreRequest::Event(EventRequest::Wait(WaitEventRequest::new(handle))); + let response = CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)), + ))); + let channel = FakeControlChannel::new(Some(BrokerResponse::Core(response.clone()))); + let mut local = BrokerLocal::new(channel); + local.state = ConnectionState::Active { + negotiated_protocol_version: LOCAL_PROTOCOL_VERSION, + }; + + assert_eq!( + local.active_core_request(request.clone()).unwrap(), + response + ); + assert_eq!( + local.channel.sent_request, + Some(BrokerRequest::Core(request)) + ); + } + + struct FakeControlChannel { + sent_request: Option, + response: Option, + } + + impl FakeControlChannel { + const fn new(response: Option) -> Self { + Self { + sent_request: None, + response, + } + } + } + + impl LocalControlChannel for FakeControlChannel { + type Error = Infallible; + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.sent_request = Some(request.clone()); + Ok(()) + } + + fn recv_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(self.response.take().map(ReceivedBrokerResponse::Response)) + } + } +} diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml new file mode 100644 index 000000000..2fccbca4e --- /dev/null +++ b/litebox_broker_protocol/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "litebox_broker_protocol" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs new file mode 100644 index 000000000..83f4d848d --- /dev/null +++ b/litebox_broker_protocol/src/channel.rs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerRequest, BrokerResponse}; + +/// Peer identity information supplied by the channel or host layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but channels still return an explicit credential value so the host layer +/// can map authenticated peer identity into BrokerCore caller identity. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum PeerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + /// + /// Channels that are expected to authenticate peers must return an error + /// from [`HostControlChannel::peer_credential`] when authentication is + /// unavailable or fails; this variant is only for deployments that + /// deliberately choose unauthenticated operation. + Unauthenticated, +} + +/// Broker authority request received from a control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ReceivedBrokerRequest { + /// A request understood by the current protocol crate. + Request(BrokerRequest), + /// A request emitted by a newer peer and not understood by this process. + Unknown, +} + +impl From for ReceivedBrokerRequest { + fn from(request: BrokerRequest) -> Self { + Self::Request(request) + } +} + +/// Broker authority response received from a control channel. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum ReceivedBrokerResponse { + /// A response understood by the current protocol crate. + Response(BrokerResponse), + /// A response emitted by a newer broker and not understood by this process. + Unknown, +} + +impl From for ReceivedBrokerResponse { + fn from(response: BrokerResponse) -> Self { + Self::Response(response) + } +} + +/// Local-side control channel for broker authority calls. +pub trait LocalControlChannel { + /// Channel-specific error type. + type Error; + + /// Sends one broker request. + fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error>; + + /// Receives one broker response. + /// + /// Returns `Ok(None)` when the broker closed the channel cleanly before + /// starting another response frame. + fn recv_response(&mut self) -> Result, Self::Error>; +} + +/// Host-side control channel for broker authority calls. +pub trait HostControlChannel { + /// Channel-specific error type. + type Error; + + /// Returns the peer credential authenticated for this channel endpoint. + fn peer_credential(&self) -> Result; + + /// Receives one broker request. + /// + /// Returns `Ok(None)` when the peer closed the channel cleanly before + /// starting another request frame. + fn recv_request(&mut self) -> Result, Self::Error>; + + /// Sends one broker response. + fn send_response(&mut self, response: &BrokerResponse) -> Result<(), Self::Error>; +} diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs new file mode 100644 index 000000000..e2113225f --- /dev/null +++ b/litebox_broker_protocol/src/error.rs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// ABI-neutral broker error category. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum ErrorCode { + /// The requested protocol version is unsupported. + UnsupportedVersion, + /// The request is structurally invalid. + MalformedRequest, + /// The request is validly encoded but violates the connection state machine. + ProtocolState, + /// The request is unsupported by this broker protocol implementation. + UnsupportedOperation, + /// Broker hit an internal condition or an error category this protocol cannot represent. + Internal, + /// Policy denied the operation. + PolicyDenied, + /// The referenced object does not exist. + UnknownObject, + /// The referenced object generation is stale. + StaleHandle, + /// The referenced object type does not match the operation. + WrongObjectType, + /// The caller lacks the required broker rights. + InvalidRights, + /// Broker-side resource exhaustion. + ResourceExhausted, + /// The operation would block in the current event state. + WouldBlock, + /// Error code emitted by a newer broker and not understood by this local peer. + /// + /// This variant is reserved for raw codes not assigned by this protocol + /// version. + Unknown(u16), +} + +impl ErrorCode { + /// Raw error values are part of the broker wire ABI; do not renumber + /// assigned values. + /// + /// Values `0` and `1` remain unassigned so null/default-looking values never + /// represent concrete broker errors. + /// + /// Converts a raw protocol error code to an error category. + pub const fn from_raw(raw: u16) -> Self { + match raw { + 2 => Self::UnsupportedVersion, + 3 => Self::MalformedRequest, + 10 => Self::ProtocolState, + 11 => Self::UnsupportedOperation, + 12 => Self::Internal, + 4 => Self::PolicyDenied, + 5 => Self::UnknownObject, + 6 => Self::StaleHandle, + 7 => Self::WrongObjectType, + 8 => Self::InvalidRights, + 9 => Self::ResourceExhausted, + 13 => Self::WouldBlock, + raw => Self::Unknown(raw), + } + } + + /// Returns the raw protocol error code. + pub const fn as_raw(self) -> u16 { + match self { + Self::UnsupportedVersion => 2, + Self::MalformedRequest => 3, + Self::ProtocolState => 10, + Self::UnsupportedOperation => 11, + Self::Internal => 12, + Self::PolicyDenied => 4, + Self::UnknownObject => 5, + Self::StaleHandle => 6, + Self::WrongObjectType => 7, + Self::InvalidRights => 8, + Self::ResourceExhausted => 9, + Self::WouldBlock => 13, + Self::Unknown(raw) => raw, + } + } +} + +impl fmt::Display for ErrorCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedVersion => f.write_str("unsupported broker protocol version"), + Self::MalformedRequest => f.write_str("malformed broker request"), + Self::ProtocolState => f.write_str("broker protocol state violation"), + Self::UnsupportedOperation => f.write_str("unsupported broker operation"), + Self::Internal => f.write_str("internal broker error"), + Self::PolicyDenied => f.write_str("broker policy denied the operation"), + Self::UnknownObject => f.write_str("unknown broker object"), + Self::StaleHandle => f.write_str("stale broker handle"), + Self::WrongObjectType => f.write_str("wrong broker object type"), + Self::InvalidRights => f.write_str("invalid broker rights"), + Self::ResourceExhausted => f.write_str("broker resource exhausted"), + Self::WouldBlock => f.write_str("broker operation would block"), + Self::Unknown(raw) => write!(f, "unknown broker error code {raw}"), + } + } +} + +impl core::error::Error for ErrorCode {} diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs new file mode 100644 index 000000000..efbaf7137 --- /dev/null +++ b/litebox_broker_protocol/src/event.rs @@ -0,0 +1,167 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::ObjectHandle; + +/// Broker-authoritative readiness state for one object. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReadinessState { + /// Whether an event read/consume operation can complete without blocking. + pub read_ready: bool, + /// Whether an event write/add operation can complete without blocking. + pub write_ready: bool, + /// Monotonic readiness generation used to invalidate user-side readiness caches. + pub generation: u64, +} + +impl ReadinessState { + /// Creates a readiness state. + pub const fn new(read_ready: bool, write_ready: bool, generation: u64) -> Self { + Self { + read_ready, + write_ready, + generation, + } + } +} + +/// Result of checking whether a broker event read wait would complete now. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WaitOutcome { + /// The object is read-ready now. + Ready(ReadinessState), + /// The object is not read-ready; deployment-specific wait plumbing may block. + WouldBlock(ReadinessState), +} + +/// How a broker event consume operation should remove readiness credits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventConsumeMode { + /// Consume all currently available credits. + All, + /// Consume one credit. + One, +} + +/// Request to create a broker-owned event object. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct CreateEventRequest { + /// Initial readiness credits. + pub initial_count: u64, +} + +impl CreateEventRequest { + /// Creates an event create request. + pub const fn new(initial_count: u64) -> Self { + Self { initial_count } + } +} + +/// Response to an event create request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CreateEventResponse { + /// Created event handle. + pub handle: ObjectHandle, +} + +impl CreateEventResponse { + /// Creates an event create response. + pub const fn new(handle: ObjectHandle) -> Self { + Self { handle } + } +} + +/// Request to check whether an event wait would complete now. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WaitEventRequest { + /// Event handle. + pub handle: ObjectHandle, +} + +impl WaitEventRequest { + /// Creates an event wait request. + pub const fn new(handle: ObjectHandle) -> Self { + Self { handle } + } +} + +/// Response to an event wait request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WaitEventResponse { + /// Current wait outcome. + pub outcome: WaitOutcome, +} + +impl WaitEventResponse { + /// Creates an event wait response. + pub const fn new(outcome: WaitOutcome) -> Self { + Self { outcome } + } +} + +/// Request to add readiness credits to an event. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AddEventRequest { + /// Event handle. + pub handle: ObjectHandle, + /// Readiness credits to add. + pub value: u64, +} + +impl AddEventRequest { + /// Creates an event add request. + pub const fn new(handle: ObjectHandle, value: u64) -> Self { + Self { handle, value } + } +} + +/// Response to an event add request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct AddEventResponse { + /// Readiness state after adding credits. + pub readiness: ReadinessState, +} + +impl AddEventResponse { + /// Creates an event add response. + pub const fn new(readiness: ReadinessState) -> Self { + Self { readiness } + } +} + +/// Request to consume readiness credits from an event. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConsumeEventRequest { + /// Event handle. + pub handle: ObjectHandle, + /// Consume mode. + pub mode: EventConsumeMode, +} + +impl ConsumeEventRequest { + /// Creates an event consume request. + pub const fn new(handle: ObjectHandle, mode: EventConsumeMode) -> Self { + Self { handle, mode } + } +} + +/// Result of consuming readiness credits from a broker-owned event object. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct EventConsumption { + /// Number of readiness credits consumed. + pub value: u64, + /// Readiness state after consuming credits. + pub readiness: ReadinessState, +} + +impl EventConsumption { + /// Creates an event consumption result. + pub const fn new(value: u64, readiness: ReadinessState) -> Self { + Self { value, readiness } + } +} + +/// Response to an event consume request. +pub type ConsumeEventResponse = EventConsumption; diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs new file mode 100644 index 000000000..0f081bec3 --- /dev/null +++ b/litebox_broker_protocol/src/lib.rs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared broker protocol types and channel contracts. +//! +//! This crate describes broker-visible opaque handles, errors, versions, +//! request/response messages, and the transport-neutral control-channel +//! contracts used to carry them. It does not know whether messages move over +//! Unix sockets, shared rings, kernel traps, or another IPC mechanism. + +#![no_std] + +extern crate alloc; + +pub mod channel; +pub mod error; +pub mod event; +pub mod message; +pub mod object; +pub mod wire; + +pub use channel::{ + HostControlChannel, LocalControlChannel, PeerCredential, ReceivedBrokerRequest, + ReceivedBrokerResponse, +}; +pub use error::ErrorCode; +pub use event::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, + WaitEventRequest, WaitEventResponse, WaitOutcome, +}; +pub use message::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, +}; +pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; + +/// Major/minor broker protocol version. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ProtocolVersion { + /// Incompatible protocol version. + pub major: u16, + /// Backward-compatible protocol revision within a major version. + pub minor: u16, +} + +impl ProtocolVersion { + /// Creates a protocol version. + pub const fn new(major: u16, minor: u16) -> Self { + Self { major, minor } + } + + /// Returns whether this requested version is supported by `supported`. + /// + /// Minor revisions are backward-compatible within a major version, so a + /// broker can serve a peer requesting the same major version and a minor + /// version no newer than the broker supports. + pub const fn is_supported_by(self, supported: Self) -> bool { + self.major == supported.major && self.minor <= supported.minor + } +} + +/// Initial broker protocol version implemented by the split-broker POC. +pub const INITIAL_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs new file mode 100644 index 000000000..2b321711b --- /dev/null +++ b/litebox_broker_protocol/src/message.rs @@ -0,0 +1,101 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::ProtocolVersion; +use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, ErrorCode, WaitEventRequest, WaitEventResponse, +}; + +/// Broker request sent over the control channel. +/// +/// The outer broker request is intentionally small. Object-family and +/// domain-specific operations are grouped below it so new object families do not +/// accumulate as unrelated top-level broker variants. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerRequest { + /// Protocol negotiation request. + Negotiate { + /// Required protocol version. + protocol_version: ProtocolVersion, + }, + /// BrokerCore authority request. + Core(CoreRequest), +} + +/// Request adapted by the broker host into a BrokerCore domain call. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreRequest { + /// Event object request family. + Event(EventRequest), +} + +/// Broker-owned event object request. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventRequest { + /// Create a broker-owned event object. + Create(CreateEventRequest), + /// Check whether an event wait would complete now. + Wait(WaitEventRequest), + /// Add readiness credits to an event. + Add(AddEventRequest), + /// Consume readiness credits from an event. + Consume(ConsumeEventRequest), +} + +/// Broker response sent over the control channel. +/// +/// Common connection/protocol outcomes stay at this layer. Domain payloads are +/// grouped under [`CoreResponse`] so future object families can evolve without +/// turning the broker envelope into a flat operation/result list. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerResponse { + /// Negotiation result. + Negotiated { + /// Broker protocol version supported by this endpoint. + /// + /// The broker returns its supported version after validating that the + /// requested version is supported according to + /// [`ProtocolVersion::is_supported_by`](crate::ProtocolVersion::is_supported_by). + broker_protocol_version: ProtocolVersion, + }, + /// Negotiation failed because the requested version is unsupported. + /// + /// The connection remains in negotiation state and the local peer may retry + /// with a compatible version using the broker-supported version advertised + /// here. + VersionMismatch { + /// Broker protocol version supported by this endpoint. + broker_protocol_version: ProtocolVersion, + }, + /// BrokerCore authority response. + Core(CoreResponse), + /// Operation failed with an ABI-neutral broker error. + Error(ErrorCode), +} + +/// Response returned by a BrokerCore domain request. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreResponse { + /// Event object response family. + Event(EventResponse), +} + +/// Broker-owned event object response. +#[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventResponse { + /// Create operation response. + Create(CreateEventResponse), + /// Wait operation response. + Wait(WaitEventResponse), + /// Add operation response. + Add(AddEventResponse), + /// Consume operation response. + Consume(ConsumeEventResponse), +} diff --git a/litebox_broker_protocol/src/object.rs b/litebox_broker_protocol/src/object.rs new file mode 100644 index 000000000..141be47be --- /dev/null +++ b/litebox_broker_protocol/src/object.rs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// Broker object reference handle returned to the local core. +/// +/// The local core may cache this value, but the broker remains authoritative for +/// object identity, object lifetime, reference lifetime, type, rights, and +/// reference generation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ObjectHandle { + /// Opaque broker reference identifier owned by one authenticated process association. + pub reference_id: ObjectReferenceId, + /// Reference generation used to reject stale handles after reference-slot reuse. + pub reference_generation: ObjectReferenceGeneration, +} + +impl ObjectHandle { + /// Creates an object handle. + pub const fn new( + reference_id: ObjectReferenceId, + reference_generation: ObjectReferenceGeneration, + ) -> Self { + Self { + reference_id, + reference_generation, + } + } +} + +/// Broker-owned object reference identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectReferenceId(u64); + +impl ObjectReferenceId { + /// Creates an object reference identifier from its raw protocol value. + pub const fn new(raw: u64) -> Self { + Self(raw) + } + + /// Returns the raw protocol value. + pub const fn get(self) -> u64 { + self.0 + } +} + +/// Generation attached to a broker object reference. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectReferenceGeneration(u64); + +impl ObjectReferenceGeneration { + /// Creates a reference generation from its raw protocol value. + pub const fn new(raw: u64) -> Self { + Self(raw) + } + + /// Returns the raw protocol value. + pub const fn get(self) -> u64 { + self.0 + } +} diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs new file mode 100644 index 000000000..af5a3a171 --- /dev/null +++ b/litebox_broker_protocol/src/wire.rs @@ -0,0 +1,314 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Reusable byte codec for broker request/response control-channel messages. +//! +//! The wire codec mirrors the protocol DTO hierarchy: +//! - this module owns public encode/decode entry points and top-level broker +//! envelope tags; +//! - `core_message` owns `CoreRequest`/`CoreResponse` family tags; +//! - object-family modules such as `event` own their operation and nested value +//! tags; +//! - `primitive` owns shared scalar/value encoders. +//! +//! New object families should add a core family tag and a private family codec +//! module instead of adding flat helpers here. Existing payloads are positional; +//! changing fields is an ABI change, so prefer a new operation tag or explicit +//! negotiated-version gate for payload evolution. + +use core::fmt; + +use alloc::vec::Vec; + +use crate::{ + BrokerRequest, BrokerResponse, ErrorCode, ReceivedBrokerRequest, ReceivedBrokerResponse, +}; + +use primitive::{Decoder, Encoder}; + +mod core_message; +mod event; +mod primitive; + +const REQUEST_TAG_NEGOTIATE: u8 = 0; +const REQUEST_TAG_CORE: u8 = 1; + +const RESPONSE_TAG_NEGOTIATED: u8 = 0; +const RESPONSE_TAG_CORE: u8 = 1; +const RESPONSE_TAG_ERROR: u8 = 2; +const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; + +/// Error produced while encoding or decoding a broker wire message. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WireError { + /// The frame ended before a complete field could be decoded. + TruncatedFrame, + /// The frame contained bytes after the decoded message. + TrailingBytes, + /// A boolean field was not encoded as 0 or 1. + InvalidBoolean, + /// A decoder offset overflowed. + OffsetOverflow, +} + +impl fmt::Display for WireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TruncatedFrame => f.write_str("truncated broker wire frame"), + Self::TrailingBytes => f.write_str("trailing broker wire bytes"), + Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), + Self::OffsetOverflow => f.write_str("broker wire offset overflow"), + } + } +} + +impl core::error::Error for WireError {} + +/// Encodes a broker request body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_request(request: BrokerRequest) -> Vec { + let mut encoder = Encoder::default(); + match request { + BrokerRequest::Negotiate { protocol_version } => { + encoder.u8(REQUEST_TAG_NEGOTIATE); + encoder.protocol_version(protocol_version); + } + BrokerRequest::Core(request) => { + encoder.u8(REQUEST_TAG_CORE); + core_message::encode_core_request(&mut encoder, request); + } + } + encoder.finish() +} + +/// Decodes a broker request body. +pub fn decode_request(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let request = match tag { + REQUEST_TAG_NEGOTIATE => BrokerRequest::Negotiate { + protocol_version: decoder.protocol_version()?, + }, + REQUEST_TAG_CORE => match core_message::decode_core_request(&mut decoder)? { + Some(request) => BrokerRequest::Core(request), + None => return Ok(ReceivedBrokerRequest::Unknown), + }, + _ => return Ok(ReceivedBrokerRequest::Unknown), + }; + decoder.finish()?; + Ok(ReceivedBrokerRequest::Request(request)) +} + +/// Encodes a broker response body. +/// +/// Successful encodings are always non-empty because the first byte is the +/// message tag. +pub fn encode_response(response: BrokerResponse) -> Vec { + let mut encoder = Encoder::default(); + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_NEGOTIATED); + encoder.protocol_version(broker_protocol_version); + } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_VERSION_MISMATCH); + encoder.protocol_version(broker_protocol_version); + } + BrokerResponse::Core(response) => { + encoder.u8(RESPONSE_TAG_CORE); + core_message::encode_core_response(&mut encoder, response); + } + BrokerResponse::Error(error) => { + encoder.u8(RESPONSE_TAG_ERROR); + encoder.u16(error.as_raw()); + } + } + encoder.finish() +} + +/// Decodes a broker response body. +pub fn decode_response(frame: &[u8]) -> Result { + let mut decoder = Decoder::new(frame); + let tag = decoder.u8()?; + let response = match tag { + RESPONSE_TAG_NEGOTIATED => BrokerResponse::Negotiated { + broker_protocol_version: decoder.protocol_version()?, + }, + RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { + broker_protocol_version: decoder.protocol_version()?, + }, + RESPONSE_TAG_CORE => match core_message::decode_core_response(&mut decoder)? { + Some(response) => BrokerResponse::Core(response), + None => return Ok(ReceivedBrokerResponse::Unknown), + }, + RESPONSE_TAG_ERROR => { + let error = ErrorCode::from_raw(decoder.u16()?); + BrokerResponse::Error(error) + } + _ => return Ok(ReceivedBrokerResponse::Unknown), + }; + decoder.finish()?; + Ok(ReceivedBrokerResponse::Response(response)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, + CoreResponse, CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, + EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, + ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, + }; + + #[test] + fn request_codec_round_trips_all_variants() { + let handle = sample_handle(); + let requests = [ + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(1, 0), + }, + event_request(EventRequest::Create(CreateEventRequest::new(0))), + event_request(EventRequest::Create(CreateEventRequest::new(7))), + event_request(EventRequest::Wait(WaitEventRequest::new(handle))), + event_request(EventRequest::Add(AddEventRequest::new(handle, 3))), + event_request(EventRequest::Consume(ConsumeEventRequest::new( + handle, + EventConsumeMode::All, + ))), + event_request(EventRequest::Consume(ConsumeEventRequest::new( + handle, + EventConsumeMode::One, + ))), + ]; + + for request in requests { + assert_eq!( + decode_request(&encode_request(request.clone())).unwrap(), + ReceivedBrokerRequest::Request(request) + ); + } + } + + #[test] + fn response_codec_round_trips_all_variants() { + let handle = sample_handle(); + let responses = [ + BrokerResponse::Negotiated { + broker_protocol_version: ProtocolVersion::new(1, 0), + }, + BrokerResponse::VersionMismatch { + broker_protocol_version: ProtocolVersion::new(1, 0), + }, + event_response(EventResponse::Create(CreateEventResponse::new(handle))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, false, 8)), + ))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 9)), + ))), + event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, true, 10), + ))), + event_response(EventResponse::Consume(ConsumeEventResponse::new( + 3, + ReadinessState::new(false, true, 11), + ))), + BrokerResponse::Error(ErrorCode::PolicyDenied), + BrokerResponse::Error(ErrorCode::WouldBlock), + BrokerResponse::Error(ErrorCode::Internal), + ]; + + for response in responses { + assert_eq!( + decode_response(&encode_response(response.clone())).unwrap(), + ReceivedBrokerResponse::Response(response) + ); + } + } + + #[test] + fn decode_rejects_malformed_request_frames() { + assert_eq!( + decode_request(&[0xff, 1, 2, 3]), + Ok(ReceivedBrokerRequest::Unknown) + ); + let mut unknown_consume_mode = encode_request(event_request(EventRequest::Consume( + ConsumeEventRequest::new(sample_handle(), EventConsumeMode::All), + ))); + *unknown_consume_mode.last_mut().unwrap() = 0xff; + assert_eq!( + decode_request(&unknown_consume_mode), + Ok(ReceivedBrokerRequest::Unknown) + ); + assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); + let mut frame = encode_request(event_request(EventRequest::Create( + CreateEventRequest::new(0), + ))); + frame.push(0xff); + assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); + } + + #[test] + fn decode_rejects_malformed_response_frames() { + assert_eq!( + decode_response(&[0xff, 1, 2, 3]), + Ok(ReceivedBrokerResponse::Unknown) + ); + assert_eq!( + decode_response(&[1, 0, 1, 0xff]), + Ok(ReceivedBrokerResponse::Unknown) + ); + assert_eq!( + decode_response(&[2, 0xff, 0xff]), + Ok(ReceivedBrokerResponse::Response(BrokerResponse::Error( + ErrorCode::Unknown(0xffff) + ))) + ); + + let mut invalid_bool = [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!( + decode_response(&invalid_bool), + Err(WireError::InvalidBoolean) + ); + + invalid_bool[3] = 1; + invalid_bool[4] = 1; + invalid_bool[12] = 1; + let mut frame = invalid_bool.to_vec(); + frame.push(0xff); + assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); + } + + #[test] + fn event_add_response_wire_shape_is_pinned() { + assert_eq!( + encode_response(event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, false, 0x0102_0304_0506_0708) + )))), + [1, 0, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1] + ); + } + + const fn sample_handle() -> ObjectHandle { + ObjectHandle::new( + ObjectReferenceId::new(13), + ObjectReferenceGeneration::new(14), + ) + } + + const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) + } + + const fn event_response(response: EventResponse) -> BrokerResponse { + BrokerResponse::Core(CoreResponse::Event(response)) + } +} diff --git a/litebox_broker_protocol/src/wire/core_message.rs b/litebox_broker_protocol/src/wire/core_message.rs new file mode 100644 index 000000000..dbd0665da --- /dev/null +++ b/litebox_broker_protocol/src/wire/core_message.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{CoreRequest, CoreResponse}; + +use super::WireError; +use super::event; +use super::primitive::{Decoder, Encoder}; + +// Core tags select object-family codecs. Add new object families here, then +// keep their operation-specific tags inside a dedicated family module. +const CORE_REQUEST_TAG_EVENT: u8 = 0; +const CORE_RESPONSE_TAG_EVENT: u8 = 0; + +pub(super) fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { + match request { + CoreRequest::Event(request) => { + encoder.u8(CORE_REQUEST_TAG_EVENT); + event::encode_event_request(encoder, request); + } + } +} + +pub(super) fn decode_core_request( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let request = match decoder.u8()? { + CORE_REQUEST_TAG_EVENT => match event::decode_event_request(decoder)? { + Some(request) => CoreRequest::Event(request), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(request)) +} + +pub(super) fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { + match response { + CoreResponse::Event(response) => { + encoder.u8(CORE_RESPONSE_TAG_EVENT); + event::encode_event_response(encoder, response); + } + } +} + +pub(super) fn decode_core_response( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let response = match decoder.u8()? { + CORE_RESPONSE_TAG_EVENT => match event::decode_event_response(decoder)? { + Some(response) => CoreResponse::Event(response), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(response)) +} diff --git a/litebox_broker_protocol/src/wire/event.rs b/litebox_broker_protocol/src/wire/event.rs new file mode 100644 index 000000000..eaf4cfa01 --- /dev/null +++ b/litebox_broker_protocol/src/wire/event.rs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventRequest, EventResponse, + ReadinessState, WaitEventRequest, WaitEventResponse, WaitOutcome, +}; + +use super::WireError; +use super::primitive::{Decoder, Encoder}; + +// Event operation tags live with the event family. Future event operations +// should add tags here; unrelated object families should get their own module. +const EVENT_REQUEST_TAG_CREATE: u8 = 0; +const EVENT_REQUEST_TAG_WAIT: u8 = 1; +const EVENT_REQUEST_TAG_ADD: u8 = 2; +const EVENT_REQUEST_TAG_CONSUME: u8 = 3; + +const EVENT_RESPONSE_TAG_CREATED: u8 = 0; +const EVENT_RESPONSE_TAG_WAITED: u8 = 1; +const EVENT_RESPONSE_TAG_ADDED: u8 = 2; +const EVENT_RESPONSE_TAG_CONSUMED: u8 = 3; + +const WAIT_OUTCOME_TAG_READY: u8 = 1; +const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; +const EVENT_CONSUME_MODE_TAG_ALL: u8 = 1; +const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; + +pub(super) fn encode_event_request(encoder: &mut Encoder, request: EventRequest) { + match request { + EventRequest::Create(request) => { + encoder.u8(EVENT_REQUEST_TAG_CREATE); + encoder.u64(request.initial_count); + } + EventRequest::Wait(request) => { + encoder.u8(EVENT_REQUEST_TAG_WAIT); + encoder.handle(request.handle); + } + EventRequest::Add(request) => { + encoder.u8(EVENT_REQUEST_TAG_ADD); + encoder.handle(request.handle); + encoder.u64(request.value); + } + EventRequest::Consume(request) => { + encoder.u8(EVENT_REQUEST_TAG_CONSUME); + encoder.handle(request.handle); + encode_consume_mode(encoder, request.mode); + } + } +} + +pub(super) fn decode_event_request( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let request = match decoder.u8()? { + EVENT_REQUEST_TAG_CREATE => EventRequest::Create(CreateEventRequest::new(decoder.u64()?)), + EVENT_REQUEST_TAG_WAIT => EventRequest::Wait(WaitEventRequest::new(decoder.handle()?)), + EVENT_REQUEST_TAG_ADD => { + EventRequest::Add(AddEventRequest::new(decoder.handle()?, decoder.u64()?)) + } + EVENT_REQUEST_TAG_CONSUME => EventRequest::Consume(ConsumeEventRequest::new( + decoder.handle()?, + match decode_consume_mode(decoder)? { + Some(mode) => mode, + None => return Ok(None), + }, + )), + _ => return Ok(None), + }; + + Ok(Some(request)) +} + +pub(super) fn encode_event_response(encoder: &mut Encoder, response: EventResponse) { + match response { + EventResponse::Create(response) => { + encoder.u8(EVENT_RESPONSE_TAG_CREATED); + encoder.handle(response.handle); + } + EventResponse::Wait(response) => { + encoder.u8(EVENT_RESPONSE_TAG_WAITED); + encode_wait_outcome(encoder, response.outcome); + } + EventResponse::Add(response) => { + encoder.u8(EVENT_RESPONSE_TAG_ADDED); + encode_readiness(encoder, response.readiness); + } + EventResponse::Consume(response) => { + encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); + encoder.u64(response.value); + encode_readiness(encoder, response.readiness); + } + } +} + +pub(super) fn decode_event_response( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { + let response = match decoder.u8()? { + EVENT_RESPONSE_TAG_CREATED => { + EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) + } + EVENT_RESPONSE_TAG_WAITED => EventResponse::Wait(WaitEventResponse::new( + match decode_wait_outcome(decoder)? { + Some(outcome) => outcome, + None => return Ok(None), + }, + )), + EVENT_RESPONSE_TAG_ADDED => { + EventResponse::Add(AddEventResponse::new(decode_readiness(decoder)?)) + } + EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( + decoder.u64()?, + decode_readiness(decoder)?, + )), + _ => return Ok(None), + }; + + Ok(Some(response)) +} + +fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { + match outcome { + WaitOutcome::Ready(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_READY); + encode_readiness(encoder, readiness); + } + WaitOutcome::WouldBlock(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); + encode_readiness(encoder, readiness); + } + } +} + +fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, WireError> { + match decoder.u8()? { + WAIT_OUTCOME_TAG_READY => Ok(Some(WaitOutcome::Ready(decode_readiness(decoder)?))), + WAIT_OUTCOME_TAG_WOULD_BLOCK => { + Ok(Some(WaitOutcome::WouldBlock(decode_readiness(decoder)?))) + } + _ => Ok(None), + } +} + +fn encode_readiness(encoder: &mut Encoder, readiness: ReadinessState) { + encoder.bool(readiness.read_ready); + encoder.bool(readiness.write_ready); + encoder.u64(readiness.generation); +} + +fn decode_readiness(decoder: &mut Decoder<'_>) -> Result { + Ok(ReadinessState::new( + decoder.bool()?, + decoder.bool()?, + decoder.u64()?, + )) +} + +fn encode_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { + match mode { + EventConsumeMode::All => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ALL); + } + EventConsumeMode::One => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ONE); + } + } +} + +fn decode_consume_mode(decoder: &mut Decoder<'_>) -> Result, WireError> { + match decoder.u8()? { + EVENT_CONSUME_MODE_TAG_ALL => Ok(Some(EventConsumeMode::All)), + EVENT_CONSUME_MODE_TAG_ONE => Ok(Some(EventConsumeMode::One)), + _ => Ok(None), + } +} diff --git a/litebox_broker_protocol/src/wire/primitive.rs b/litebox_broker_protocol/src/wire/primitive.rs new file mode 100644 index 000000000..cdfad237a --- /dev/null +++ b/litebox_broker_protocol/src/wire/primitive.rs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::vec::Vec; + +use crate::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion}; + +use super::WireError; + +#[derive(Default)] +pub(super) struct Encoder { + bytes: Vec, +} + +impl Encoder { + pub(super) fn finish(self) -> Vec { + self.bytes + } + + pub(super) fn bool(&mut self, value: bool) { + self.u8(u8::from(value)); + } + + pub(super) fn u8(&mut self, value: u8) { + self.bytes.push(value); + } + + pub(super) fn u16(&mut self, value: u16) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + pub(super) fn u64(&mut self, value: u64) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + pub(super) fn protocol_version(&mut self, version: ProtocolVersion) { + self.u16(version.major); + self.u16(version.minor); + } + + pub(super) fn handle(&mut self, handle: ObjectHandle) { + self.u64(handle.reference_id.get()); + self.u64(handle.reference_generation.get()); + } +} + +pub(super) struct Decoder<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Decoder<'a> { + pub(super) const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + pub(super) fn finish(&self) -> Result<(), WireError> { + if self.offset == self.bytes.len() { + Ok(()) + } else { + Err(WireError::TrailingBytes) + } + } + + pub(super) fn bool(&mut self) -> Result { + match self.u8()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(WireError::InvalidBoolean), + } + } + + pub(super) fn u8(&mut self) -> Result { + let bytes = self.take(1)?; + Ok(bytes[0]) + } + + pub(super) fn u16(&mut self) -> Result { + let bytes = self.take(2)?; + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + pub(super) fn u64(&mut self) -> Result { + let bytes = self.take(8)?; + Ok(u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], + ])) + } + + pub(super) fn protocol_version(&mut self) -> Result { + Ok(ProtocolVersion::new(self.u16()?, self.u16()?)) + } + + pub(super) fn handle(&mut self) -> Result { + let reference_id = ObjectReferenceId::new(self.u64()?); + let reference_generation = ObjectReferenceGeneration::new(self.u64()?); + + Ok(ObjectHandle::new(reference_id, reference_generation)) + } + + fn take(&mut self, len: usize) -> Result<&'a [u8], WireError> { + let end = self + .offset + .checked_add(len) + .ok_or(WireError::OffsetOverflow)?; + let bytes = self + .bytes + .get(self.offset..end) + .ok_or(WireError::TruncatedFrame)?; + self.offset = end; + Ok(bytes) + } +} diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml new file mode 100644 index 000000000..da5909d56 --- /dev/null +++ b/litebox_broker_transport/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_transport" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs new file mode 100644 index 000000000..9603907ab --- /dev/null +++ b/litebox_broker_transport/src/lib.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Broker transport implementations. +//! +//! Transports own hosted or platform-specific framing and I/O. Portable broker +//! protocol messages, local-side adapters, host-side request handling, and core +//! authority state live in separate crates. + +pub mod unix_socket; diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs new file mode 100644 index 000000000..766896ba9 --- /dev/null +++ b/litebox_broker_transport/src/unix_socket.rs @@ -0,0 +1,378 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Unix-domain-socket broker channel for hosted userland deployments. +//! +//! This module deliberately uses `std` because Unix-domain sockets and `std::io` +//! framing are hosted userland concerns. Portable broker interfaces live in the +//! no_std protocol, local, core, and host crates. + +use std::io::{self, Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::time::{Duration, Instant}; + +use litebox_broker_protocol::wire::{ + WireError, decode_request, decode_response, encode_request, encode_response, +}; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, HostControlChannel, LocalControlChannel, PeerCredential, + ReceivedBrokerRequest, ReceivedBrokerResponse, +}; + +const MAX_FRAME_LEN: usize = 64 * 1024; + +/// Local-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamLocalControlChannel { + stream: UnixStream, + io_timeout: Option, + io_deadline: Option, + active_request_deadline: Option, +} + +impl UnixStreamLocalControlChannel { + /// Creates a local control channel from an already-connected Unix stream. + pub const fn from_connected(stream: UnixStream) -> Self { + Self { + stream, + io_timeout: None, + io_deadline: None, + active_request_deadline: None, + } + } + + /// Connects to a userland broker Unix socket. + pub fn connect(path: impl AsRef) -> io::Result { + UnixStream::connect(path).map(Self::from_connected) + } + + /// Sets the read and write timeout for broker control-channel operations. + pub fn set_io_timeout(&mut self, timeout: Option) -> io::Result<()> { + self.io_timeout = timeout; + self.io_deadline = None; + self.active_request_deadline = None; + self.set_stream_io_timeout(timeout) + } + + /// Sets a wall-clock deadline for broker control-channel operations. + pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + self.io_deadline = deadline; + self.active_request_deadline = None; + match deadline { + Some(deadline) => self.set_stream_io_timeout(Some(io_timeout_for_deadline(deadline)?)), + None => self.set_stream_io_timeout(self.io_timeout), + } + } + + fn set_stream_io_timeout(&self, timeout: Option) -> io::Result<()> { + self.stream.set_read_timeout(timeout)?; + self.stream.set_write_timeout(timeout) + } + + fn current_deadline(&mut self) -> io::Result> { + if let Some(deadline) = self.io_deadline { + return Ok(Some(deadline)); + } + if let Some(deadline) = self.active_request_deadline { + return Ok(Some(deadline)); + } + let Some(timeout) = self.io_timeout else { + return Ok(None); + }; + let deadline = deadline_after(timeout)?; + self.active_request_deadline = Some(deadline); + Ok(Some(deadline)) + } + + fn clear_active_request_deadline(&mut self) -> io::Result<()> { + if self.io_deadline.is_none() { + self.active_request_deadline = None; + self.set_stream_io_timeout(self.io_timeout)?; + } + Ok(()) + } +} + +/// Host-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamHostControlChannel { + stream: UnixStream, + io_deadline: Option, +} + +impl UnixStreamHostControlChannel { + /// Creates a host control channel from an accepted Unix stream. + pub const fn from_accepted(stream: UnixStream) -> Self { + Self { + stream, + io_deadline: None, + } + } + + /// Sets a wall-clock deadline for all broker control-channel operations. + pub fn set_io_deadline(&mut self, deadline: Option) -> io::Result<()> { + self.io_deadline = deadline; + if let Some(deadline) = deadline { + let timeout = io_timeout_for_deadline(deadline)?; + self.stream.set_read_timeout(Some(timeout))?; + self.stream.set_write_timeout(Some(timeout)) + } else { + self.stream.set_read_timeout(None)?; + self.stream.set_write_timeout(None) + } + } +} + +impl LocalControlChannel for UnixStreamLocalControlChannel { + type Error = io::Error; + + fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { + let frame = encode_request(request.clone()); + let deadline = self.current_deadline()?; + let result = write_frame_with_deadline(&mut self.stream, &frame, deadline); + if result.is_err() { + self.active_request_deadline = None; + } + result + } + + fn recv_response(&mut self) -> io::Result> { + let deadline = self.current_deadline()?; + let result = match read_frame_with_deadline(&mut self.stream, deadline)? { + Some(frame) => decode_response(&frame).map(Some).map_err(wire_error), + None => Ok(None), + }; + self.clear_active_request_deadline()?; + result + } +} + +impl HostControlChannel for UnixStreamHostControlChannel { + type Error = io::Error; + + fn peer_credential(&self) -> io::Result { + // TODO(broker): replace the PoC placeholder with Unix peer credential extraction + // before this channel is used as an authenticated deployment boundary. + Ok(PeerCredential::Unauthenticated) + } + + fn recv_request(&mut self) -> io::Result> { + let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { + return Ok(None); + }; + decode_request(&frame).map(Some).map_err(wire_error) + } + + fn send_response(&mut self, response: &BrokerResponse) -> io::Result<()> { + write_frame_with_deadline( + &mut self.stream, + &encode_response(response.clone()), + self.io_deadline, + ) + } +} + +fn read_frame_with_deadline( + stream: &mut UnixStream, + deadline: Option, +) -> io::Result>> { + let mut len_buf = [0; 4]; + let mut read = 0; + while read < len_buf.len() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.read(&mut len_buf[read..]) { + Ok(0) if read == 0 => return Ok(None), + Ok(0) => return Err(invalid_data("truncated broker frame length")), + Ok(len) => read += len, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + + let len = u32::from_le_bytes(len_buf) as usize; + if len == 0 || len > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } + + let mut frame = vec![0; len]; + let mut read = 0; + while read < frame.len() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.read(&mut frame[read..]) { + Ok(0) => return Err(invalid_data("truncated broker frame")), + Ok(len) => read += len, + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(Some(frame)) +} + +fn write_frame_with_deadline( + stream: &mut UnixStream, + frame: &[u8], + deadline: Option, +) -> io::Result<()> { + if frame.is_empty() || frame.len() > MAX_FRAME_LEN { + return Err(invalid_data("invalid broker frame length")); + } + let len = u32::try_from(frame.len()).map_err(|_| invalid_data("broker frame too large"))?; + write_all_with_deadline(stream, &len.to_le_bytes(), deadline)?; + write_all_with_deadline(stream, frame, deadline) +} + +fn write_all_with_deadline( + stream: &mut UnixStream, + mut buffer: &[u8], + deadline: Option, +) -> io::Result<()> { + while !buffer.is_empty() { + refresh_stream_io_deadline(stream, deadline)?; + match stream.write(buffer) { + Ok(0) => { + return Err(io::Error::new( + io::ErrorKind::WriteZero, + "failed to write broker frame", + )); + } + Ok(written) => buffer = &buffer[written..], + Err(error) if error.kind() == io::ErrorKind::Interrupted => {} + Err(error) => return Err(error), + } + } + Ok(()) +} + +fn refresh_stream_io_deadline(stream: &UnixStream, deadline: Option) -> io::Result<()> { + if let Some(deadline) = deadline { + let timeout = io_timeout_for_deadline(deadline)?; + stream.set_read_timeout(Some(timeout))?; + stream.set_write_timeout(Some(timeout))?; + } + Ok(()) +} + +fn io_timeout_for_deadline(deadline: Instant) -> io::Result { + let timeout = deadline + .checked_duration_since(Instant::now()) + .filter(|timeout| !timeout.is_zero()) + .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "broker I/O deadline expired"))?; + Ok(timeout) +} + +fn deadline_after(timeout: Duration) -> io::Result { + Instant::now() + .checked_add(timeout) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "broker I/O timeout overflow")) +} + +fn invalid_data(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +fn wire_error(error: WireError) -> io::Error { + io::Error::new( + io::ErrorKind::InvalidData, + format!("invalid broker wire message: {error}"), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn frame_round_trip() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + write_frame_with_deadline(&mut writer, &[1, 2, 3], None).unwrap(); + + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .unwrap(), + [1, 2, 3] + ); + } + + #[test] + fn clean_eof_before_frame_is_close() { + let (writer, mut reader) = UnixStream::pair().unwrap(); + drop(writer); + + assert!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .is_none() + ); + } + + #[test] + fn malformed_frames_are_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&[1, 0]).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&0u32.to_le_bytes()).unwrap(); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer + .write_all(&u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_le_bytes()) + .unwrap(); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&4u32.to_le_bytes()).unwrap(); + writer.write_all(&[1, 2]).unwrap(); + drop(writer); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), + io::ErrorKind::InvalidData + ); + } + + #[test] + fn local_response_read_io_timeout_is_wall_clock() { + let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); + channel + .set_io_timeout(Some(Duration::from_millis(50))) + .unwrap(); + + let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); + host_stream.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + std::thread::sleep(Duration::from_millis(20)); + if host_stream.write_all(&[0]).is_err() { + break; + } + } + + let error = reader.join().expect("timeout reader panicked"); + assert!( + matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ), + "unexpected timeout error kind: {error:?}" + ); + } +} diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml new file mode 100644 index 000000000..8e3502f1c --- /dev/null +++ b/litebox_broker_userland/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "litebox_broker_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5.33", features = ["derive"] } +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_host = { path = "../litebox_broker_host", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[[bin]] +name = "litebox-broker-userland" +path = "src/main.rs" + +[dev-dependencies] +litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs new file mode 100644 index 000000000..d3c91f578 --- /dev/null +++ b/litebox_broker_userland/src/main.rs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::error::Error; +use std::os::unix::net::UnixListener; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + +use clap::Parser; +use litebox_broker_core::{BrokerCore, PolicyEngine}; +use litebox_broker_host::serve_connection; +use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; + +const SESSION_TIMEOUT: Duration = Duration::from_secs(5); + +#[derive(Parser, Debug)] +struct CliArgs { + /// Broker Unix socket path to bind. + #[arg(long, value_name = "PATH", value_hint = clap::ValueHint::FilePath)] + socket: PathBuf, +} + +fn main() -> Result<(), Box> { + let args = CliArgs::parse(); + let listener = UnixListener::bind(args.socket)?; + let (stream, _) = listener.accept()?; + let mut channel = UnixStreamHostControlChannel::from_accepted(stream); + channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; + let mut broker = BrokerCore::new(PolicyEngine::event_only())?; + serve_connection(&mut broker, &mut channel)?; + Ok(()) +} diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs new file mode 100644 index 000000000..1d18bdb42 --- /dev/null +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::env; +use std::fs; +use std::io; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use litebox_broker_host::HOST_PROTOCOL_VERSION; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + +#[test] +fn separate_process_broker_serves_event_object_requests() { + let socket_path = SocketPathGuard::new(unique_socket_path()); + let mut child = ChildGuard::new(spawn_broker(socket_path.path())); + let mut channel = connect_with_retry(socket_path.path()).unwrap(); + channel + .set_io_timeout(Some(Duration::from_secs(5))) + .unwrap(); + let mut local = BrokerLocal::new(channel); + + assert_eq!(local.negotiate().unwrap(), HOST_PROTOCOL_VERSION); + + let handle = local.create_event().unwrap(); + assert_eq!( + local.wait_event(handle).unwrap(), + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) + ); + + assert_eq!( + local.add_event(handle, 1).unwrap(), + ReadinessState::new(true, true, 1) + ); + + assert_eq!( + local.wait_event(handle).unwrap(), + WaitOutcome::Ready(ReadinessState::new(true, true, 1)) + ); + drop(local); + assert!(child.wait().unwrap().success()); +} + +fn spawn_broker(socket_path: &Path) -> Child { + Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) + .arg("--socket") + .arg(socket_path) + .spawn() + .unwrap() +} + +struct ChildGuard { + child: Option, +} + +impl ChildGuard { + fn new(child: Child) -> Self { + Self { child: Some(child) } + } + + fn wait(&mut self) -> io::Result { + let status = self.child.as_mut().expect("child process missing").wait(); + if status.is_ok() { + self.child = None; + } + status + } +} + +impl Drop for ChildGuard { + fn drop(&mut self) { + if let Some(mut child) = self.child.take() { + match child.try_wait() { + Ok(Some(_status)) => {} + Ok(None) => { + let _ = child.kill(); + let _ = child.wait(); + } + Err(_error) => { + let _ = child.kill(); + let _ = child.wait(); + } + } + } + } +} + +struct SocketPathGuard { + path: PathBuf, +} + +impl SocketPathGuard { + fn new(path: PathBuf) -> Self { + Self { path } + } + + fn path(&self) -> &Path { + &self.path + } +} + +impl Drop for SocketPathGuard { + fn drop(&mut self) { + let _ = fs::remove_file(&self.path); + } +} + +fn connect_with_retry(socket_path: &Path) -> io::Result { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match UnixStreamLocalControlChannel::connect(socket_path) { + Ok(channel) => return Ok(channel), + Err(error) if Instant::now() < deadline => { + if error.kind() != io::ErrorKind::NotFound + && error.kind() != io::ErrorKind::ConnectionRefused + { + return Err(error); + } + thread::sleep(Duration::from_millis(10)); + } + Err(error) => return Err(error), + } + } +} + +fn unique_socket_path() -> PathBuf { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + env::temp_dir().join(format!( + "litebox-broker-userland-{}-{now}.sock", + std::process::id() + )) +} diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 5153a83fa..3c1177a76 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,6 +536,20 @@ where } } +impl From for Errno { + fn from(value: litebox::event::counter::EventCounterError) -> Self { + match value { + litebox::event::counter::EventCounterError::InvalidInput => Errno::EINVAL, + litebox::event::counter::EventCounterError::WouldBlock + | litebox::event::counter::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::event::counter::EventCounterError::Io + | litebox::event::counter::EventCounterError::UnexpectedResponse + | litebox::event::counter::EventCounterError::Unavailable => Errno::EIO, + _ => Errno::EIO, + } + } +} + impl From for Errno { fn from(value: litebox::fs::errors::ReadDirError) -> Self { match value { diff --git a/litebox_platform_linux_userland/src/lib.rs b/litebox_platform_linux_userland/src/lib.rs index d87e55fa4..ece99163f 100644 --- a/litebox_platform_linux_userland/src/lib.rs +++ b/litebox_platform_linux_userland/src/lib.rs @@ -481,6 +481,75 @@ impl LinuxUserland { .unwrap(), ], ), + // Broker control-channel I/O runs through a host Unix socket in the + // current POC. The transport refreshes read/write timeouts around + // each request. + ( + libc::SYS_setsockopt, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new( + 1, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SOL_SOCKET as u64, + ) + .unwrap(), + SeccompCondition::new( + 2, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SO_RCVTIMEO as u64, + ) + .unwrap(), + ]) + .unwrap(), + SeccompRule::new(vec![ + SeccompCondition::new( + 1, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SOL_SOCKET as u64, + ) + .unwrap(), + SeccompCondition::new( + 2, + SeccompCmpArgLen::Dword, + SeccompCmpOp::Eq, + libc::SO_SNDTIMEO as u64, + ) + .unwrap(), + ]) + .unwrap(), + ], + ), + // Connected UnixStream I/O may use sendto/recvfrom rather than raw + // read/write. Limit these rules to connected-socket calls that do + // not name a peer address. + ( + libc::SYS_sendto, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new(4, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + SeccompCondition::new(5, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + ]) + .unwrap(), + ], + ), + ( + libc::SYS_recvfrom, + vec![ + SeccompRule::new(vec![ + SeccompCondition::new(4, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + SeccompCondition::new(5, SeccompCmpArgLen::Qword, SeccompCmpOp::Eq, 0) + .unwrap(), + ]) + .unwrap(), + ], + ), (libc::SYS_close, vec![]), ]; let rule_map: std::collections::BTreeMap> = diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 6a5b88374..25c47f8a3 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -8,6 +8,9 @@ anyhow = "1.0.97" clap = { version = "4.5.33", features = ["derive"] } libc = { version = "0.2.169", default-features = false } litebox = { version = "0.1.0", path = "../litebox" } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } +litebox_broker_transport = { version = "0.1.0", path = "../litebox_broker_transport" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } litebox_platform_linux_userland = { version = "0.1.0", path = "../litebox_platform_linux_userland" } litebox_platform_multiplex = { version = "0.1.0", path = "../litebox_platform_multiplex", default-features = false, features = ["platform_linux_userland"] } @@ -21,6 +24,8 @@ litebox_util_log = { version = "0.1.0", path = "../litebox_util_log", features = sha2 = "0.10" walkdir = "2.0" glob = "0.3" +litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } +litebox_broker_host = { version = "0.1.0", path = "../litebox_broker_host" } [features] lock_tracing = ["litebox/lock_tracing"] diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs new file mode 100644 index 000000000..7634d861a --- /dev/null +++ b/litebox_runner_linux_userland/src/broker.rs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::{ + path::Path, + thread, + time::{Duration, Instant}, +}; + +use anyhow::{Context as _, Result}; +use litebox_broker_local::BrokerLocal; +use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; + +const SETUP_TIMEOUT: Duration = Duration::from_secs(5); +const ACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); +const RETRY_DELAY: Duration = Duration::from_millis(20); +type Local = BrokerLocal; + +pub(crate) struct BrokerConnection { + local: Local, +} + +pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { + match socket_path { + Some(path) => connect_to_endpoint(path).map(Some), + None => Ok(None), + } +} + +impl BrokerConnection { + pub(crate) fn into_local(self) -> Local { + self.local + } +} + +fn connect_to_endpoint(socket_path: &Path) -> Result { + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let mut local = connect_with_retry(socket_path, setup_deadline) + .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; + local + .control_channel_mut() + .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) + .context("failed to configure broker active request timeout")?; + Ok(BrokerConnection { local }) +} + +fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { + loop { + match UnixStreamLocalControlChannel::connect(socket_path) { + Ok(mut channel) => { + channel + .set_io_deadline(Some(setup_deadline)) + .context("failed to configure broker setup deadline")?; + let mut local = BrokerLocal::new(channel); + local.negotiate().context("broker negotiation failed")?; + return Ok(local); + } + Err(error) => { + if Instant::now() >= setup_deadline { + return Err(error).context("timed out connecting to broker"); + } + } + } + let remaining = setup_deadline.saturating_duration_since(Instant::now()); + thread::sleep(RETRY_DELAY.min(remaining)); + } +} diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 9a18b4a7c..23e866c58 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -9,6 +9,8 @@ use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; use std::path::{Path, PathBuf}; +mod broker; + extern crate alloc; // Use a stable non-root guest identity instead of mirroring the host user. This keeps shim @@ -77,6 +79,15 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub program_from_tar: bool, + /// Connect to an already-running broker Unix socket and verify the control path. + #[arg( + long = "broker-socket", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + requires = "unstable", + help_heading = "Unstable Options" + )] + pub broker_socket: Option, } struct MmappedFile { @@ -201,7 +212,18 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; + + let shim_builder = if let Some(broker_connection) = broker_connection { + litebox_shim_linux::LinuxShimBuilder::new_with_litebox( + litebox::LiteBox::new_with_broker_local( + litebox_platform_multiplex::platform(), + broker_connection.into_local(), + ), + ) + } else { + litebox_shim_linux::LinuxShimBuilder::new() + }; let litebox = shim_builder.litebox(); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. let tid = unsafe { libc::syscall(libc::SYS_gettid) } diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c new file mode 100644 index 000000000..5ed53b194 --- /dev/null +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include +#include +#include +#include +#include +#include + +static int expect_eagain_read(int fd) { + uint64_t value = 0; + errno = 0; + if (read(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EAGAIN ? 0 : 2; +} + +static int write_value(int fd, uint64_t value) { + return write(fd, &value, sizeof(value)) == sizeof(value) ? 0 : 1; +} + +static int read_value(int fd, uint64_t expected) { + uint64_t value = 0; + if (read(fd, &value, sizeof(value)) != sizeof(value)) { + return 1; + } + return value == expected ? 0 : 2; +} + +static int expect_poll_events(int fd, short expected) { + struct pollfd poll_fd = { + .fd = fd, + .events = POLLIN | POLLOUT, + }; + errno = 0; + int ready = poll(&poll_fd, 1, 0); + if (ready < 0) { + return 1; + } + if ((poll_fd.revents & (POLLIN | POLLOUT)) != expected) { + return 2; + } + return 0; +} + +static int expect_eagain_write(int fd, uint64_t value) { + errno = 0; + if (write(fd, &value, sizeof(value)) != -1) { + return 1; + } + return errno == EAGAIN ? 0 : 2; +} + +static int clear_nonblock_with_ioctl(int fd) { + int nonblock = 0; + return ioctl(fd, FIONBIO, &nonblock) == 0 ? 0 : 1; +} + +int main(void) { + int fd = eventfd(0, EFD_NONBLOCK); + if (fd < 0) { + return 10; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 11; + } + if (expect_eagain_read(fd) != 0) { + return 12; + } + if (write_value(fd, 3) != 0) { + return 13; + } + if (expect_poll_events(fd, POLLIN | POLLOUT) != 0) { + return 14; + } + if (read_value(fd, 3) != 0) { + return 15; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 16; + } + if (write_value(fd, 2) != 0) { + return 17; + } + if (write_value(fd, 5) != 0) { + return 18; + } + if (read_value(fd, 7) != 0) { + return 19; + } + if (write_value(fd, 9) != 0) { + return 20; + } + if (read_value(fd, 9) != 0) { + return 21; + } + if (write_value(fd, 11) != 0) { + return 22; + } + if (read_value(fd, 11) != 0) { + return 23; + } + if (expect_eagain_read(fd) != 0) { + return 24; + } + uint64_t invalid = UINT64_MAX; + errno = 0; + if (write(fd, &invalid, sizeof(invalid)) != -1 || errno != EINVAL) { + return 25; + } + if (write_value(fd, UINT64_MAX - 1) != 0) { + return 26; + } + if (expect_poll_events(fd, POLLIN) != 0) { + return 27; + } + if (expect_eagain_write(fd, 1) != 0) { + return 28; + } + if (read_value(fd, UINT64_MAX - 1) != 0) { + return 29; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 30; + } + close(fd); + + int ioctl_toggle_fd = eventfd(1, EFD_NONBLOCK); + if (ioctl_toggle_fd < 0) { + return 31; + } + if (clear_nonblock_with_ioctl(ioctl_toggle_fd) != 0) { + return 32; + } + if (read_value(ioctl_toggle_fd, 1) != 0) { + return 33; + } + close(ioctl_toggle_fd); + + int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); + if (semaphore_fd < 0) { + return 40; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 41; + } + if (write_value(semaphore_fd, 3) != 0) { + return 42; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 43; + } + if (read_value(semaphore_fd, 1) != 0) { + return 44; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 45; + } + if (read_value(semaphore_fd, 1) != 0) { + return 46; + } + if (read_value(semaphore_fd, 1) != 0) { + return 47; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 48; + } + if (expect_eagain_read(semaphore_fd) != 0) { + return 49; + } + close(semaphore_fd); + + return 0; +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 3df36a850..912a523f0 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -9,6 +9,9 @@ use std::{ path::{Path, PathBuf}, }; +const BROKER_HELPER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); +const BROKER_ONLY_C_TESTS: &[&str] = &["eventfd.c"]; + #[must_use] struct Runner { command: std::process::Command, @@ -119,6 +122,12 @@ impl Runner { self } + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + fn broker_socket(&mut self, socket_path: &Path) -> &mut Self { + self.command.arg("--broker-socket").arg(socket_path); + self + } + #[cfg_attr(not(target_arch = "x86_64"), expect(dead_code))] fn with_fs_path(&mut self, f: impl FnOnce(&Path)) -> &mut Self { f(&self.tar_dir); @@ -186,9 +195,18 @@ fn find_c_test_files(dir: &str) -> Vec { files } +fn is_broker_only_c_test(path: &Path) -> bool { + path.file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| BROKER_ONLY_C_TESTS.contains(&name)) +} + #[test] fn test_dynamic_lib_with_rewriter() { for path in find_c_test_files("./tests") { + if is_broker_only_c_test(&path) { + continue; + } let stem = path .file_stem() .and_then(|s| s.to_str()) @@ -202,6 +220,9 @@ fn test_dynamic_lib_with_rewriter() { #[test] fn test_static_exec_with_rewriter() { for path in find_c_test_files("./tests") { + if is_broker_only_c_test(&path) { + continue; + } let stem = path .file_stem() .and_then(|s| s.to_str()) @@ -226,6 +247,198 @@ fn run_which(prog: &str) -> std::path::PathBuf { prog_path } +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn unique_test_socket_path(name: &str) -> PathBuf { + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + std::env::temp_dir().join(format!( + "litebox-{name}-{}-{nonce}.sock", + std::process::id() + )) +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +struct TestBroker { + thread: Option>, + done_rx: std::sync::mpsc::Receiver<()>, + event_request_count_rx: std::sync::mpsc::Receiver, + socket_path: PathBuf, +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl TestBroker { + fn next_event_request_count(&self) -> usize { + self.event_request_count_rx + .recv_timeout(BROKER_HELPER_TIMEOUT) + .expect("broker test host did not report event request count") + } + + fn join(mut self) { + self.done_rx + .recv_timeout(BROKER_HELPER_TIMEOUT) + .expect("broker test host did not finish"); + self.thread + .take() + .expect("broker test host thread missing") + .join() + .expect("broker test host panicked"); + let _ = std::fs::remove_file(&self.socket_path); + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl Drop for TestBroker { + fn drop(&mut self) { + let _ = std::fs::remove_file(&self.socket_path); + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +fn spawn_test_broker( + socket_path: &Path, + policy: litebox_broker_core::PolicyEngine, + connection_count: usize, +) -> TestBroker { + let _ = std::fs::remove_file(socket_path); + + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let (done_tx, done_rx) = std::sync::mpsc::channel(); + let (event_request_count_tx, event_request_count_rx) = std::sync::mpsc::channel(); + let server_socket_path = socket_path.to_path_buf(); + let cleanup_socket_path = socket_path.to_path_buf(); + let broker_thread = std::thread::spawn(move || { + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) + .expect("failed to bind broker test socket"); + let mut core = + litebox_broker_core::BrokerCore::new(policy).expect("failed to create broker core"); + ready_tx.send(()).expect("failed to report broker ready"); + + for _ in 0..connection_count { + let (stream, _) = listener + .accept() + .expect("failed to accept broker local control connection"); + stream + .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker test read timeout"); + stream + .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) + .expect("failed to configure broker test write timeout"); + let mut channel = CountingHostControlChannel::new( + litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream), + ); + let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) + .expect("broker host failed"); + assert_eq!( + termination, + litebox_broker_host::ConnectionTermination::PeerClosed + ); + event_request_count_tx + .send(channel.event_request_count()) + .expect("failed to report broker event request count"); + } + })); + let _ = std::fs::remove_file(&server_socket_path); + let _ = done_tx.send(()); + if let Err(panic) = result { + std::panic::resume_unwind(panic); + } + }); + + ready_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("broker test host did not start"); + TestBroker { + thread: Some(broker_thread), + done_rx, + event_request_count_rx, + socket_path: cleanup_socket_path, + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +struct CountingHostControlChannel { + inner: T, + event_request_count: usize, +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl CountingHostControlChannel { + const fn new(inner: T) -> Self { + Self { + inner, + event_request_count: 0, + } + } + + const fn event_request_count(&self) -> usize { + self.event_request_count + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl litebox_broker_protocol::HostControlChannel for CountingHostControlChannel +where + T: litebox_broker_protocol::HostControlChannel, +{ + type Error = T::Error; + + fn peer_credential(&self) -> Result { + self.inner.peer_credential() + } + + fn recv_request( + &mut self, + ) -> Result, Self::Error> { + let received = self.inner.recv_request()?; + if matches!( + received, + Some(litebox_broker_protocol::ReceivedBrokerRequest::Request( + litebox_broker_protocol::BrokerRequest::Core( + litebox_broker_protocol::CoreRequest::Event(_) + ) + )) + ) { + self.event_request_count += 1; + } + Ok(received) + } + + fn send_response( + &mut self, + response: &litebox_broker_protocol::BrokerResponse, + ) -> Result<(), Self::Error> { + self.inner.send_response(response) + } +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +fn test_runner_broker_integration_with_rewriter() { + let true_path = run_which("true"); + let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); + let socket_path = unique_test_socket_path("runner-broker"); + let broker_thread = spawn_test_broker( + &socket_path, + litebox_broker_core::PolicyEngine::event_only(), + 2, + ); + + Runner::new(&true_path, "broker_true_rewriter") + .broker_socket(&socket_path) + .run(); + assert_eq!(broker_thread.next_event_request_count(), 0); + + Runner::new(&target, "broker_eventfd_rewriter") + .broker_socket(&socket_path) + .run(); + assert!(broker_thread.next_event_request_count() > 0); + + broker_thread.join(); +} + #[cfg(target_arch = "x86_64")] #[test] fn test_node_with_rewriter() { diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 09835c541..9fa295a24 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -14,10 +14,10 @@ extern crate alloc; +use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; -use alloc::sync::Arc; use core::cell::{Cell, RefCell}; use litebox::{ LiteBox, @@ -165,10 +165,13 @@ impl LinuxShimBuilder { /// Returns a new shim builder. pub fn new() -> Self { let platform = litebox_platform_multiplex::platform(); - Self { - platform, - litebox: LiteBox::new(platform), - } + Self::new_with_litebox(LiteBox::new(platform)) + } + + /// Returns a new shim builder using an already-created LiteBox instance. + pub fn new_with_litebox(litebox: LiteBox) -> Self { + let platform = litebox_platform_multiplex::platform(); + Self { platform, litebox } } /// Returns the litebox object for the shim. diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 3c9d30077..df1aad275 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -635,7 +635,10 @@ mod test { #[test] fn test_epoll_with_eventfd() { let (task, epoll) = setup_epoll(); - let eventfd = crate::syscalls::eventfd::EventFile::new(0, EfdFlags::CLOEXEC); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::CLOEXEC) + .unwrap(); let typed = task .global .litebox @@ -658,8 +661,7 @@ mod test { ) .unwrap(); - // spawn a thread to write to the eventfd - { + let writer = { let global = task.global.clone(); let files = Arc::clone(&files); std::thread::spawn(move || { @@ -674,11 +676,12 @@ mod test { .with_entry(&typed, |entry| { entry.write(&WaitState::new(platform()).context(), 1) }); - }); - } + }) + }; epoll .wait(&task.global, &WaitState::new(platform()).context(), 1024) .unwrap(); + writer.join().unwrap(); } #[test] @@ -730,7 +733,10 @@ mod test { let task = crate::syscalls::tests::init_platform(None); let mut set = super::PollSet::with_capacity(0); - let eventfd = crate::syscalls::eventfd::EventFile::new(0, EfdFlags::empty()); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(); let typed = task .global diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 1c18bc8ed..0102a5a8e 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -8,6 +8,7 @@ use core::sync::atomic::AtomicU32; use litebox::{ event::{ Events, IOPollable, + counter::{EventCounter, EventCounterError, EventCounterReadMode}, observer::Observer, polling::{Pollee, TryOpError}, wait::WaitContext, @@ -15,10 +16,11 @@ use litebox::{ fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, platform::TimeProvider, - sync::RawSyncPrimitivesProvider, + sync::{Mutex, RawSyncPrimitivesProvider}, }; use litebox_common_linux::{EfdFlags, errno::Errno}; -use litebox_platform_multiplex::Platform; + +use crate::{GlobalState, Platform, ShimFS}; pub(crate) struct EventfdSubsystem; impl FdEnabledSubsystem for EventfdSubsystem { @@ -26,101 +28,205 @@ impl FdEnabledSubsystem for EventfdSubsystem { } impl FdEnabledSubsystemEntry for EventFile {} +/// Backing counter for a Linux eventfd file description. +/// +/// New blocking eventfds still use the shim-local implementation to keep the +/// initial broker-backed scope narrow. Broker-backed nonblocking eventfds can +/// still be switched to blocking mode because the local-core counter can block +/// through LiteBox-local readiness notifications. +enum EventFileCounter { + ShimLocal { + count: Mutex, + pollee: Pollee, + }, + LocalCore(EventCounter), +} + pub(crate) struct EventFile { - counter: litebox::sync::Mutex, + counter: EventFileCounter, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, - pollee: Pollee, } -impl EventFile { - pub(crate) fn new(count: u64, flags: EfdFlags) -> Self { - let mut status = OFlags::RDWR; - status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); - - Self { - counter: litebox::sync::Mutex::new(count), - status: AtomicU32::new(status.bits()), - semaphore: flags.contains(EfdFlags::SEMAPHORE), +impl EventFileCounter { + fn shim_local(count: u64) -> Self { + Self::ShimLocal { + count: Mutex::new(count), pollee: Pollee::new(), } } - fn try_read(&self) -> Result> { - let mut counter = self.counter.lock(); - if *counter == 0 { + fn local_core(counter: EventCounter) -> Self { + Self::LocalCore(counter) + } + + fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + semaphore: bool, + ) -> Result { + match self { + Self::ShimLocal { count, pollee } => pollee + .wait(cx, nonblock, Events::IN, || { + Self::try_read_local(count, pollee, semaphore) + }) + .map_err(Errno::from), + Self::LocalCore(counter) => counter + .read(cx, nonblock, consume_mode(semaphore)) + .map_err(Errno::from), + } + } + + fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result { + match self { + Self::ShimLocal { count, pollee } => pollee + .wait(cx, nonblock, Events::OUT, || { + Self::try_write_local(count, pollee, value) + }) + .map_err(Errno::from), + Self::LocalCore(counter) => counter.write(cx, nonblock, value).map_err(Errno::from), + } + } + + fn try_read_local( + count: &Mutex, + pollee: &Pollee, + semaphore: bool, + ) -> Result> { + let mut count = count.lock(); + if *count == 0 { return Err(TryOpError::TryAgain); } - let res = if self.semaphore { 1 } else { *counter }; - *counter -= res; + let res = if semaphore { 1 } else { *count }; + *count -= res; - drop(counter); - self.pollee.notify_observers(Events::OUT); + drop(count); + pollee.notify_observers(Events::OUT); Ok(res) } - pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::IN, - || self.try_read(), - ) - .map_err(Errno::from) - } - - fn try_write(&self, value: u64) -> Result> { - let mut counter = self.counter.lock(); - if let Some(new_value) = (*counter).checked_add(value) { - // The maximum value that may be stored in the counter is the largest unsigned - // 64-bit value minus 1 (i.e., 0xfffffffffffffffe) - if new_value != u64::MAX { - *counter = new_value; - drop(counter); - self.pollee.notify_observers(Events::IN); - return Ok(8); - } + fn try_write_local( + count: &Mutex, + pollee: &Pollee, + value: u64, + ) -> Result> { + if value == u64::MAX { + return Err(TryOpError::Other(Errno::EINVAL)); + } + + let mut count = count.lock(); + if let Some(new_value) = (*count).checked_add(value) + && new_value != u64::MAX + { + *count = new_value; + drop(count); + pollee.notify_observers(Events::IN); + return Ok(core::mem::size_of::()); } Err(TryOpError::TryAgain) } + fn check_io_events(&self) -> Events { + match self { + Self::ShimLocal { count, .. } => { + let count = count.lock(); + let mut events = Events::empty(); + if *count != 0 { + events |= Events::IN; + } + if *count < u64::MAX - 1 { + events |= Events::OUT; + } + events + } + Self::LocalCore(counter) => counter.check_io_events(), + } + } + + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + match self { + Self::ShimLocal { pollee, .. } => pollee.register_observer(observer, mask), + Self::LocalCore(counter) => counter.register_observer(observer, mask), + } + } +} + +impl EventFile { + fn new(counter: EventFileCounter, flags: EfdFlags) -> Self { + let mut status = OFlags::RDWR; + status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); + Self { + counter, + status: AtomicU32::new(status.bits()), + semaphore: flags.contains(EfdFlags::SEMAPHORE), + } + } + + pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { + self.counter.read(cx, self.is_nonblocking(), self.semaphore) + } + pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::OUT, - || self.try_write(value), - ) - .map_err(Errno::from) + self.counter.write(cx, self.is_nonblocking(), value) } super::common_functions_for_file_status!(); + + fn is_nonblocking(&self) -> bool { + self.get_status().contains(OFlags::NONBLOCK) + } } impl IOPollable for EventFile { fn check_io_events(&self) -> Events { - let counter = self.counter.lock(); - let mut events = Events::empty(); - if *counter != 0 { - events |= Events::IN; - } - // if it is possible to write a value of at least "1" - // without blocking, the file is writable - let is_writable = *counter < u64::MAX - 1; - if is_writable { - events |= Events::OUT; + self.counter.check_io_events() + } + + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + self.counter.register_observer(observer, mask); + } +} + +impl GlobalState { + pub(crate) fn create_linux_eventfd( + &self, + initval: u32, + flags: EfdFlags, + ) -> Result, Errno> { + if flags + .intersects((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) + { + return Err(Errno::EINVAL); } - events + let count = u64::from(initval); + let counter = if flags.contains(EfdFlags::NONBLOCK) { + match EventCounter::new(&self.litebox, count) { + Ok(counter) => EventFileCounter::local_core(counter), + Err(EventCounterError::Unavailable) => EventFileCounter::shim_local(count), + Err(error) => return Err(error.into()), + } + } else { + EventFileCounter::shim_local(count) + }; + Ok(EventFile::new(counter, flags)) } +} - fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - self.pollee.register_observer(observer, mask); +fn consume_mode(semaphore: bool) -> EventCounterReadMode { + if semaphore { + EventCounterReadMode::One + } else { + EventCounterReadMode::All } } @@ -134,30 +240,43 @@ mod tests { #[test] fn test_semaphore_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::SEMAPHORE)); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::SEMAPHORE) + .unwrap(), + ); let total = 8; - for _ in 0..total { - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - copied_eventfd - .read(&WaitState::new(platform()).context()) - .unwrap(); - }); - } + let handles: std::vec::Vec<_> = (0..total) + .map(|_| { + let copied_eventfd = eventfd.clone(); + std::thread::spawn(move || { + copied_eventfd + .read(&WaitState::new(platform()).context()) + .unwrap(); + }) + }) + .collect(); std::thread::sleep(core::time::Duration::from_millis(500)); eventfd .write(&WaitState::new(platform()).context(), total) .unwrap(); + for handle in handles { + handle.join().unwrap(); + } } #[test] fn test_blocking_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::empty())); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { copied_eventfd @@ -180,9 +299,13 @@ mod tests { #[test] fn test_blocking_eventfd_no_race_on_massive_readwrite() { - let _task = crate::syscalls::tests::init_platform(None); + let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::empty())); + let eventfd = alloc::sync::Arc::new( + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { for _ in 0..10000 { @@ -199,46 +322,21 @@ mod tests { } #[test] - fn test_nonblocking_eventfd() { - let _task = crate::syscalls::tests::init_platform(None); - - let eventfd = alloc::sync::Arc::new(super::EventFile::new(0, EfdFlags::NONBLOCK)); - let copied_eventfd = eventfd.clone(); - std::thread::spawn(move || { - // first write should succeed immediately - copied_eventfd - .write(&WaitState::new(platform()).context(), 1) - .unwrap(); - // block until the first read finishes - while let Err(e) = - copied_eventfd.write(&WaitState::new(platform()).context(), u64::MAX - 1) - { - assert_eq!(e, Errno::EAGAIN, "Unexpected error: {e:?}"); - core::hint::spin_loop(); - } - }); - - let read = |eventfd: &super::EventFile, - expected_value: u64| { - loop { - match eventfd.read(&WaitState::new(platform()).context()) { - Ok(ret) => { - assert_eq!(ret, expected_value); - break; - } - Err(Errno::EAGAIN) => { - // busy wait - // TODO: use poll rather than busy wait - } - Err(e) => panic!("Unexpected error: {e:?}"), - } - core::hint::spin_loop(); - } - }; + fn test_nonblocking_eventfd_uses_shim_local_without_broker_control() { + let task = crate::syscalls::tests::init_platform(None); - // block until the first write - read(&eventfd, 1); - // block until the second write - read(&eventfd, u64::MAX - 1); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .unwrap(); + assert_eq!( + eventfd.read(&WaitState::new(platform()).context()), + Err(Errno::EAGAIN) + ); + assert_eq!( + eventfd.write(&WaitState::new(platform()).context(), 1), + Ok(8) + ); + assert_eq!(eventfd.read(&WaitState::new(platform()).context()), Ok(1)); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index a857477f4..3560d8956 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1734,7 +1734,7 @@ impl Task { return Err(Errno::EINVAL); } - let eventfd = super::eventfd::EventFile::new(u64::from(initval), flags); + let eventfd = self.global.create_linux_eventfd(initval, flags)?; let mut dt = self.global.litebox.descriptor_table_mut(); let typed = dt.insert::(eventfd); if flags.contains(EfdFlags::CLOEXEC) {