From f2216efa5061cdba3a823d972af48d30dfac4304 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 4 Sep 2026 19:02:11 -0700 Subject: [PATCH 1/3] Add cross-session reference duplication Duplicate event and pipe references into a target session with attenuated rights, target policy and quota admission, and shared object lifetime. Reject socket duplication while readiness remains association-bound. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/session.rs | 136 ++++++++++++++++++++++++ litebox_broker_core/src/socket/tests.rs | 25 +++++ 2 files changed, 161 insertions(+) diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 45a0a787ff..7b2f62a6ea 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -134,6 +134,14 @@ impl BrokerSession { .policy .principal_object_rights(self.caller_credential)?; let object = Arc::new(RwLock::new(object)); + self.create_object_reference_with_rights(object, rights) + } + + fn create_object_reference_with_rights( + &self, + object: Arc>, + rights: ObjectRights, + ) -> Result { let mut session_references = self.references.lock(); self.prepare_session_references(&mut session_references, 1)?; let mut references = self.core.references.write(); @@ -178,6 +186,59 @@ impl BrokerSession { Ok(handle) } + /// Duplicates a supported object reference into another session. + /// + /// The returned handle is owned by `target` and refers to the same + /// underlying event or pipe endpoint. `rights` must be nonempty, allowed by + /// the target's policy, and no broader than the source reference's rights. + /// The source reference is unchanged. Socket references are not supported + /// because their readiness registration is currently bound to one session + /// and handle. + /// + /// Pipe capacity remains charged to the session that created the pipe. + /// Child creation must call this operation explicitly according to the + /// guest operating system's inheritance semantics. + pub fn duplicate_object_reference_to( + &self, + handle: ObjectHandle, + target: &BrokerSession, + rights: ObjectRights, + ) -> Result { + if rights.is_empty() { + return Err(BrokerError::InvalidRights); + } + + let object = { + let references = self.core.references.read(); + let reference = references.get(&handle).ok_or(BrokerError::UnknownObject)?; + if reference.session_id != self.session_id { + return Err(BrokerError::UnknownObject); + } + if !reference.rights.contains(rights) { + return Err(BrokerError::InvalidRights); + } + Arc::clone(&reference.object) + }; + + { + let object = object.read(); + match &*object { + ObjectEntry::Event(_) | ObjectEntry::Pipe(_) => {} + ObjectEntry::Socket(_) => return Err(BrokerError::UnsupportedOperation), + ObjectEntry::Reserved => return Err(BrokerError::Internal), + } + } + + let target_rights = target + .core + .policy + .principal_object_rights(target.caller_credential)?; + if !target_rights.contains(rights) { + return Err(BrokerError::PolicyDenied); + } + target.create_object_reference_with_rights(object, rights) + } + pub(crate) fn create_object_reference_pair( &self, first: ObjectEntry, @@ -576,6 +637,81 @@ mod tests { assert_eq!(session_references.pending_handles, 0); } + #[test] + fn supported_references_duplicate_between_sessions() { + let broker = BrokerCore::new_with_limits( + PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()), + BrokerCoreLimits::new(5, TEST_MAX_PIPE_CAPACITY) + .with_session_quotas(3, TEST_MAX_PIPE_CAPACITY_PER_SESSION), + Arc::new(crate::socket::UnsupportedSocketProvider), + Arc::new(crate::random::TestRandomProvider), + Arc::new(crate::stdio::UnsupportedStdioProvider), + ) + .unwrap(); + let source = broker + .create_session(CallerCredential::HostGuaranteed) + .unwrap(); + let target = broker + .create_session(CallerCredential::HostGuaranteed) + .unwrap(); + let denied_target = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + + let event = crate::event::create(&source, 1).unwrap(); + let duplicated_event = source + .duplicate_object_reference_to(event, &target, ObjectRights::WAIT) + .unwrap(); + assert_ne!(duplicated_event, event); + assert_eq!( + source.duplicate_object_reference_to(event, &denied_target, ObjectRights::WAIT), + Err(BrokerError::PolicyDenied) + ); + assert_eq!(source.close_object_reference(event), Ok(())); + assert_eq!( + crate::event::add(&target, duplicated_event, 1), + Err(BrokerError::InvalidRights) + ); + assert_eq!( + crate::event::consume(&target, duplicated_event, EventConsumeMode::One), + Ok(EventConsumption { + value: 1, + readiness: ReadinessFlags::WRITE, + }) + ); + assert_eq!( + target.duplicate_object_reference_to(duplicated_event, &source, ObjectRights::WRITE), + Err(BrokerError::InvalidRights) + ); + assert_eq!(target.close_object_reference(duplicated_event), Ok(())); + + let (reader, writer) = crate::pipe::create(&source, 4, 2).unwrap(); + let duplicated_writer = source + .duplicate_object_reference_to(writer, &target, ObjectRights::WRITE) + .unwrap(); + assert_eq!(source.close_object_reference(writer), Ok(())); + assert_eq!(crate::pipe::write(&target, duplicated_writer, &[1]), Ok(1)); + assert_eq!( + crate::pipe::read(&source, reader, 1), + Ok(std::vec::Vec::from([1])) + ); + + let first = crate::event::create(&target, 0).unwrap(); + let second = crate::event::create(&target, 0).unwrap(); + assert_eq!( + source.duplicate_object_reference_to(reader, &target, ObjectRights::WAIT), + Err(BrokerError::ResourceExhausted) + ); + assert_eq!(broker.references.read().len(), 4); + + assert_eq!(target.close_object_reference(first), Ok(())); + assert_eq!(target.close_object_reference(second), Ok(())); + assert_eq!(target.close_object_reference(duplicated_writer), Ok(())); + assert_eq!(source.close_object_reference(reader), Ok(())); + assert!(broker.references.read().is_empty()); + assert_eq!(broker.reserved_pipe_capacity.load(Ordering::Relaxed), 0); + } + #[test] fn object_reference_lifecycle_uses_public_core_constructor_once() { let socket_provider = Arc::new(crate::socket::tests::TestSocketProvider::default()); diff --git a/litebox_broker_core/src/socket/tests.rs b/litebox_broker_core/src/socket/tests.rs index 39afd9628a..d67640f789 100644 --- a/litebox_broker_core/src/socket/tests.rs +++ b/litebox_broker_core/src/socket/tests.rs @@ -1358,6 +1358,31 @@ pub(crate) fn check_socket_lifecycle(broker: &BrokerCore, provider: &TestSocketP check_duplicate_port_binding_retires_socket(broker, provider); } +#[test] +fn socket_reference_duplication_is_unsupported() { + let provider = Arc::new(TestSocketProvider::default()); + let broker = test_broker(Arc::clone(&provider) as Arc); + let source = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let target = broker + .create_session(CallerCredential::Unauthenticated) + .unwrap(); + let handle = create( + &source, + create_request(), + Arc::new(TestReadinessSink::default()), + ) + .unwrap(); + + assert_eq!( + source.duplicate_object_reference_to(handle, &target, ObjectRights::WAIT), + Err(BrokerError::UnsupportedOperation) + ); + assert_eq!(source.close_object_reference(handle), Ok(())); + assert!(broker.references.read().is_empty()); +} + fn check_platform_socket_retires_before_last_arc_drop( broker: &BrokerCore, provider: &TestSocketProvider, From 5f16c23d2188409118cf8fc9df4ddcac654ac129 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 4 Sep 2026 19:12:02 -0700 Subject: [PATCH 2/3] Remove redundant socket duplication test The duplication implementation directly rejects socket entries, so keep the focused event and pipe behavior coverage without a separate implementation-shaped socket test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/socket/tests.rs | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/litebox_broker_core/src/socket/tests.rs b/litebox_broker_core/src/socket/tests.rs index d67640f789..39afd9628a 100644 --- a/litebox_broker_core/src/socket/tests.rs +++ b/litebox_broker_core/src/socket/tests.rs @@ -1358,31 +1358,6 @@ pub(crate) fn check_socket_lifecycle(broker: &BrokerCore, provider: &TestSocketP check_duplicate_port_binding_retires_socket(broker, provider); } -#[test] -fn socket_reference_duplication_is_unsupported() { - let provider = Arc::new(TestSocketProvider::default()); - let broker = test_broker(Arc::clone(&provider) as Arc); - let source = broker - .create_session(CallerCredential::Unauthenticated) - .unwrap(); - let target = broker - .create_session(CallerCredential::Unauthenticated) - .unwrap(); - let handle = create( - &source, - create_request(), - Arc::new(TestReadinessSink::default()), - ) - .unwrap(); - - assert_eq!( - source.duplicate_object_reference_to(handle, &target, ObjectRights::WAIT), - Err(BrokerError::UnsupportedOperation) - ); - assert_eq!(source.close_object_reference(handle), Ok(())); - assert!(broker.references.read().is_empty()); -} - fn check_platform_socket_retires_before_last_arc_drop( broker: &BrokerCore, provider: &TestSocketProvider, From 121ae63a1cb00c91de3bcd04f4d954029c25ef44 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 4 Sep 2026 19:39:18 -0700 Subject: [PATCH 3/3] Reuse the broker core in duplication tests Run the cross-session duplication scenario through the existing single-core lifecycle test instead of constructing a second process-global broker core. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 239ec5f7-870a-4259-bcae-4ca85fb913a0 --- litebox_broker_core/src/session.rs | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/litebox_broker_core/src/session.rs b/litebox_broker_core/src/session.rs index 7b2f62a6ea..41fefa2369 100644 --- a/litebox_broker_core/src/session.rs +++ b/litebox_broker_core/src/session.rs @@ -637,25 +637,15 @@ mod tests { assert_eq!(session_references.pending_handles, 0); } - #[test] - fn supported_references_duplicate_between_sessions() { - let broker = BrokerCore::new_with_limits( - PolicyEngine::with_host_guaranteed_rights(ObjectRights::all()), - BrokerCoreLimits::new(5, TEST_MAX_PIPE_CAPACITY) - .with_session_quotas(3, TEST_MAX_PIPE_CAPACITY_PER_SESSION), - Arc::new(crate::socket::UnsupportedSocketProvider), - Arc::new(crate::random::TestRandomProvider), - Arc::new(crate::stdio::UnsupportedStdioProvider), - ) - .unwrap(); + fn check_supported_references_duplicate_between_sessions(broker: &BrokerCore) { let source = broker - .create_session(CallerCredential::HostGuaranteed) + .create_session(CallerCredential::Unauthenticated) .unwrap(); let target = broker - .create_session(CallerCredential::HostGuaranteed) + .create_session(CallerCredential::Unauthenticated) .unwrap(); let denied_target = broker - .create_session(CallerCredential::Unauthenticated) + .create_session(CallerCredential::HostGuaranteed) .unwrap(); let event = crate::event::create(&source, 1).unwrap(); @@ -696,16 +686,14 @@ mod tests { Ok(std::vec::Vec::from([1])) ); - let first = crate::event::create(&target, 0).unwrap(); - let second = crate::event::create(&target, 0).unwrap(); + let event = crate::event::create(&target, 0).unwrap(); assert_eq!( source.duplicate_object_reference_to(reader, &target, ObjectRights::WAIT), Err(BrokerError::ResourceExhausted) ); - assert_eq!(broker.references.read().len(), 4); + assert_eq!(broker.references.read().len(), 3); - assert_eq!(target.close_object_reference(first), Ok(())); - assert_eq!(target.close_object_reference(second), Ok(())); + assert_eq!(target.close_object_reference(event), Ok(())); assert_eq!(target.close_object_reference(duplicated_writer), Ok(())); assert_eq!(source.close_object_reference(reader), Ok(())); assert!(broker.references.read().is_empty()); @@ -744,6 +732,7 @@ mod tests { check_pending_references_count_toward_session_quota(&broker); check_pipe_capacity_quota_is_per_session(&broker); check_pipe_capacity_outlives_session_for_in_flight_object(&broker); + check_supported_references_duplicate_between_sessions(&broker); crate::socket::tests::check_socket_lifecycle(&broker, &socket_provider); check_pair_handle_exhaustion(&broker);