From 6b1b897b2e53a339e89bd4a337e6491330ee4b90 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 26 May 2026 17:07:08 -0700 Subject: [PATCH 01/66] broker design v0.1 --- docs/broker-design.md | 387 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 docs/broker-design.md diff --git a/docs/broker-design.md b/docs/broker-design.md new file mode 100644 index 000000000..d70b4238c --- /dev/null +++ b/docs/broker-design.md @@ -0,0 +1,387 @@ +# LiteBox Broker Split Design + +## Goal + +Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel platforms. + +This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, or another guest/runtime ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. + +The design separates LiteBox into: + +```text +Always user mode: + Shim + LocalCore + ShimPlatform + optional shim-specific local clients + +Authority domain: + BrokerCore + optional BrokerServices + BrokerPlatform +``` + +The authority domain differs by deployment: + +| Deployment | Broker location | +|---|---| +| **Userland platform** | privileged broker process | +| **Kernel platform** | kernel or equivalent trusted domain | + +## Component model + +```text +guest workload + | + v +Shim + | + v +LocalCore + optional shim-specific local clients + | + v +BrokerClient + | + v +BrokerCore + optional BrokerServices + | + v +BrokerPlatform +``` + +### Shim + +Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap handling, argument decoding, return-value conventions, exception delivery, frame construction, and guest-visible ABI details. + +### LocalCore + +Runs in user mode. Provides ergonomic support to the shim, local caching, fast-path helpers, guest memory marshalling, and per-workload views of state. + +LocalCore is **not trusted** for security. + +### ShimPlatform + +Runs in user mode. Provides only local execution mechanics: guest pointer representation, local TLS, local trampoline/entry support, local synchronization, and other process-local helpers. + +### BrokerClient + +Boundary adapter from local user-mode components to the broker authority domain. + +In userland, this is IPC. In kernel mode, this is a syscall/upcall-style ABI into the trusted domain. + +### BrokerCore + +Required, shim-neutral trusted substrate. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, readiness, and generic policy decisions. + +BrokerCore should not bake in every shim's ABI semantics. It provides the authority primitives that shims and broker services build on. + +### BrokerServices + +Optional trusted extensions hosted inside the broker authority domain. + +A BrokerService is useful when a shim or domain has security-relevant semantics that are too specific to belong in BrokerCore. Examples include OP-TEE TA/session authority, secure-storage semantics, PTA access control, or another guest ABI's domain-specific resource model. + +BrokerServices should not reinvent authority. They should use BrokerCore capabilities, identities, memory grants, wait queues, lifecycle state, and accounting wherever possible. + +### BrokerPlatform + +Trusted backend for privileged operations: address-space control, host I/O, filesystem/device/network access, randomness/secrets, timers, scheduling hooks, firewall enforcement, and platform-specific primitives. + +## Two interfaces + +There are two conceptual interfaces and one physical crossing. + +| Interface | Userland deployment | Kernel deployment | Purpose | +|---|---|---|---| +| **Shim <-> LocalCore** | same address space | same address space | ergonomic user-mode ABI implementation | +| **LocalCore / shim-specific local client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | + +The broker crossing should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService: + +```text +BrokerRequest { + caller_identity, + target: BrokerCore | BrokerService(service_id), + operation, + handles, + memory_grants, + payload, +} +``` + +The crossing must be explicit, stable, handle-based, versioned, and switch-friendly. The user-mode interface can remain ergonomic because it always stays in user mode. + +Logical API split: + +| API | Shape | +|---|---| +| `Shim -> LocalCore` | ergonomic in-process API | +| `Shim -> shim-specific local client` | ergonomic typed client for optional BrokerService | +| `LocalCore -> BrokerCore` | generic capability/resource protocol | +| `shim-specific local client -> BrokerService` | shim/domain-specific protocol | +| `BrokerService -> BrokerCore` | trusted in-domain API | + +The "shim-specific local client" is not a new authority layer. It is the local, typed client-side half of an optional BrokerService. + +## Deployment contract and negotiation + +The local runner and global broker should match through a shared deployment contract, not ad hoc discovery. + +Shared spec crates should define: + +- the broker envelope and handle/memory-grant formats; +- BrokerCore protocol versions; +- BrokerService IDs, protocol versions, request/response types, and feature requirements; +- BrokerPlatform feature names and capability profiles; +- deployment profiles that bind a shim, local platform, required services, and required broker features. + +Startup should fail closed: + +1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. +2. The local side connects to the broker and sends required BrokerCore, BrokerService, and BrokerPlatform feature versions. +3. The broker replies with supported services and capabilities. +4. The local side starts only if the required versions and features match. + +ShimPlatform should not depend on kernel internals. It should depend on a negotiated BrokerPlatform capability profile: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. The broker owns enforcement; local code only adapts to supported capabilities. + +## Security invariant + +The core security rule is: + +> User-mode Shim, LocalCore, and ShimPlatform may request operations and cache derived state, but they must never create authority. + +Therefore BrokerCore, BrokerServices, and BrokerPlatform must be authoritative for: + +- workload, process, session, thread, and namespace identity; +- handle/resource capabilities; +- handle passing, duplication, revocation, and cleanup; +- shared memory and mapping permissions; +- filesystem, network, device, and host I/O policy; +- signal, event, wait, readiness, and lifecycle state; +- randomness, secrets, and trusted time; +- quotas and revocation; +- network and application-level firewall policy; +- shim/domain-specific trusted semantics when a BrokerService is present. + +A compromised user-mode shim/core should not be able to escape the broker-granted authority. + +## State ownership + +| State | Owner | +|---|---| +| guest ABI decoding state | Shim | +| per-workload cache/view | LocalCore | +| shim-specific local typed client state | optional local client | +| guest memory marshalling | LocalCore + broker revalidation | +| private synchronization fast paths | LocalCore | +| shared synchronization | BrokerCore | +| guest-visible handle numbers | LocalCore view, BrokerCore authority | +| open/shared resource descriptions | BrokerCore | +| shim/domain-specific authoritative state | optional BrokerService backed by BrokerCore | +| IPC/event/queue/socket-like resources | BrokerCore-owned resources | +| address-space mappings | BrokerCore/BrokerPlatform authority | +| host-visible I/O | BrokerPlatform | +| process/session/workload lifecycle | BrokerCore | + +## Network and firewall enforcement + +The design can support a network or application-level firewall, but only if the broker is the authoritative network path. + +In that configuration, LocalCore and ShimPlatform must not send or receive traffic directly through a platform network device. They may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, applies policy, and then invokes BrokerPlatform to interact with the real network backend. + +Possible datapath shapes: + +| Shape | Security property | Performance tradeoff | +|---|---|---| +| broker-mediated control and data | simplest to reason about; every byte is broker-visible | highest IPC/switch overhead | +| broker-approved shared rings | broker still owns policy and drains/fills rings | lower overhead, more queue/accounting complexity | +| broker protocol proxy | enables application-level policy when broker sees plaintext/protocol metadata | protocol-specific and more complex | + +Layer-specific implications: + +- Packet and connection policy can be enforced by the broker from packet headers, connection metadata, endpoint capabilities, and resource labels. +- Application-level policy requires the broker to see application-level metadata or plaintext. If the guest workload performs end-to-end encryption entirely inside its own address space, the broker can still enforce connection-level policy, but it cannot inspect encrypted payloads unless the design explicitly uses a broker proxy, broker-managed protocol endpoint, or broker-controlled keys. +- Centralizing traffic in the broker creates a throughput and latency bottleneck. The design should plan for batching, shared-memory rings, flow control, per-resource quotas, and efficient policy caching. + +## Performance model + +The design should avoid "every operation is remote" while preserving security. + +Use: + +- local guest ABI decoding; +- local cache for non-authoritative handle/process/session views; +- direct user-mode fast paths for private state; +- batched broker calls where possible; +- shared-memory rings for bulk IPC and network data; +- broker-mediated setup with local data-plane access where security allows; +- explicit invalidation/revocation for stale LocalCore caches. + +The broker path is required for authority changes, cross-workload operations, shared resources, host-visible effects, and firewall-enforced traffic. + +## Why this is better than moving all core into broker + +Moving the whole core into broker would make the trusted boundary too large and too chatty. It would also force guest pointer handling, guest ABI compatibility policy, and shim-specific logic into the trusted domain. + +This split keeps the trusted computing base smaller: + +```text +User mode: + compatibility, marshalling, caching, fast paths + +Authority domain: + validation, capabilities, shared state, optional domain authority, host effects +``` + +## Main risks + +| Risk | Mitigation | +|---|---| +| LocalCore cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | +| user shim bypasses policy | broker validates every security-relevant request | +| ABI becomes too chatty | batching, shared memory data planes, local private fast paths | +| duplicated logic | keep policy/authority in BrokerCore; keep ABI translation in LocalCore | +| handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | +| address-space lifecycle complexity | broker-authoritative mappings, shared object IDs, careful copy-on-write/shared-memory design | +| firewall datapath bottleneck | shared rings, batching, quotas, policy caching, and broker-side flow control | +| encrypted traffic hides application data | enforce metadata/connection policy unless using broker proxying or broker-managed keys/endpoints | +| BrokerServices become a second monolithic core | make them optional, small, versioned, and backed by BrokerCore primitives | +| local platform depends on trusted-domain internals | expose negotiated BrokerPlatform capability profiles instead of implementation details | + +## Prior-art positioning + +LiteBox's proposed design combines ideas from several systems: + +- **Drawbridge / Graphene / Gramine**: user-mode library OS compatibility. +- **gVisor**: guest ABI mediation outside the host kernel fast path. +- **Exokernel / seL4-like systems**: small trusted authority boundary and explicit capabilities. +- **Userland broker systems**: privileged service owns shared state and host effects. + +The distinctive LiteBox claim is one architecture that supports both: + +```text +userland broker process +kernel broker +``` + +while keeping: + +```text +Shim + LocalCore + ShimPlatform +``` + +always in user mode. + +## Mapping current components + +The current code does not match the final boundaries exactly. In particular, some current shims and platforms are linked together in the trusted domain for existing deployments. The mapping below describes the intended migration target. + +### `litebox_shim_optee` + +Today, this crate combines OP-TEE ABI handling, TA loading, per-TA state, page-manager use, syscall dispatch, and some session/object/crypto bookkeeping. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| OP-TEE entry/request decoding, return conventions, TA ABI details | Shim | +| TA-local syscall helpers, guest buffer marshalling, local loader helpers | LocalCore | +| non-authoritative TA/session/object caches | LocalCore | +| authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access control | OP-TEE BrokerService backed by BrokerCore | +| generic identities, capabilities, memory grants, lifecycle, wait/notify, accounting | BrokerCore | +| trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform | + +This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable local-core and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. + +### `litebox` + +Today, `litebox` is an in-process core library. Its current `LiteBox` object and subsystems assume Rust references, generic platform traits, in-process locks, and in-process descriptor/resource identity. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| ergonomic in-process helpers used by shims | LocalCore | +| guest-visible handle table view | LocalCore backed by BrokerCore | +| shared resource identity/lifetime | BrokerCore | +| synchronization/wait/readiness authority for shared objects | BrokerCore | +| platform trait surface | split into ShimPlatform and BrokerPlatform traits | +| shim/domain-specific authority | optional BrokerServices, not generic BrokerCore | + +The important change is that the current core API should not become the cross-boundary ABI. LocalCore can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. + +### `litebox_platform_lvbs` + +Today, this crate is effectively a trusted-domain platform: it owns page tables, user-memory validation, VTL switching, syscall/exception entry, host calls, randomness/secrets hooks, and network backend hooks. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| page-table and address-space management | BrokerPlatform | +| VTL/trusted-domain entry and dispatch glue | BrokerPlatform / broker entry layer | +| normal-world memory mapping and validation | BrokerPlatform | +| host I/O, network backend hooks, root key/secrets | BrokerPlatform | +| user-mode shim local helpers | new/extracted ShimPlatform surface, not the current crate wholesale | + +In the new model, LVBS is mostly the kernel/trusted-domain BrokerPlatform. The user-mode ShimPlatform for an OP-TEE workload should be a smaller local execution layer that learns available trusted-domain behavior through a negotiated capability profile, not by depending on LVBS internals. + +### `litebox_runner_lvbs` + +Today, this crate boots the trusted-domain runtime, initializes the LVBS platform, dispatches VTL calls, handles OP-TEE messages, creates task page tables, manages sessions, and directly invokes the OP-TEE shim. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| early boot and trusted-domain initialization | broker bootstrap | +| VTL call dispatch | broker external entry layer | +| session/page-table orchestration | BrokerCore + OP-TEE BrokerService + BrokerPlatform | +| direct long-lived ownership of shim objects | should move out of trusted domain or become broker-managed process/session objects | +| local/broker compatibility checks | deployment profile negotiation | + +The runner becomes less of an application runner and more of a broker bootstrap/entrypoint for the trusted deployment. + +### OP-TEE-on-userland runner + +The current OP-TEE userland runner sets a platform, builds `OpteeShim`, loads binaries, optionally rewrites syscall instructions, and runs the workload in one process. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| command-line harness and binary loading for tests | runner/test harness | +| `OpteeShim` construction | Shim + LocalCore process setup | +| platform selection for local execution | ShimPlatform setup | +| broker/service compatibility | deployment profile negotiation | +| shared/security-authoritative state | separate privileged broker process | + +This runner is a good prototype for the userland deployment shape, but it currently lacks a separate broker authority. + +### `litebox_platform_multiplex` + +Today, this crate chooses one monolithic platform type at compile time. + +Future mapping: + +| Current responsibility | Target component | +|---|---| +| selecting a single `Platform` | split into ShimPlatform selection and BrokerPlatform selection | +| global platform accessor for shim-side code | ShimPlatform accessor | +| trusted backend selection | BrokerPlatform accessor inside the broker | + +This split is needed because the current platform traits mix local execution mechanics with trusted authority. + +## Initial implementation direction + +The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority substrate: + +1. Define the component split: `Shim`, `LocalCore`, `ShimPlatform`, `BrokerClient`, `BrokerCore`, optional `BrokerServices`, and `BrokerPlatform`. +2. Define the shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, and deployment profile format. +3. Define broker-owned identity for workloads, processes, sessions, and threads. +4. Define broker-owned capability/resource IDs with generation checks. +5. Make LocalCore handle tables broker-backed views. +6. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, and BrokerPlatform features. +7. Add one simple broker-owned shared resource, such as an event object or pipe-like queue. +8. Add a broker wait/wakeup channel. +9. Prototype broker-owned network resources with firewall enforcement. +10. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. +11. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. + +That gives a controlled path from current single-process/single-session assumptions toward true shared-state support without rewriting every shim and platform at once. From 6259a2b3171401afe59c44c4f40669b08407d6a9 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 26 May 2026 19:26:01 -0700 Subject: [PATCH 02/66] updated --- docs/broker-design.md | 320 ++++++++++++++++++++++++++++-------------- 1 file changed, 217 insertions(+), 103 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index d70b4238c..50bc79b57 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -4,72 +4,103 @@ Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel platforms. -This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, or another guest/runtime ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. +This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, or another guest ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. The design separates LiteBox into: ```text Always user mode: - Shim + LocalCore + ShimPlatform - optional shim-specific local clients + Shim + UserCore + UserPlatform + optional shim-specific user clients + +UserPlatform support: + hosted userland: existing host OS/user ABI + broker kernel: BrokerHost Authority domain: - BrokerCore + optional BrokerServices + BrokerPlatform + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform ``` The authority domain differs by deployment: -| Deployment | Broker location | -|---|---| -| **Userland platform** | privileged broker process | -| **Kernel platform** | kernel or equivalent trusted domain | +| Deployment | Broker location | UserPlatform support | +|---|---|---| +| **Userland platform** | privileged broker process | existing host OS/user ABI | +| **Kernel platform** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry transport without decoding broker requests | ## Component model ```text -guest workload - | - v -Shim - | - v -LocalCore + optional shim-specific local clients - | - v -BrokerClient - | - v -BrokerCore + optional BrokerServices - | - v -BrokerPlatform +User mode: + guest workload + | + v + Shim + | + v + UserCore + optional shim-specific user clients + -- via BrokerClient adapter over UserPlatform transport --> + broker authority interface + + UserPlatform implements the traits required by UserCore and provides the transport used by the BrokerClient adapter. + +Hosted userland: + UserPlatform <-> host OS/user ABI + +Broker-kernel deployment: + UserPlatform <-> BrokerHost + +Authority domain: + broker authority interface -> BrokerCore + optional BrokerServices + | | + | consult | execute after authorization + v v + PolicyEngine BrokerPlatform ``` ### Shim Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap handling, argument decoding, return-value conventions, exception delivery, frame construction, and guest-visible ABI details. -### LocalCore +### UserCore Runs in user mode. Provides ergonomic support to the shim, local caching, fast-path helpers, guest memory marshalling, and per-workload views of state. -LocalCore is **not trusted** for security. +UserCore is **not trusted** for security. + +### UserPlatform + +Runs in user mode. Implements the platform traits required by UserCore. Provides deployment-specific local execution mechanics: guest pointer representation, local TLS, local trampoline/entry support, local synchronization, local memory allocation support, logging/debug plumbing, and the low-level transport used by `BrokerClient`. -### ShimPlatform +UserPlatform is **not trusted** for security; it executes in the same user-mode context as the guest and UserCore. -Runs in user mode. Provides only local execution mechanics: guest pointer representation, local TLS, local trampoline/entry support, local synchronization, and other process-local helpers. +UserPlatform is not intended to be universal. A hosted userland UserPlatform may use native host syscalls for private, non-authoritative mechanics. A broker-kernel deployment either must provide the user ABI that this UserPlatform expects or must select a different UserPlatform. -### BrokerClient +UserPlatform knows the local execution ABI and the broker transport/profile. It talks only to the host support layer, which is the host OS/user ABI in hosted userland or BrokerHost in broker-kernel deployments. It does **not** call BrokerCore, BrokerServices, or BrokerPlatform directly, and it does not interpret broker-authority payloads. -Boundary adapter from local user-mode components to the broker authority domain. +### BrokerClient adapter -In userland, this is IPC. In kernel mode, this is a syscall/upcall-style ABI into the trusted domain. +Thin in-process adapter used by UserCore and shim-specific user clients to call the broker authority interface. + +BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the transport supplied by UserPlatform. + +### BrokerHost + +Kernel-side host support for UserPlatform in broker-kernel deployments. + +BrokerHost provides the user-mode execution substrate that UserPlatform expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker transport endpoints, and possibly a compatibility ABI subset. + +BrokerHost is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is the host-side component that lets user-mode LiteBox processes run and reach the broker. It may be trusted kernel code, but it should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. + +BrokerHost may carry broker authority traffic as transport, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry layer in BrokerCore, not BrokerHost. + +In broker-kernel deployments, BrokerHost shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; BrokerHost code is in the TCB and must be audited accordingly. ### BrokerCore -Required, shim-neutral trusted substrate. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, readiness, and generic policy decisions. +Required, shim-neutral trusted substrate. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, and readiness. -BrokerCore should not bake in every shim's ABI semantics. It provides the authority primitives that shims and broker services build on. +BrokerCore should not bake in every shim's ABI semantics. It provides the authority primitives that shims, broker services, and PolicyEngine build on. BrokerCore enforces structural invariants, such as capability validity and object lifetime, and supplies state/context to PolicyEngine for authorization. ### BrokerServices @@ -77,22 +108,39 @@ Optional trusted extensions hosted inside the broker authority domain. A BrokerService is useful when a shim or domain has security-relevant semantics that are too specific to belong in BrokerCore. Examples include OP-TEE TA/session authority, secure-storage semantics, PTA access control, or another guest ABI's domain-specific resource model. -BrokerServices should not reinvent authority. They should use BrokerCore capabilities, identities, memory grants, wait queues, lifecycle state, and accounting wherever possible. +BrokerServices should not reinvent authority. They should use BrokerCore capabilities, identities, memory grants, wait queues, lifecycle state, and accounting wherever possible. A BrokerService must not grant or exercise authority over a resource without going through BrokerCore's capability/lifecycle primitives and PolicyEngine authorization, even when its own protocol is shim-specific. + +### PolicyEngine + +Trusted policy decision and audit component inside the broker authority domain. + +PolicyEngine is the broker's reference-monitor component. BrokerCore and BrokerServices gather context, validate structural invariants, and ask PolicyEngine to authorize authority-changing or host-effecting operations before BrokerCore mutates authoritative state, a BrokerService grants domain authority, or BrokerPlatform performs backend execution. + +PolicyEngine should not own all broker state. It consumes facts from BrokerCore and BrokerServices, returns allow/deny decisions plus constraints, and emits audit records. Keeping policy decisions here prevents firewall, filesystem, device, storage, and domain-specific access checks from being hidden in BrokerPlatform or duplicated across BrokerServices. ### BrokerPlatform -Trusted backend for privileged operations: address-space control, host I/O, filesystem/device/network access, randomness/secrets, timers, scheduling hooks, firewall enforcement, and platform-specific primitives. +Trusted backend for privileged operations: address-space control, host I/O, filesystem/device/network access, randomness/secrets, timers, scheduling hooks, host-side execution of PolicyEngine-authorized operations, and platform-specific primitives. -## Two interfaces +## Three interfaces -There are two conceptual interfaces and one physical crossing. +There are three logical interfaces. In a broker-kernel deployment, the UserPlatform/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding, validation, dispatch, and authorization remain broker-authority responsibilities. | Interface | Userland deployment | Kernel deployment | Purpose | |---|---|---|---| -| **Shim <-> LocalCore** | same address space | same address space | ergonomic user-mode ABI implementation | -| **LocalCore / shim-specific local client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | +| **Shim <-> UserCore** | same address space | same address space | ergonomic user-mode ABI implementation | +| **UserPlatform <-> host support layer** | host OS/user ABI | BrokerHost ABI | local non-authoritative mechanics | +| **UserCore / shim-specific user client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | -The broker crossing should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService: +The interfaces have different stability and trust requirements: + +| Interface | Shape | Authority | +|---|---|---| +| `Shim <-> UserCore` | ergonomic in-process API | no authority; user-mode compatibility layer | +| `UserPlatform <-> host support layer` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | +| `UserCore / shim-specific user client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | + +The broker authority interface should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService: ```text BrokerRequest { @@ -105,48 +153,78 @@ BrokerRequest { } ``` -The crossing must be explicit, stable, handle-based, versioned, and switch-friendly. The user-mode interface can remain ergonomic because it always stays in user mode. +PolicyEngine is not a user-callable broker target. BrokerCore and BrokerServices call it inside the authority domain before granting authority, mutating protected state, or invoking BrokerPlatform for host-visible effects. -Logical API split: +The negotiated policy profile is bound to the authenticated broker session or deployment profile, not chosen by each request. It only needs an explicit request field if a future design supports multiple simultaneous policy profiles on one authenticated transport. + +The broker authority interface must be explicit, stable, handle-based, versioned, and switch-friendly. The UserPlatform/host-support interface is also versioned, but it is selected per deployment and only supports local mechanics. The Shim/UserCore interface can remain ergonomic because it always stays in user mode. + +External broker authority APIs: | API | Shape | |---|---| -| `Shim -> LocalCore` | ergonomic in-process API | -| `Shim -> shim-specific local client` | ergonomic typed client for optional BrokerService | -| `LocalCore -> BrokerCore` | generic capability/resource protocol | -| `shim-specific local client -> BrokerService` | shim/domain-specific protocol | +| `UserCore -> BrokerCore` | generic capability/resource protocol | +| `shim-specific user client -> BrokerService` | shim/domain-specific protocol | + +Internal broker-authority APIs: + +| API | Shape | +|---|---| +| `BrokerCore / BrokerService -> PolicyEngine` | in-domain authorization and audit | | `BrokerService -> BrokerCore` | trusted in-domain API | +| `BrokerCore / BrokerService -> BrokerPlatform` | backend execution after PolicyEngine authorization | + +The "shim-specific user client" is not a new authority layer. It is the user-mode, typed client-side half of an optional BrokerService. + +## UserPlatform and host calls -The "shim-specific local client" is not a new authority layer. It is the local, typed client-side half of an optional BrokerService. +UserPlatform may use local host/kernel calls, but only for local mechanics that do not create LiteBox authority. + +| UserPlatform operation | Allowed? | Requirement | +|---|---|---| +| private memory allocation, TLS, logging, local scratch mappings | yes | must not grant guest-visible authority | +| private locks/futex-like synchronization | yes | must not represent broker-owned shared state | +| broker transport notification | yes | broker validates every request | +| direct host file, network, or device access for guest-visible resources | no, unless broker-granted | must be mediated by PolicyEngine-authorized broker policy | +| guest-visible mappings or executable/shared memory | only with broker grant | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform applies | +| trusted randomness, secrets, or security-sensitive time | no | must come from broker authority | + +If a UserPlatform uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through BrokerHost or the runner must select a different UserPlatform. The stable portability target is the shim/UserCore/broker contract, not a single universal UserPlatform binary. ## Deployment contract and negotiation -The local runner and global broker should match through a shared deployment contract, not ad hoc discovery. +The user-side runner and global broker should match through a shared deployment contract, not ad hoc discovery. Shared spec crates should define: - the broker envelope and handle/memory-grant formats; +- broker transport authentication and peer identity binding; - BrokerCore protocol versions; - BrokerService IDs, protocol versions, request/response types, and feature requirements; -- BrokerPlatform feature names and capability profiles; -- deployment profiles that bind a shim, local platform, required services, and required broker features. +- PolicyEngine policy versions, policy profile IDs, and audit requirements; +- broker capability names and profiles; +- UserPlatform/BrokerHost ABI names and versions; +- deployment profiles that bind a shim, UserPlatform, broker transport, required services, and required broker features. Startup should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. -2. The local side connects to the broker and sends required BrokerCore, BrokerService, and BrokerPlatform feature versions. -3. The broker replies with supported services and capabilities. -4. The local side starts only if the required versions and features match. +2. The runner selects a UserPlatform that matches the deployment's host ABI. +3. The user side establishes an authenticated broker transport. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. +4. The broker binds the caller identity used in `BrokerRequest` to the authenticated peer. User mode does not choose its own authority identity. +5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserPlatform, and BrokerHost versions. +6. The broker replies with supported services and capabilities. +7. The user side starts only if the required versions and features match. -ShimPlatform should not depend on kernel internals. It should depend on a negotiated BrokerPlatform capability profile: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. The broker owns enforcement; local code only adapts to supported capabilities. +UserPlatform should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. ## Security invariant The core security rule is: -> User-mode Shim, LocalCore, and ShimPlatform may request operations and cache derived state, but they must never create authority. +> User-mode Shim, UserCore, and UserPlatform may request operations and cache derived state, but they must never create authority. -Therefore BrokerCore, BrokerServices, and BrokerPlatform must be authoritative for: +Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be authoritative for: - workload, process, session, thread, and namespace identity; - handle/resource capabilities; @@ -155,49 +233,60 @@ Therefore BrokerCore, BrokerServices, and BrokerPlatform must be authoritative f - filesystem, network, device, and host I/O policy; - signal, event, wait, readiness, and lifecycle state; - randomness, secrets, and trusted time; +- timers and scheduling-visible state; - quotas and revocation; - network and application-level firewall policy; - shim/domain-specific trusted semantics when a BrokerService is present. A compromised user-mode shim/core should not be able to escape the broker-granted authority. +All authority-changing or host-effecting operations must be authorized by PolicyEngine before BrokerCore state mutation, BrokerService authority grants, or BrokerPlatform backend execution. BrokerCore remains responsible for structural capability/lifecycle validity; BrokerServices provide domain-specific context; PolicyEngine makes the policy decision. + +Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. UserCore and UserPlatform must be assumed hostile for the duration of an in-flight request. + ## State ownership | State | Owner | |---|---| | guest ABI decoding state | Shim | -| per-workload cache/view | LocalCore | -| shim-specific local typed client state | optional local client | -| guest memory marshalling | LocalCore + broker revalidation | -| private synchronization fast paths | LocalCore | +| per-workload cache/view | UserCore | +| shim-specific user client state | optional shim-specific user client | +| guest memory marshalling | UserCore + broker revalidation | +| private user-platform mechanics | UserPlatform via host support layer | +| private synchronization fast paths | UserCore | | shared synchronization | BrokerCore | -| guest-visible handle numbers | LocalCore view, BrokerCore authority | +| guest-visible handle numbers | UserCore view, BrokerCore authority | | open/shared resource descriptions | BrokerCore | | shim/domain-specific authoritative state | optional BrokerService backed by BrokerCore | +| policy decisions, constraints, and audit records | PolicyEngine | | IPC/event/queue/socket-like resources | BrokerCore-owned resources | -| address-space mappings | BrokerCore/BrokerPlatform authority | -| host-visible I/O | BrokerPlatform | +| guest-visible/security-sensitive address-space mappings | BrokerCore + PolicyEngine + BrokerPlatform | +| user-platform-only scratch mappings | UserPlatform via host support layer | +| host-visible I/O | BrokerPlatform executes PolicyEngine-authorized policy | | process/session/workload lifecycle | BrokerCore | +UserCore-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. + ## Network and firewall enforcement The design can support a network or application-level firewall, but only if the broker is the authoritative network path. -In that configuration, LocalCore and ShimPlatform must not send or receive traffic directly through a platform network device. They may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, applies policy, and then invokes BrokerPlatform to interact with the real network backend. +In that configuration, UserCore and UserPlatform must not send or receive guest-visible traffic directly through a platform network device. They may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. Possible datapath shapes: | Shape | Security property | Performance tradeoff | |---|---|---| | broker-mediated control and data | simplest to reason about; every byte is broker-visible | highest IPC/switch overhead | -| broker-approved shared rings | broker still owns policy and drains/fills rings | lower overhead, more queue/accounting complexity | +| broker-approved shared rings | broker still owns state and PolicyEngine authorizes policy | lower overhead, more queue/accounting complexity | | broker protocol proxy | enables application-level policy when broker sees plaintext/protocol metadata | protocol-specific and more complex | Layer-specific implications: -- Packet and connection policy can be enforced by the broker from packet headers, connection metadata, endpoint capabilities, and resource labels. -- Application-level policy requires the broker to see application-level metadata or plaintext. If the guest workload performs end-to-end encryption entirely inside its own address space, the broker can still enforce connection-level policy, but it cannot inspect encrypted payloads unless the design explicitly uses a broker proxy, broker-managed protocol endpoint, or broker-controlled keys. +- Packet and connection policy can be authorized by PolicyEngine from packet headers, connection metadata, endpoint capabilities, and resource labels supplied by BrokerCore/BrokerServices. +- Application-level policy requires PolicyEngine or a broker-side policy helper to see application-level metadata or plaintext. If the guest workload performs end-to-end encryption entirely inside its own address space, the broker can still enforce connection-level policy, but it cannot inspect encrypted payloads unless the design explicitly uses a broker proxy, broker-managed protocol endpoint, or broker-controlled keys. - Centralizing traffic in the broker creates a throughput and latency bottleneck. The design should plan for batching, shared-memory rings, flow control, per-resource quotas, and efficient policy caching. +- Broker-side policy decisions must be made on stable data. If packet descriptors or payload metadata arrive through shared memory, the broker must snapshot or revalidate them before enforcement. ## Performance model @@ -207,11 +296,12 @@ Use: - local guest ABI decoding; - local cache for non-authoritative handle/process/session views; -- direct user-mode fast paths for private state; +- direct UserPlatform fast paths for private state; - batched broker calls where possible; - shared-memory rings for bulk IPC and network data; - broker-mediated setup with local data-plane access where security allows; -- explicit invalidation/revocation for stale LocalCore caches. +- cached PolicyEngine decisions when the cache key includes all security-relevant context and supports revocation; +- explicit invalidation/revocation for stale UserCore caches. The broker path is required for authority changes, cross-workload operations, shared resources, host-visible effects, and firewall-enforced traffic. @@ -226,23 +316,27 @@ User mode: compatibility, marshalling, caching, fast paths Authority domain: - validation, capabilities, shared state, optional domain authority, host effects + validation, capabilities, shared state, policy enforcement, optional domain authority, host effects ``` ## Main risks | Risk | Mitigation | |---|---| -| LocalCore cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | -| user shim bypasses policy | broker validates every security-relevant request | +| UserCore cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | +| user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | | ABI becomes too chatty | batching, shared memory data planes, local private fast paths | -| duplicated logic | keep policy/authority in BrokerCore; keep ABI translation in LocalCore | +| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in UserCore | | handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | | address-space lifecycle complexity | broker-authoritative mappings, shared object IDs, careful copy-on-write/shared-memory design | | firewall datapath bottleneck | shared rings, batching, quotas, policy caching, and broker-side flow control | | encrypted traffic hides application data | enforce metadata/connection policy unless using broker proxying or broker-managed keys/endpoints | | BrokerServices become a second monolithic core | make them optional, small, versioned, and backed by BrokerCore primitives | -| local platform depends on trusted-domain internals | expose negotiated BrokerPlatform capability profiles instead of implementation details | +| UserPlatform depends on unavailable host ABI | select UserPlatform by deployment profile, or provide the needed ABI through BrokerHost | +| UserPlatform depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | +| shared-memory TOCTOU or double-fetch bugs | validate broker requests against private snapshots or revalidate every use of user-controlled fields | +| unauthenticated broker transport | authenticate peers before negotiation and bind `caller_identity` to the authenticated transport endpoint | +| PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | ## Prior-art positioning @@ -263,7 +357,7 @@ kernel broker while keeping: ```text -Shim + LocalCore + ShimPlatform +Shim + UserCore + UserPlatform ``` always in user mode. @@ -272,6 +366,8 @@ always in user mode. The current code does not match the final boundaries exactly. In particular, some current shims and platforms are linked together in the trusted domain for existing deployments. The mapping below describes the intended migration target. +Where a current crate spans both user-mode and authority responsibilities, the mapping below describes the destination split, not a one-to-one rename. + ### `litebox_shim_optee` Today, this crate combines OP-TEE ABI handling, TA loading, per-TA state, page-manager use, syscall dispatch, and some session/object/crypto bookkeeping. @@ -281,13 +377,14 @@ Future mapping: | Current responsibility | Target component | |---|---| | OP-TEE entry/request decoding, return conventions, TA ABI details | Shim | -| TA-local syscall helpers, guest buffer marshalling, local loader helpers | LocalCore | -| non-authoritative TA/session/object caches | LocalCore | -| authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access control | OP-TEE BrokerService backed by BrokerCore | +| TA-local syscall helpers, guest buffer marshalling, local loader helpers | UserCore, with broker revalidation for broker-visible data | +| non-authoritative TA/session/object caches | UserCore | +| authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access-control context | OP-TEE BrokerService backed by BrokerCore | +| OP-TEE policy decisions and audit, including PTA and secure-storage authorization | PolicyEngine | | generic identities, capabilities, memory grants, lifecycle, wait/notify, accounting | BrokerCore | -| trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform | +| trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform after PolicyEngine authorization | -This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable local-core and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. +This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable UserCore and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. ### `litebox` @@ -297,14 +394,14 @@ Future mapping: | Current responsibility | Target component | |---|---| -| ergonomic in-process helpers used by shims | LocalCore | -| guest-visible handle table view | LocalCore backed by BrokerCore | +| ergonomic in-process helpers used by shims | UserCore | +| guest-visible handle table view | UserCore backed by BrokerCore | | shared resource identity/lifetime | BrokerCore | | synchronization/wait/readiness authority for shared objects | BrokerCore | -| platform trait surface | split into ShimPlatform and BrokerPlatform traits | -| shim/domain-specific authority | optional BrokerServices, not generic BrokerCore | +| platform trait surface | split into UserPlatform, BrokerHost, and BrokerPlatform surfaces | +| shim/domain-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | -The important change is that the current core API should not become the cross-boundary ABI. LocalCore can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. +The important change is that the current core API should not become the cross-boundary ABI. UserCore can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. ### `litebox_platform_lvbs` @@ -315,26 +412,30 @@ Future mapping: | Current responsibility | Target component | |---|---| | page-table and address-space management | BrokerPlatform | -| VTL/trusted-domain entry and dispatch glue | BrokerPlatform / broker entry layer | +| VTL/trusted-domain trap and transport mechanism | BrokerHost | +| broker request decode, validation, and dispatch | broker entry layer in BrokerCore | | normal-world memory mapping and validation | BrokerPlatform | -| host I/O, network backend hooks, root key/secrets | BrokerPlatform | -| user-mode shim local helpers | new/extracted ShimPlatform surface, not the current crate wholesale | +| host I/O, network backend hooks, root key/secrets | BrokerPlatform executing PolicyEngine-authorized operations | +| user-mode shim platform helpers | new/extracted UserPlatform surface, not the current crate wholesale | -In the new model, LVBS is mostly the kernel/trusted-domain BrokerPlatform. The user-mode ShimPlatform for an OP-TEE workload should be a smaller local execution layer that learns available trusted-domain behavior through a negotiated capability profile, not by depending on LVBS internals. +In the new model, LVBS contributes both BrokerPlatform and BrokerHost pieces. BrokerPlatform owns privileged backend authority. BrokerHost exposes the host ABI that lets a user-mode UserPlatform execute and reach the broker. The LVBS-targeted UserPlatform selected by a deployment profile, such as `optee-on-lvbs`, should be a smaller platform layer than the current LVBS crate. ### `litebox_runner_lvbs` -Today, this crate boots the trusted-domain runtime, initializes the LVBS platform, dispatches VTL calls, handles OP-TEE messages, creates task page tables, manages sessions, and directly invokes the OP-TEE shim. +Today, this crate boots the trusted-domain environment, initializes the LVBS platform, dispatches VTL calls, handles OP-TEE messages, creates task page tables, manages sessions, and directly invokes the OP-TEE shim. Future mapping: | Current responsibility | Target component | |---|---| | early boot and trusted-domain initialization | broker bootstrap | -| VTL call dispatch | broker external entry layer | -| session/page-table orchestration | BrokerCore + OP-TEE BrokerService + BrokerPlatform | -| direct long-lived ownership of shim objects | should move out of trusted domain or become broker-managed process/session objects | +| VTL trap/transport dispatch | BrokerHost | +| broker request decode, validation, and dispatch | broker entry layer in BrokerCore | +| session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | +| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + UserCore | +| authoritative TA/session/object identity currently held by the runner | BrokerCore + OP-TEE BrokerService + PolicyEngine | | local/broker compatibility checks | deployment profile negotiation | +| local user ABI support | BrokerHost | The runner becomes less of an application runner and more of a broker bootstrap/entrypoint for the trusted deployment. @@ -347,8 +448,8 @@ Future mapping: | Current responsibility | Target component | |---|---| | command-line harness and binary loading for tests | runner/test harness | -| `OpteeShim` construction | Shim + LocalCore process setup | -| platform selection for local execution | ShimPlatform setup | +| `OpteeShim` construction | Shim + UserCore process setup | +| platform selection for local execution | UserPlatform setup | | broker/service compatibility | deployment profile negotiation | | shared/security-authoritative state | separate privileged broker process | @@ -362,25 +463,38 @@ Future mapping: | Current responsibility | Target component | |---|---| -| selecting a single `Platform` | split into ShimPlatform selection and BrokerPlatform selection | -| global platform accessor for shim-side code | ShimPlatform accessor | +| selecting a single `Platform` | split into UserPlatform selection always, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | +| global platform accessor for shim-side code | UserPlatform accessor | | trusted backend selection | BrokerPlatform accessor inside the broker | +| policy module/profile selection | PolicyEngine configuration inside the broker | This split is needed because the current platform traits mix local execution mechanics with trusted authority. +### Other shims, platforms, and runners + +The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-crate migration plan. Other existing crates follow the same destination split: + +| Current component | Target shape | +|---|---| +| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserCore-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | +| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserPlatform implementations; native host calls are limited to local non-authoritative mechanics and broker transport. | +| `litebox_platform_linux_kernel` | broker-kernel pieces split like LVBS: privileged backend operations become BrokerPlatform, while any user-mode support/trap transport becomes BrokerHost. | +| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserPlatform, create Shim/UserCore, authenticate to the broker, and negotiate deployment profile compatibility. | +| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; split external entry/transport support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | + ## Initial implementation direction The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority substrate: -1. Define the component split: `Shim`, `LocalCore`, `ShimPlatform`, `BrokerClient`, `BrokerCore`, optional `BrokerServices`, and `BrokerPlatform`. -2. Define the shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, and deployment profile format. +1. Define the component split: `Shim`, `UserCore`, `UserPlatform`, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. +2. Define the BrokerClient adapter, shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, transport authentication, policy profile/version format, UserPlatform/BrokerHost ABI versions, and deployment profile format. 3. Define broker-owned identity for workloads, processes, sessions, and threads. 4. Define broker-owned capability/resource IDs with generation checks. -5. Make LocalCore handle tables broker-backed views. -6. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, and BrokerPlatform features. -7. Add one simple broker-owned shared resource, such as an event object or pipe-like queue. +5. Make UserCore handle tables broker-backed views. +6. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, UserPlatform, and BrokerHost features. +7. Add a minimal PolicyEngine that can authorize/deny one simple broker-owned shared resource, such as an event object or pipe-like queue. 8. Add a broker wait/wakeup channel. -9. Prototype broker-owned network resources with firewall enforcement. +9. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. 10. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. 11. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. From b45215f98684359c4b844483ea99a9223fc991c9 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 27 May 2026 20:35:34 -0700 Subject: [PATCH 03/66] Document broker split design Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 418 +++++++++++++++++++++++++++++++----------- 1 file changed, 314 insertions(+), 104 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index 50bc79b57..c7defecab 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -2,18 +2,18 @@ ## Goal -Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel platforms. +Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel-backed deployments. -This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, or another guest ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. +This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, a Windows-like ABI, or another guest ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. The design separates LiteBox into: ```text Always user mode: - Shim + UserCore + UserPlatform + Shim + UserLiteBox optional shim-specific user clients -UserPlatform support: +Host support for UserLiteBox: hosted userland: existing host OS/user ABI broker kernel: BrokerHost @@ -23,10 +23,10 @@ Authority domain: The authority domain differs by deployment: -| Deployment | Broker location | UserPlatform support | +| Deployment | Broker location | UserLiteBox host support | |---|---|---| -| **Userland platform** | privileged broker process | existing host OS/user ABI | -| **Kernel platform** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry transport without decoding broker requests | +| **Userland broker** | privileged broker process | existing host OS/user ABI | +| **Kernel broker** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry transport without decoding broker requests | ## Component model @@ -38,17 +38,15 @@ User mode: Shim | v - UserCore + optional shim-specific user clients - -- via BrokerClient adapter over UserPlatform transport --> + UserLiteBox + optional shim-specific user clients + -- via BrokerClient adapter over UserLiteBox transport --> broker authority interface - UserPlatform implements the traits required by UserCore and provides the transport used by the BrokerClient adapter. - Hosted userland: - UserPlatform <-> host OS/user ABI + UserLiteBox <-> host OS/user ABI Broker-kernel deployment: - UserPlatform <-> BrokerHost + UserLiteBox <-> BrokerHost Authority domain: broker authority interface -> BrokerCore + optional BrokerServices @@ -62,33 +60,37 @@ Authority domain: Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap handling, argument decoding, return-value conventions, exception delivery, frame construction, and guest-visible ABI details. -### UserCore - -Runs in user mode. Provides ergonomic support to the shim, local caching, fast-path helpers, guest memory marshalling, and per-workload views of state. +### UserLiteBox -UserCore is **not trusted** for security. +Runs in user mode. It is the combined user-mode LiteBox component that replaces the earlier top-level split between user-core logic and user-platform mechanics. -### UserPlatform +UserLiteBox contains: -Runs in user mode. Implements the platform traits required by UserCore. Provides deployment-specific local execution mechanics: guest pointer representation, local TLS, local trampoline/entry support, local synchronization, local memory allocation support, logging/debug plumbing, and the low-level transport used by `BrokerClient`. +- user-facing core APIs used by shims; +- guest pointer and guest memory marshalling helpers; +- local caches and non-authoritative views of broker state; +- private synchronization and wait helpers; +- broker-owned control/event/data channel wrappers; +- a thin BrokerClient adapter; +- an internal user-platform layer that talks to host support. -UserPlatform is **not trusted** for security; it executes in the same user-mode context as the guest and UserCore. +UserLiteBox is **not trusted** for security. It executes in the same user-mode context as the guest and shim. It may request operations and cache derived state, but it must never create authority. -UserPlatform is not intended to be universal. A hosted userland UserPlatform may use native host syscalls for private, non-authoritative mechanics. A broker-kernel deployment either must provide the user ABI that this UserPlatform expects or must select a different UserPlatform. +The old distinction between user core and user platform can remain as an internal implementation structure if it is useful for code organization. It is not a security boundary and does not need to be reflected as a top-level architectural component. -UserPlatform knows the local execution ABI and the broker transport/profile. It talks only to the host support layer, which is the host OS/user ABI in hosted userland or BrokerHost in broker-kernel deployments. It does **not** call BrokerCore, BrokerServices, or BrokerPlatform directly, and it does not interpret broker-authority payloads. +Because UserLiteBox always runs in user mode, it can use Rust `std` heavily in deployments that provide a normal user-mode runtime: allocation, collections, threads, TLS, synchronization, IPC clients, async runtimes, richer errors, and broker-channel abstractions. ### BrokerClient adapter -Thin in-process adapter used by UserCore and shim-specific user clients to call the broker authority interface. +Thin in-process adapter used by UserLiteBox and shim-specific user clients to call the broker authority interface. -BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the transport supplied by UserPlatform. +BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the transport supplied by UserLiteBox's host-support layer. ### BrokerHost -Kernel-side host support for UserPlatform in broker-kernel deployments. +Kernel-side host support for UserLiteBox in broker-kernel deployments. -BrokerHost provides the user-mode execution substrate that UserPlatform expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker transport endpoints, and possibly a compatibility ABI subset. +BrokerHost provides the user-mode execution substrate that UserLiteBox expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker transport endpoints, and possibly a compatibility ABI subset. BrokerHost is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is the host-side component that lets user-mode LiteBox processes run and reach the broker. It may be trusted kernel code, but it should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. @@ -106,7 +108,7 @@ BrokerCore should not bake in every shim's ABI semantics. It provides the author Optional trusted extensions hosted inside the broker authority domain. -A BrokerService is useful when a shim or domain has security-relevant semantics that are too specific to belong in BrokerCore. Examples include OP-TEE TA/session authority, secure-storage semantics, PTA access control, or another guest ABI's domain-specific resource model. +A BrokerService is useful when a shim or domain has security-relevant semantics that are too specific to belong in BrokerCore. Examples include OP-TEE TA/session authority, secure-storage semantics, POSIX process/signal semantics, filesystem semantics, socket semantics, or another guest ABI's domain-specific resource model. BrokerServices should not reinvent authority. They should use BrokerCore capabilities, identities, memory grants, wait queues, lifecycle state, and accounting wherever possible. A BrokerService must not grant or exercise authority over a resource without going through BrokerCore's capability/lifecycle primitives and PolicyEngine authorization, even when its own protocol is shim-specific. @@ -124,21 +126,21 @@ Trusted backend for privileged operations: address-space control, host I/O, file ## Three interfaces -There are three logical interfaces. In a broker-kernel deployment, the UserPlatform/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding, validation, dispatch, and authorization remain broker-authority responsibilities. +There are three logical interfaces. In a broker-kernel deployment, the UserLiteBox/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding, validation, dispatch, and authorization remain broker-authority responsibilities. | Interface | Userland deployment | Kernel deployment | Purpose | |---|---|---|---| -| **Shim <-> UserCore** | same address space | same address space | ergonomic user-mode ABI implementation | -| **UserPlatform <-> host support layer** | host OS/user ABI | BrokerHost ABI | local non-authoritative mechanics | -| **UserCore / shim-specific user client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | +| **Shim <-> UserLiteBox** | same address space | same address space | ergonomic user-mode guest ABI implementation | +| **UserLiteBox <-> host support layer** | host OS/user ABI | BrokerHost ABI | local non-authoritative mechanics | +| **UserLiteBox / shim-specific user client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | The interfaces have different stability and trust requirements: | Interface | Shape | Authority | |---|---|---| -| `Shim <-> UserCore` | ergonomic in-process API | no authority; user-mode compatibility layer | -| `UserPlatform <-> host support layer` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | -| `UserCore / shim-specific user client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | +| `Shim <-> UserLiteBox` | ergonomic in-process API | no authority; user-mode compatibility layer | +| `UserLiteBox <-> host support layer` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | +| `UserLiteBox / shim-specific user client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | The broker authority interface should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService: @@ -157,13 +159,11 @@ PolicyEngine is not a user-callable broker target. BrokerCore and BrokerServices The negotiated policy profile is bound to the authenticated broker session or deployment profile, not chosen by each request. It only needs an explicit request field if a future design supports multiple simultaneous policy profiles on one authenticated transport. -The broker authority interface must be explicit, stable, handle-based, versioned, and switch-friendly. The UserPlatform/host-support interface is also versioned, but it is selected per deployment and only supports local mechanics. The Shim/UserCore interface can remain ergonomic because it always stays in user mode. - External broker authority APIs: | API | Shape | |---|---| -| `UserCore -> BrokerCore` | generic capability/resource protocol | +| `UserLiteBox -> BrokerCore` | generic capability/resource protocol | | `shim-specific user client -> BrokerService` | shim/domain-specific protocol | Internal broker-authority APIs: @@ -176,20 +176,110 @@ Internal broker-authority APIs: The "shim-specific user client" is not a new authority layer. It is the user-mode, typed client-side half of an optional BrokerService. -## UserPlatform and host calls +## Broker protocol and channels + +The broker protocol follows the stricter durable-unicorn shape: a custom, versioned, ABI-neutral object-operation protocol, not a host syscall proxy. + +Each sandboxed process has exactly one authenticated broker association. That association may contain multiple logical traffic classes: + +| Channel | Direction | Purpose | +|---|---|---| +| control | bidirectional | handshake, object operations, operation responses | +| event | broker to process | lifecycle, readiness, interrupt-like events, broker/session failure | +| data | bidirectional | bulk payload bytes associated with authorized object operations | + +The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. UserLiteBox does not prove identity by filling in request fields. + +The protocol exposes broker-owned objects through opaque, typed object identifiers. Shims may map those identifiers to guest-visible integers, but the broker remains authoritative for object type, lifetime, generation, rights, and policy. Object rights are broker-internal; UserLiteBox cannot amplify authority by editing request fields. + +The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. UserLiteBox receives only broker object identifiers, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. + +Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. The control channel authorizes an operation and binds it to an object and request identifier; the data channel carries bytes for that authorized operation. Shared memory is an optimization, not an authority transfer, and all shared-memory contents remain untrusted. + +## Rust `std` and runtime strategy + +The new split should not force one Rust runtime model everywhere. + +| Component | Recommended baseline | +|---|---| +| `UserLiteBox` | `std`, because it always runs in user mode | +| userland broker | `std` | +| broker-kernel deployment | start with `no_std + alloc + BrokerHost/BrokerPlatform traits`; consider a custom `std` target only if the host grows rich enough | +| shared protocol/types | `no_std + alloc` where feasible, so they can cross user/kernel and userland/kernel-broker deployments | + +Using `std` in UserLiteBox is a simplification, not a security decision. UserLiteBox is still untrusted. `std` merely makes the user-mode implementation easier: normal collections, `std::sync`, broker object wrappers, IPC clients, threads, and async/data-channel libraries can be used when the deployment supports them. + +Strict host-syscall profiles constrain how much of `std` can be used after lockdown. Any `std` functionality that may issue disallowed host syscalls must either run only during bootstrap, be avoided in strict mode, or be implemented on top of the approved broker/host-support ABI. -UserPlatform may use local host/kernel calls, but only for local mechanics that do not create LiteBox authority. +For a broker kernel or trusted host, a custom Rust `std` target may eventually be useful if BrokerHost can provide enough primitives: allocator, blocking/scheduling, synchronization, time, I/O, panic policy, and TLS. It should not be the first requirement. A staged design is safer: -| UserPlatform operation | Allowed? | Requirement | +1. Define a small BrokerHost/BrokerPlatform trait surface. +2. Implement it for a `std` userland broker. +3. Implement it for LVBS/SNP/kernel-backed brokers with `no_std + alloc`. +4. Only introduce a custom `std` target if the trait surface naturally becomes "basically std." + +## UserLiteBox and host calls + +UserLiteBox may use local host/kernel calls, but only for local mechanics that do not create LiteBox authority. + +| UserLiteBox operation | Allowed? | Requirement | |---|---|---| | private memory allocation, TLS, logging, local scratch mappings | yes | must not grant guest-visible authority | | private locks/futex-like synchronization | yes | must not represent broker-owned shared state | | broker transport notification | yes | broker validates every request | -| direct host file, network, or device access for guest-visible resources | no, unless broker-granted | must be mediated by PolicyEngine-authorized broker policy | -| guest-visible mappings or executable/shared memory | only with broker grant | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform applies | +| broker-owned shared-ring data movement | yes | ring ownership, cursor movement, and frames are validated by the broker | +| direct host file, network, or device access for guest-visible resources | no | must be mediated by broker-owned objects and PolicyEngine-authorized broker policy | +| guest-visible mappings or executable/shared memory | only through broker/UserLiteBox mediation | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform/BrokerHost applies | | trusted randomness, secrets, or security-sensitive time | no | must come from broker authority | -If a UserPlatform uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through BrokerHost or the runner must select a different UserPlatform. The stable portability target is the shim/UserCore/broker contract, not a single universal UserPlatform binary. +If UserLiteBox uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through BrokerHost or the runner must select a different UserLiteBox build/profile. The stable portability target is the shim/UserLiteBox/broker contract, not a single universal UserLiteBox binary. + +### Host syscall profiles + +The strict design still needs a small host-kernel interface for local mechanics, but the interface must be explicitly profiled and locked down. + +| Phase/profile | Allowed host-kernel access | +|---|---| +| bootstrap | setup-only calls such as mapping broker-created shared memory, installing signal handlers, setting TLS, creating local scratch mappings, and preparing syscall-capture/trampoline state | +| fast local mode | a small allowlist for performance, such as futex waits/wakes on private locks or broker-ring cursors | +| strict mode | post-lockdown calls only; on Linux this can target `SECCOMP_MODE_STRICT`-like behavior where only `read`, `write`, `_exit`, and `sigreturn` remain available | + +Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, `fcntl`, and similar authority-bearing or mapping-changing calls must not reach the host unrestricted after lockdown. Guest-visible mapping operations must enter the shim/UserLiteBox path and then either be emulated from pre-reserved local memory or mediated by the broker. + +If unrestricted `mmap`/`mprotect` remain available to the sandbox, broker mapping policy is bypassable. The design must either block those syscalls after bootstrap, constrain them to anonymous private local mechanics with a host-enforced profile, or route them through UserLiteBox/BrokerHost mediation. + +### Linux userland bootstrap profile + +The durable-unicorn Linux experiment provides a concrete hosted-userland profile: + +- the broker creates one anonymous `memfd` per spawned runner; +- the `memfd` is inherited by the runner and identified by an environment/argument convention; +- the `memfd` contains shared metadata and broker-created rings; +- the broker binds the mapped ring set to the host-authenticated spawned runner identity; +- the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. + +The initial Linux ring set can use five unidirectional rings: + +| Ring | Direction | Purpose | +|---|---|---| +| control | broker to runner | broker responses, setup, control messages | +| control | runner to broker | broker requests and responses | +| event | broker to runner | asynchronous events and fail-closed notifications | +| data | broker to runner | bulk response/event payload bytes | +| data | runner to broker | bulk request payload bytes | + +Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a UserLiteBox scheduler, but keeping the MPSC layout preserves one transport format. + +Shared-memory rings are not trusted. The broker validates header magic/version, ring offsets/capacities, producer/consumer roles, cursor movement, frame bounds, and frame contents before acting. Impossible cursor movement, malformed frames, or writes inconsistent with ring ownership are protocol failures. + +Linux can expose two protection modes: + +| Mode | Shape | +|---|---| +| fast-futex mode | allows a small syscall allowlist, including futex wait/wake on ring cursors or private locks | +| strict-seccomp mode | installs mappings, fds, signal handlers, and trampoline state before lockdown; after lockdown, only a strict syscall set remains available | + +In strict-seccomp mode, guest host-thread parallelism may need to become a shim/UserLiteBox scheduling illusion rather than real host threads. This is a compatibility/performance tradeoff, not a broker policy bypass. ## Deployment contract and negotiation @@ -203,26 +293,29 @@ Shared spec crates should define: - BrokerService IDs, protocol versions, request/response types, and feature requirements; - PolicyEngine policy versions, policy profile IDs, and audit requirements; - broker capability names and profiles; -- UserPlatform/BrokerHost ABI names and versions; -- deployment profiles that bind a shim, UserPlatform, broker transport, required services, and required broker features. +- UserLiteBox/BrokerHost ABI names and versions; +- control/event/data channel formats; +- shared-memory/ring layout versions and validation rules; +- host syscall profiles for bootstrap, fast local mode, and strict mode; +- deployment profiles that bind a shim, UserLiteBox profile, broker transport, required services, and required broker features. Startup should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. -2. The runner selects a UserPlatform that matches the deployment's host ABI. +2. The runner selects a UserLiteBox profile that matches the deployment's host ABI. 3. The user side establishes an authenticated broker transport. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. 4. The broker binds the caller identity used in `BrokerRequest` to the authenticated peer. User mode does not choose its own authority identity. -5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserPlatform, and BrokerHost versions. +5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserLiteBox, BrokerHost, channel/ring, and host-syscall-profile versions. 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. -UserPlatform should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. +UserLiteBox should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. ## Security invariant The core security rule is: -> User-mode Shim, UserCore, and UserPlatform may request operations and cache derived state, but they must never create authority. +> User-mode Shim and UserLiteBox may request operations and cache derived state, but they must never create authority. Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be authoritative for: @@ -238,40 +331,139 @@ Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be a - network and application-level firewall policy; - shim/domain-specific trusted semantics when a BrokerService is present. -A compromised user-mode shim/core should not be able to escape the broker-granted authority. +A compromised user-mode shim/UserLiteBox should not be able to escape the broker-granted authority. All authority-changing or host-effecting operations must be authorized by PolicyEngine before BrokerCore state mutation, BrokerService authority grants, or BrokerPlatform backend execution. BrokerCore remains responsible for structural capability/lifecycle validity; BrokerServices provide domain-specific context; PolicyEngine makes the policy decision. -Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. UserCore and UserPlatform must be assumed hostile for the duration of an in-flight request. +Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. UserLiteBox must be assumed hostile for the duration of an in-flight request. ## State ownership | State | Owner | |---|---| | guest ABI decoding state | Shim | -| per-workload cache/view | UserCore | +| per-workload cache/view | UserLiteBox | | shim-specific user client state | optional shim-specific user client | -| guest memory marshalling | UserCore + broker revalidation | -| private user-platform mechanics | UserPlatform via host support layer | -| private synchronization fast paths | UserCore | +| guest memory marshalling | UserLiteBox + broker revalidation | +| private user-mode mechanics | UserLiteBox via host support layer | +| private synchronization fast paths | UserLiteBox | | shared synchronization | BrokerCore | -| guest-visible handle numbers | UserCore view, BrokerCore authority | +| guest-visible handle numbers | UserLiteBox view, BrokerCore authority | | open/shared resource descriptions | BrokerCore | | shim/domain-specific authoritative state | optional BrokerService backed by BrokerCore | | policy decisions, constraints, and audit records | PolicyEngine | | IPC/event/queue/socket-like resources | BrokerCore-owned resources | | guest-visible/security-sensitive address-space mappings | BrokerCore + PolicyEngine + BrokerPlatform | -| user-platform-only scratch mappings | UserPlatform via host support layer | +| user-only scratch mappings | UserLiteBox via host support layer | | host-visible I/O | BrokerPlatform executes PolicyEngine-authorized policy | | process/session/workload lifecycle | BrokerCore | -UserCore-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. +UserLiteBox-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. + +## Process and session model + +The stricter baseline uses one broker per sandbox session and one sandboxed host process per guest process. + +| Concept | Owner | +|---|---| +| sandbox session | broker | +| guest process identity | BrokerCore | +| authenticated host process association | broker transport / BrokerHost or host OS | +| guest-visible process semantics | shim + UserLiteBox, backed by BrokerCore identity | +| process creation | broker-mediated | +| `exec`-like ABI behavior | shim/UserLiteBox within an existing broker association unless policy requires otherwise | + +All guest processes in one sandbox session share one broker. The broker assigns guest process identity, creates or authorizes the private channel set for each process, and binds the channel set to the host-authenticated peer identity. A guest process cannot claim another process's identity by choosing request fields. + +POSIX-like `fork` is broker-mediated at the identity/channel/resource level, while ABI-specific memory and descriptor inheritance semantics remain shim/UserLiteBox work. POSIX-like `exec` is preferably a shim/UserLiteBox replacement of guest memory and ABI state inside the existing sandboxed host process, so BrokerCore does not become ABI-specific. + +## UserLiteBox vs BrokerCore in current `litebox` + +The current `litebox` crate should not be split by whole module. Most modules mix ergonomic user-facing logic with authority-bearing state. The useful split is by responsibility. + +| Current area | Keep in UserLiteBox | Move to BrokerCore / broker side | +|---|---|---| +| `LiteBox` object | user-mode facade, std-backed helpers, broker connection/session object | broker session/workload identity | +| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, generations, rights, dup/pass/close/refcounts | +| `fd::RawDescriptorStorage` | raw-int fd conversion for shim ABI | validation that a handle is live and authorized | +| fd metadata | local ABI metadata and cached hints | shared open-description metadata and metadata inherited/duplicated/passed across processes | +| `path.rs` | string/CStr conversion, cheap normalization helpers | authoritative path lookup, namespace traversal, permission checks | +| `fs::*` | user-facing file API stubs, buffer marshalling, broker data-channel wrappers | filesystem namespace, inode/node identity, cwd/root, permissions, open file descriptions | +| `pipes.rs` | typed pipe fd facade and read/write marshalling | pipe object, ring state, endpoint lifetime, readiness/wakeup | +| `event::wait` | per-thread wait context, blocking current thread, local timeout conversion | shared wait queues, readiness state, cross-process wake routing | +| `event::polling` | readiness cache and ergonomic polling facade | authoritative readiness generations for shared objects | +| `sync::futex` | private futex fast path | shared futex table keyed by shared memory object/address | +| `net::*` | socket API facade and send/recv buffer marshalling | socket objects, local ports, listen backlog, connection state, firewall-visible flow state | +| `mm::PageManager` | loader helpers, guest pointer handling, cached VMA view | authoritative mappings, permissions, memory grants, shared mappings, page-fault decisions | +| `tls.rs` | shim/user-local TLS | none | +| `utils::id_pool` and similar utilities | reusable helper where local-only | broker-owned ID allocation when IDs carry authority | +| current `platform` traits | internal UserLiteBox host-support layer where local-only | BrokerHost/BrokerPlatform traits where trusted-domain or backend effects are required | + +Concrete examples: + +- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, rights, generation, refcount, passing, duplication, close, and revoke semantics. +- `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. UserLiteBox should only marshal paths/buffers and use broker-owned data channels or rings for payloads. +- `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. UserLiteBox should keep the socket facade and use broker-owned rings for data movement. +- `mm::PageManager` currently owns VMA state and calls platform page-management operations. UserLiteBox can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. + +## Control path and data path separation + +The broker should be on the control path for authority, but it does not need to be on every byte of every data path. + +```text +Control path: + UserLiteBox -> BrokerCore/BrokerService -> PolicyEngine -> BrokerPlatform + +Data path: + UserLiteBox moves bytes through broker-owned data channels or rings +``` + +The broker still owns setup, rights, revocation model, object identity, and audit boundaries. Once it creates a constrained data channel or shared ring, UserLiteBox can move bytes without a control RPC per byte, but the broker still owns the object and validates frames/cursors before acting. + +Good candidates: + +| Surface | Local data path candidate | Broker-controlled setup | +|---|---|---| +| regular file read/write | broker data channel or broker-owned shared file cache | open/path resolution/permissions/flags | +| read-only file/executable pages | broker-approved mapping/static backing | file open + mapping permission | +| pipe/queue bulk bytes | shared memory ring per pipe endpoint | create pipe, endpoint rights, readiness/wakeup | +| local IPC/domain sockets | shared rings between endpoints | connect/bind/permission/routing | +| network sockets | broker-owned shared TX/RX rings when policy allows | socket/create/connect/bind/listen/firewall | +| 9P filesystem | shared-memory request/data rings | attach/walk/open/auth/policy | +| private futexes | entirely local for private memory | broker only for shared futexes | +| event/poll readiness | local cache for readiness bits | authoritative wait queue/wakeup generation | +| guest memory copy | local copy-in/copy-out | broker validates grants for broker-visible ops | + +Rule: + +> Data path may be local only through broker-owned channels or rings whose use cannot exceed the approved broker object rights. + +If immediate revocation, full audit, or byte-level policy is required, the data path stays broker-mediated. + +## No host-handle delegation in the baseline + +The durable-unicorn experiment chose the stricter rule that the broker never passes host file descriptors, HANDLEs, sockets, directory handles, or similar host-native objects to untrusted code. This design adopts that rule as the baseline. + +Host resources stay broker-owned: + +```text +UserLiteBox asks broker: open(path, flags) +BrokerCore resolves object/capability +PolicyEngine authorizes path/flags/caller +BrokerPlatform opens host object and stores host handle privately +Broker returns broker object id, not host fd/HANDLE +UserLiteBox uses control/data channels for operations +``` + +This avoids moving enforcement into UserLiteBox or the runner, avoids cross-OS handle-rights mismatches, and makes revocation/audit simpler. The cost is higher broker involvement on data operations. Performance should first be recovered through broker-owned data rings, batching, and object-specific data channels rather than raw host-handle delegation. + +Reintroducing host-handle delegation would require a separate future design note with object-specific proof obligations. It is not assumed by this architecture. ## Network and firewall enforcement The design can support a network or application-level firewall, but only if the broker is the authoritative network path. -In that configuration, UserCore and UserPlatform must not send or receive guest-visible traffic directly through a platform network device. They may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. +In that configuration, UserLiteBox must not send or receive guest-visible traffic directly through a platform network device. It may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. Possible datapath shapes: @@ -296,12 +488,12 @@ Use: - local guest ABI decoding; - local cache for non-authoritative handle/process/session views; -- direct UserPlatform fast paths for private state; +- direct UserLiteBox fast paths for private state; - batched broker calls where possible; -- shared-memory rings for bulk IPC and network data; +- shared-memory rings for bulk IPC, pipe, queue, and network data; - broker-mediated setup with local data-plane access where security allows; - cached PolicyEngine decisions when the cache key includes all security-relevant context and supports revocation; -- explicit invalidation/revocation for stale UserCore caches. +- explicit invalidation/revocation for stale UserLiteBox caches. The broker path is required for authority changes, cross-workload operations, shared resources, host-visible effects, and firewall-enforced traffic. @@ -313,7 +505,7 @@ This split keeps the trusted computing base smaller: ```text User mode: - compatibility, marshalling, caching, fast paths + compatibility, marshalling, caching, broker-owned local data channels Authority domain: validation, capabilities, shared state, policy enforcement, optional domain authority, host effects @@ -323,29 +515,40 @@ Authority domain: | Risk | Mitigation | |---|---| -| UserCore cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | +| UserLiteBox cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | | user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | -| ABI becomes too chatty | batching, shared memory data planes, local private fast paths | -| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in UserCore | +| ABI becomes too chatty | batching, shared memory data planes, control/event/data channel split, local private fast paths | +| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in UserLiteBox | | handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | +| broker bottleneck from no host-handle delegation | use broker-owned rings, batching, object-specific data channels, and policy caching | | address-space lifecycle complexity | broker-authoritative mappings, shared object IDs, careful copy-on-write/shared-memory design | | firewall datapath bottleneck | shared rings, batching, quotas, policy caching, and broker-side flow control | | encrypted traffic hides application data | enforce metadata/connection policy unless using broker proxying or broker-managed keys/endpoints | | BrokerServices become a second monolithic core | make them optional, small, versioned, and backed by BrokerCore primitives | -| UserPlatform depends on unavailable host ABI | select UserPlatform by deployment profile, or provide the needed ABI through BrokerHost | -| UserPlatform depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | +| UserLiteBox depends on unavailable host ABI | select UserLiteBox profile by deployment, or provide the needed ABI through BrokerHost | +| UserLiteBox depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | | shared-memory TOCTOU or double-fetch bugs | validate broker requests against private snapshots or revalidate every use of user-controlled fields | | unauthenticated broker transport | authenticate peers before negotiation and bind `caller_identity` to the authenticated transport endpoint | | PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | +| custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the host support surface justifies it | ## Prior-art positioning -LiteBox's proposed design combines ideas from several systems: +LiteBox's proposed design combines ideas from several systems rather than copying any one of them. -- **Drawbridge / Graphene / Gramine**: user-mode library OS compatibility. -- **gVisor**: guest ABI mediation outside the host kernel fast path. -- **Exokernel / seL4-like systems**: small trusted authority boundary and explicit capabilities. -- **Userland broker systems**: privileged service owns shared state and host effects. +| System | Relevant idea | LiteBox lesson | +|---|---|---| +| **Drawbridge** | application + LibOS in a picoprocess over a narrow Host ABI | keep UserLiteBox user-mode and keep the broker ABI narrow | +| **Haven** | Drawbridge-style LibOS inside shielded execution | a narrow host interface composes with stronger isolation domains, but LiteBox's broker is trusted TCB rather than untrusted host | +| **Graphene / Gramine** | LibOS plus PAL/host ABI, including SGX deployments | separate compatibility logic from host adaptation; avoid baking one host ABI into core | +| **gVisor** | userspace kernel plus brokered filesystem/gofer model | rich domains like filesystems and sockets need real broker services, not just generic RPC | +| **Chromium sandbox** | sandboxed target, broker/browser process, Mojo IPC, delegated handles | useful contrast: delegated handles can work, but LiteBox's stricter baseline keeps host handles broker-owned | +| **Capsicum** | capability mode, broker opens resources and passes restricted fds | useful contrast: capability fd passing is powerful, but requires OS support for precise rights | +| **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | UserLiteBox should not be a generic syscall escape hatch | +| **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | +| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore handles should carry object identity, rights, and generation checks | +| **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | +| **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | The distinctive LiteBox claim is one architecture that supports both: @@ -357,7 +560,7 @@ kernel broker while keeping: ```text -Shim + UserCore + UserPlatform +Shim + UserLiteBox ``` always in user mode. @@ -377,31 +580,34 @@ Future mapping: | Current responsibility | Target component | |---|---| | OP-TEE entry/request decoding, return conventions, TA ABI details | Shim | -| TA-local syscall helpers, guest buffer marshalling, local loader helpers | UserCore, with broker revalidation for broker-visible data | -| non-authoritative TA/session/object caches | UserCore | +| TA-local syscall helpers, guest buffer marshalling, local loader helpers | UserLiteBox, with broker revalidation for broker-visible data | +| non-authoritative TA/session/object caches | UserLiteBox | | authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access-control context | OP-TEE BrokerService backed by BrokerCore | | OP-TEE policy decisions and audit, including PTA and secure-storage authorization | PolicyEngine | | generic identities, capabilities, memory grants, lifecycle, wait/notify, accounting | BrokerCore | | trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform after PolicyEngine authorization | -This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable UserCore and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. +This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable UserLiteBox and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. ### `litebox` -Today, `litebox` is an in-process core library. Its current `LiteBox` object and subsystems assume Rust references, generic platform traits, in-process locks, and in-process descriptor/resource identity. +Today, `litebox` is an in-process no_std core library. Its current `LiteBox` object and subsystems assume Rust references, generic platform traits, in-process locks, and in-process descriptor/resource identity. Future mapping: | Current responsibility | Target component | |---|---| -| ergonomic in-process helpers used by shims | UserCore | -| guest-visible handle table view | UserCore backed by BrokerCore | +| ergonomic in-process helpers used by shims | UserLiteBox | +| guest-visible handle table view | UserLiteBox backed by BrokerCore | +| private sync, TLS, path conversion, guest marshalling | UserLiteBox | +| broker-owned data-channel wrappers | UserLiteBox | | shared resource identity/lifetime | BrokerCore | | synchronization/wait/readiness authority for shared objects | BrokerCore | -| platform trait surface | split into UserPlatform, BrokerHost, and BrokerPlatform surfaces | +| final policy decision/audit | PolicyEngine | +| platform trait surface | internal UserLiteBox host-support layer, BrokerHost, and BrokerPlatform surfaces | | shim/domain-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | -The important change is that the current core API should not become the cross-boundary ABI. UserCore can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. +The important change is that the current core API should not become the cross-boundary ABI. UserLiteBox can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. ### `litebox_platform_lvbs` @@ -416,9 +622,9 @@ Future mapping: | broker request decode, validation, and dispatch | broker entry layer in BrokerCore | | normal-world memory mapping and validation | BrokerPlatform | | host I/O, network backend hooks, root key/secrets | BrokerPlatform executing PolicyEngine-authorized operations | -| user-mode shim platform helpers | new/extracted UserPlatform surface, not the current crate wholesale | +| user-mode shim/platform helpers | UserLiteBox internals, not the current crate wholesale | -In the new model, LVBS contributes both BrokerPlatform and BrokerHost pieces. BrokerPlatform owns privileged backend authority. BrokerHost exposes the host ABI that lets a user-mode UserPlatform execute and reach the broker. The LVBS-targeted UserPlatform selected by a deployment profile, such as `optee-on-lvbs`, should be a smaller platform layer than the current LVBS crate. +In the new model, LVBS contributes both BrokerPlatform and BrokerHost pieces. BrokerPlatform owns privileged backend authority. BrokerHost exposes the host ABI that lets a user-mode UserLiteBox execute and reach the broker. The LVBS-targeted UserLiteBox selected by a deployment profile, such as `optee-on-lvbs`, should be smaller than the current LVBS crate. ### `litebox_runner_lvbs` @@ -432,9 +638,9 @@ Future mapping: | VTL trap/transport dispatch | BrokerHost | | broker request decode, validation, and dispatch | broker entry layer in BrokerCore | | session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | -| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + UserCore | +| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + UserLiteBox | | authoritative TA/session/object identity currently held by the runner | BrokerCore + OP-TEE BrokerService + PolicyEngine | -| local/broker compatibility checks | deployment profile negotiation | +| user/broker compatibility checks | deployment profile negotiation | | local user ABI support | BrokerHost | The runner becomes less of an application runner and more of a broker bootstrap/entrypoint for the trusted deployment. @@ -448,8 +654,8 @@ Future mapping: | Current responsibility | Target component | |---|---| | command-line harness and binary loading for tests | runner/test harness | -| `OpteeShim` construction | Shim + UserCore process setup | -| platform selection for local execution | UserPlatform setup | +| `OpteeShim` construction | Shim + UserLiteBox process setup | +| user-mode local execution | UserLiteBox setup | | broker/service compatibility | deployment profile negotiation | | shared/security-authoritative state | separate privileged broker process | @@ -463,8 +669,8 @@ Future mapping: | Current responsibility | Target component | |---|---| -| selecting a single `Platform` | split into UserPlatform selection always, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | -| global platform accessor for shim-side code | UserPlatform accessor | +| selecting a single `Platform` | split into UserLiteBox profile selection, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | +| global platform accessor for shim-side code | UserLiteBox internal host-support accessor | | trusted backend selection | BrokerPlatform accessor inside the broker | | policy module/profile selection | PolicyEngine configuration inside the broker | @@ -476,26 +682,30 @@ The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-c | Current component | Target shape | |---|---| -| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserCore-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | -| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserPlatform implementations; native host calls are limited to local non-authoritative mechanics and broker transport. | +| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserLiteBox-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | +| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserLiteBox host-support implementations; native host calls are limited to local non-authoritative mechanics and broker transport/rings. | | `litebox_platform_linux_kernel` | broker-kernel pieces split like LVBS: privileged backend operations become BrokerPlatform, while any user-mode support/trap transport becomes BrokerHost. | -| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserPlatform, create Shim/UserCore, authenticate to the broker, and negotiate deployment profile compatibility. | +| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserLiteBox profile, create Shim/UserLiteBox, authenticate to the broker, and negotiate deployment profile compatibility. | | `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; split external entry/transport support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | ## Initial implementation direction The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority substrate: -1. Define the component split: `Shim`, `UserCore`, `UserPlatform`, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. -2. Define the BrokerClient adapter, shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, transport authentication, policy profile/version format, UserPlatform/BrokerHost ABI versions, and deployment profile format. -3. Define broker-owned identity for workloads, processes, sessions, and threads. -4. Define broker-owned capability/resource IDs with generation checks. -5. Make UserCore handle tables broker-backed views. -6. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, UserPlatform, and BrokerHost features. -7. Add a minimal PolicyEngine that can authorize/deny one simple broker-owned shared resource, such as an event object or pipe-like queue. -8. Add a broker wait/wakeup channel. -9. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. -10. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. -11. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. +1. Define the component split: `Shim`, `UserLiteBox`, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. +2. Define the BrokerClient adapter, shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, transport authentication, policy profile/version format, control/event/data channel schema, shared-memory ring layout, UserLiteBox/BrokerHost ABI versions, and deployment profile format. +3. Decide the Rust runtime strategy: `std` for UserLiteBox and userland broker; `no_std + alloc + traits` for kernel broker first; custom kernel `std` target only if later justified. +4. Define host syscall profiles for bootstrap, fast local mode, and strict mode. +5. Define broker-owned identity for workloads, processes, sessions, and threads. +6. Define broker-owned capability/resource IDs with generation checks. +7. Make UserLiteBox handle tables broker-backed views. +8. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. +9. Add a minimal PolicyEngine that can authorize/deny one simple broker-owned shared resource, such as an event object or pipe-like queue. +10. Add a broker wait/wakeup channel. +11. Prototype a broker-owned pipe or queue with shared-ring data path. +12. Prototype a broker-owned file object with mediated control/data-channel I/O, not host-handle delegation. +13. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. +14. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. +15. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. That gives a controlled path from current single-process/single-session assumptions toward true shared-state support without rewriting every shim and platform at once. From 605c1c190f176d9fc2a12e4b3ba0f6f27376a979 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 27 May 2026 20:48:56 -0700 Subject: [PATCH 04/66] Document broker implementation plan Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/impl-plan.md | 379 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 docs/impl-plan.md diff --git a/docs/impl-plan.md b/docs/impl-plan.md new file mode 100644 index 000000000..93b06703c --- /dev/null +++ b/docs/impl-plan.md @@ -0,0 +1,379 @@ +# Broker Split Implementation Plan + +## Goal + +Implement the broker split as incremental vertical slices while keeping the existing LiteBox behavior working. + +The target architecture is: + +```text +User mode: + Shim + UserLiteBox + optional shim-specific user clients + +Authority domain: + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + +Kernel-broker deployments: + BrokerHost supports user-mode UserLiteBox execution and broker transport +``` + +The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/event/data channels, authenticated per-process broker associations, and fail-closed behavior. + +## Implementation principles + +- Build vertical slices, not a big-bang refactor. +- Keep UserLiteBox untrusted and broker authority explicit. +- Keep BrokerCore shim-neutral. +- Put domain-specific authority in BrokerServices. +- Put final allow/deny/audit decisions in PolicyEngine. +- Keep BrokerPlatform as authorized backend execution, not a policy owner. +- Keep BrokerHost separate from broker request decode/authorization. +- Start in userland; move to kernel-broker deployment after broker semantics are proven. + +## Phase 0: Boundary freeze + +Define and document the core vocabulary in code and docs: + +- `UserLiteBox` +- `BrokerClient` adapter +- `BrokerCore` +- `BrokerService` +- `PolicyEngine` +- `BrokerPlatform` +- `BrokerHost` + +Exit criteria: + +- Design doc and code comments use one vocabulary. +- No new code treats UserLiteBox as trusted. +- No broker API is modeled as host syscall proxying. + +## Phase 1: Shared protocol/types crate + +Create a shared crate for broker protocol types. + +Initial contents: + +- protocol version; +- session/process/workload IDs; +- broker object IDs with type tags and generations; +- request/response envelopes; +- ABI-neutral error categories; +- control/event/data channel frame headers; +- policy profile IDs; +- host syscall profile IDs; +- feature negotiation structures. + +Prefer `no_std + alloc` for shared types so they can be reused by userland and kernel-broker deployments. + +Exit criteria: + +- Shared types compile independently. +- Wire-visible fields are explicit and versioned. +- Caller identity is not caller-chosen inside request payloads. + +## Phase 2: Userland broker skeleton + +Implement a userland broker process first. + +Initial scope: + +- one broker process per sandbox session; +- one authenticated process association; +- control channel only; +- exact protocol version negotiation; +- default-deny PolicyEngine; +- fail-closed channel/session behavior. + +Exit criteria: + +- UserLiteBox can connect and negotiate. +- Broker binds caller identity to the authenticated transport. +- Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. + +## Phase 3: UserLiteBox facade + +Introduce UserLiteBox without moving every subsystem. + +Initial scope: + +- wrap existing LiteBox ergonomics behind UserLiteBox; +- add BrokerClient adapter; +- keep existing local implementations available behind a profile/feature; +- add a broker-backed handle-table view for experimental objects. + +Exit criteria: + +- Current tests can still use the local profile. +- A broker-backed profile can issue a simple broker request. +- UserLiteBox handle entries can store broker object ID + generation + cached rights. + +## Phase 4: First broker-owned object + +Start with a small event or pipe-like object, not filesystem or networking. + +Broker owns: + +- object ID and generation; +- endpoint/reference lifetime; +- close semantics; +- readiness state; +- wait/wakeup state. + +UserLiteBox owns: + +- guest-visible handle number; +- typed facade; +- buffer marshalling; +- non-authoritative readiness cache. + +Exit criteria: + +- Create, duplicate, close, wait, and readiness work through BrokerCore. +- Stale handles and wrong-generation handles are rejected. +- Process disconnect cleans up broker-owned references. + +## Phase 5: Broker-backed fd semantics + +Move fd authority to BrokerCore while keeping guest fd numbers in UserLiteBox. + +BrokerCore owns: + +- object refs; +- rights; +- generations; +- refcounts; +- dup/pass/close; +- inherited object tables; +- process-exit cleanup. + +UserLiteBox owns: + +- guest fd number allocation; +- raw-int fd conversion; +- typed fd wrappers; +- local ABI metadata and cached hints. + +Exit criteria: + +- Double close, stale fd, dup, inherited refs, and process-exit cleanup are tested. +- UserLiteBox cannot create a live broker object by editing local fd state. + +## Phase 6: Control/event/data transport + +Add the durable-unicorn-style channel split. + +Channels: + +- control: object operations and responses; +- event: broker-to-process async events; +- data: bulk payload bytes. + +Linux hosted prototype: + +- broker creates inherited private `memfd`; +- ring header has magic/version/layout; +- broker binds ring set to authenticated runner identity; +- broker validates all ring metadata, cursors, frame bounds, and producer roles. + +Exit criteria: + +- Control channel supports concurrent request IDs. +- Event channel reports readiness/lifecycle/fail-closed events. +- Data channel can carry payloads for the first broker-owned object. +- Invalid cursor movement or malformed frames fail closed. + +## Phase 7: Host syscall profiles + +Define host syscall profiles for hosted userland. + +Profiles: + +- bootstrap profile; +- fast local profile; +- strict profile. + +Linux targets: + +- fast-futex mode: small syscall allowlist, including futex for ring/private waits; +- strict-seccomp-like mode: all mappings, fds, signal handlers, and trampoline state installed before lockdown. + +Exit criteria: + +- Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, and `fcntl` cannot bypass broker policy after lockdown. +- Guest-visible mapping operations enter shim/UserLiteBox and are emulated or broker-mediated. + +## Phase 8: Filesystem BrokerService + +Add a minimal filesystem BrokerService. + +Start with a restricted virtual or host-backed filesystem. + +Broker side owns: + +- namespace roots; +- directory objects; +- open file descriptions; +- file object IDs; +- path lookup; +- permissions; +- read/write policy. + +UserLiteBox owns: + +- path string conversion; +- buffer marshalling; +- guest fd view; +- data-channel wrappers. + +Exit criteria: + +- Filesystem operations are directory-relative. +- No host fd/HANDLE is exposed to UserLiteBox. +- File data uses mediated control/data channel or broker-owned ring. +- PolicyEngine can deny open/read/write independently. + +## Phase 9: Memory and mapping authority + +Move security-sensitive mapping authority broker-side. + +Broker side owns: + +- mapping object identity; +- memory grants; +- shared mappings; +- executable mapping policy; +- page-fault decisions where applicable. + +UserLiteBox owns: + +- loader helpers; +- guest pointer handling; +- cached VMA view; +- local anonymous/private scratch allocation allowed by host profile. + +Exit criteria: + +- Broker-visible mappings require BrokerCore validation and PolicyEngine authorization. +- Executable memory cannot be created without rewrite/validation policy. +- Shared memory grants cannot be forged by UserLiteBox. + +## Phase 10: Multiprocess + +Implement one guest process as one sandboxed host process. + +Broker owns: + +- session identity; +- guest process identity; +- per-process channel set; +- process lifecycle; +- process-exit cleanup; +- inherited broker object table. + +Shim/UserLiteBox owns: + +- ABI-specific fork semantics; +- ABI-specific exec semantics; +- guest memory replacement; +- guest fd-number presentation. + +Exit criteria: + +- Broker-mediated process creation works. +- Child process cannot impersonate parent. +- Inherited objects are explicit. +- Process exit releases broker-owned refs. + +## Phase 11: Network BrokerService + +Add broker-owned networking after filesystem and process identity are stable. + +Broker side owns: + +- socket object identity; +- port allocation; +- bind/listen/connect state; +- flow metadata; +- firewall policy context; +- TX/RX rings where enabled. + +UserLiteBox owns: + +- socket syscall facade; +- send/recv marshalling; +- local readiness cache. + +Exit criteria: + +- L3/L4 firewall policy is enforced by PolicyEngine. +- UserLiteBox cannot send or receive guest-visible network traffic directly through host network devices. +- Data path uses broker-mediated operations or broker-owned rings. + +## Phase 12: Kernel-broker deployment + +Only after userland semantics are stable, implement broker-kernel deployment. + +Split trusted deployment code into: + +- BrokerHost: user-mode execution support and transport delivery; +- BrokerPlatform: privileged backend execution; +- BrokerCore/BrokerServices/PolicyEngine: shared authority logic. + +Exit criteria: + +- BrokerHost does not decode or authorize BrokerRequest. +- BrokerCore protocol and object semantics are reused. +- Kernel-broker deployment passes the same broker-object conformance tests as userland broker. + +## Phase 13: OP-TEE BrokerService + +Add OP-TEE-specific authority only where generic BrokerCore cannot express it. + +BrokerService owns: + +- TA/session authority; +- persistent object semantics; +- PTA access-control context; +- OP-TEE-specific lifecycle semantics. + +PolicyEngine owns: + +- final OP-TEE policy decisions and audit. + +Exit criteria: + +- OP-TEE shim remains user-mode ABI code. +- Trusted deployment does not need the full OP-TEE shim. +- OP-TEE authority cannot be created in UserLiteBox. + +## Suggested first milestone + +The smallest useful milestone is: + +```text +single process +userland broker +control channel only +default-deny PolicyEngine +broker-owned event/pipe object +UserLiteBox fd table maps guest fd -> broker object id +``` + +This proves the trust boundary before taking on filesystem, networking, mapping, or multiprocess complexity. + +## Validation strategy + +Add conformance tests at each layer: + +- protocol parsing rejects malformed frames; +- policy default-denies unknown operations; +- caller identity is transport-bound; +- stale object IDs fail; +- wrong generation fails; +- process disconnect cleans up refs; +- UserLiteBox local handle edits cannot create authority; +- shared-memory cursor/frame corruption fails closed; +- broker failure forces session failure. + +Prefer tests that run against both userland broker and later kernel-broker implementations. From 4c7a391719c413062f0565d0019e23bcad55c944 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 27 May 2026 21:40:55 -0700 Subject: [PATCH 05/66] Incorporate sandbox architecture findings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/docs/broker-design.md b/docs/broker-design.md index c7defecab..2c7ad614f 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -243,11 +243,23 @@ The strict design still needs a small host-kernel interface for local mechanics, | bootstrap | setup-only calls such as mapping broker-created shared memory, installing signal handlers, setting TLS, creating local scratch mappings, and preparing syscall-capture/trampoline state | | fast local mode | a small allowlist for performance, such as futex waits/wakes on private locks or broker-ring cursors | | strict mode | post-lockdown calls only; on Linux this can target `SECCOMP_MODE_STRICT`-like behavior where only `read`, `write`, `_exit`, and `sigreturn` remain available | +| arbitrated mode | selected non-delegable syscalls are trapped/validated before execution in the user process context | Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, `fcntl`, and similar authority-bearing or mapping-changing calls must not reach the host unrestricted after lockdown. Guest-visible mapping operations must enter the shim/UserLiteBox path and then either be emulated from pre-reserved local memory or mediated by the broker. If unrestricted `mmap`/`mprotect` remain available to the sandbox, broker mapping policy is bypassable. The design must either block those syscalls after bootstrap, constrain them to anonymous private local mechanics with a host-enforced profile, or route them through UserLiteBox/BrokerHost mediation. +LITESHIELD's useful distinction is between delegable and non-delegable syscalls: + +| Class | Meaning for LiteBox | +|---|---| +| delegable | translate into BrokerCore/BrokerService operations | +| local-private | allow directly under the host syscall profile because no LiteBox authority is created | +| non-delegable/arbitrated | must execute in the process context, but only after trap/validation by UserLiteBox/BrokerHost policy machinery | +| blocked | never allowed after lockdown | + +Memory-management and process-management calls are the hard cases. If they cannot be safely delegated to the broker, the host support layer needs an arbitration mechanism that can validate address ranges, mapping types, permissions, and target objects before allowing the host syscall to complete. + ### Linux userland bootstrap profile The durable-unicorn Linux experiment provides a concrete hosted-userland profile: @@ -459,6 +471,38 @@ This avoids moving enforcement into UserLiteBox or the runner, avoids cross-OS h Reintroducing host-handle delegation would require a separate future design note with object-specific proof obligations. It is not assumed by this architecture. +## Trusted data-plane services + +The stricter baseline keeps host resources broker-owned and avoids raw host-handle delegation to UserLiteBox. SKernel suggests a future performance direction that preserves this rule: introduce trusted data-plane services inside the broker authority domain. + +In that model: + +```text +UserLiteBox: + untrusted ABI compatibility, marshalling, local caches + +BrokerCore / PolicyEngine: + object identity, rights, lifecycle, policy + +Broker data-plane service: + trusted high-performance implementation of an object family + +BrokerPlatform: + authorized host/device/backend effects +``` + +A trusted data-plane service is not UserLiteBox. It is part of the TCB, like a BrokerService or BrokerPlatform-adjacent component, and can hold backend authority that UserLiteBox must not receive. + +Potential examples: + +| Service | Inspired by | LiteBox interpretation | +|---|---|---| +| filesystem data-plane service | SKernel-D FD/image-based filesystem, EROFS/TMPFS | broker-owned filesystem cache/ring service that reduces per-operation broker IPC without exposing host fds | +| network data-plane service | SKernel-D high-performance network stack with device passthrough | trusted broker-side network service; UserLiteBox talks via rings while PolicyEngine keeps firewall authority | +| memory/resource coordination service | SKernel-R/SKernel-V resource calls | BrokerHost/BrokerPlatform resource-call path for memory, CPU, and device-resource elasticity | + +This is an optimization path, not the first milestone. The initial implementation should still start with simple broker-owned objects and mediated control/data channels. If performance demands it, move hot object-family data paths into trusted broker-side services rather than expanding untrusted UserLiteBox authority. + ## Network and firewall enforcement The design can support a network or application-level firewall, but only if the broker is the authoritative network path. @@ -531,6 +575,8 @@ Authority domain: | unauthenticated broker transport | authenticate peers before negotiation and bind `caller_identity` to the authenticated transport endpoint | | PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | | custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the host support surface justifies it | +| non-delegable syscalls bypass broker mapping/resource policy | block, constrain, or trap/arbitrate them before host execution | +| trusted data-plane services grow too powerful | keep them broker-side, object-family-specific, and PolicyEngine-authorized | ## Prior-art positioning @@ -546,6 +592,8 @@ LiteBox's proposed design combines ideas from several systems rather than copyin | **Capsicum** | capability mode, broker opens resources and passes restricted fds | useful contrast: capability fd passing is powerful, but requires OS support for precise rights | | **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | UserLiteBox should not be a generic syscall escape hatch | | **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | +| **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | +| **SKernel** | split guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | | **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore handles should carry object identity, rights, and generation checks | | **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | | **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | From 76cd13f79c05574b289e12482474b219428659c6 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 10:35:06 -0700 Subject: [PATCH 06/66] Add split broker event POC Introduce modular broker protocol, transport, wire codec, client, core, server, and Unix socket implementation for the initial broker-owned event object path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 1027 ++++++++++------- Cargo.toml | 16 +- docs/broker-design.md | 94 +- docs/impl-plan.md | 49 +- litebox_broker_client/Cargo.toml | 11 + litebox_broker_client/src/error.rs | 61 + litebox_broker_client/src/event.rs | 39 + litebox_broker_client/src/lib.rs | 147 +++ litebox_broker_client/src/negotiate.rs | 36 + litebox_broker_core/Cargo.toml | 11 + litebox_broker_core/src/connection.rs | 492 ++++++++ litebox_broker_core/src/event.rs | 147 +++ litebox_broker_core/src/identity.rs | 130 +++ litebox_broker_core/src/lib.rs | 73 ++ litebox_broker_core/src/object.rs | 316 +++++ litebox_broker_core/src/policy.rs | 142 +++ litebox_broker_core/src/types.rs | 37 + litebox_broker_protocol/Cargo.toml | 10 + litebox_broker_protocol/src/error.rs | 88 ++ litebox_broker_protocol/src/lib.rs | 45 + litebox_broker_protocol/src/message.rs | 76 ++ litebox_broker_protocol/src/object.rs | 103 ++ litebox_broker_server/Cargo.toml | 11 + litebox_broker_server/src/lib.rs | 14 + litebox_broker_server/src/server.rs | 105 ++ litebox_broker_transport/Cargo.toml | 11 + litebox_broker_transport/src/lib.rs | 92 ++ litebox_broker_unix_socket/Cargo.toml | 21 + .../src/bin/litebox-broker-userland.rs | 61 + litebox_broker_unix_socket/src/lib.rs | 200 ++++ .../tests/userland_broker.rs | 82 ++ litebox_broker_wire/Cargo.toml | 11 + litebox_broker_wire/src/lib.rs | 421 +++++++ 33 files changed, 3710 insertions(+), 469 deletions(-) create mode 100644 litebox_broker_client/Cargo.toml create mode 100644 litebox_broker_client/src/error.rs create mode 100644 litebox_broker_client/src/event.rs create mode 100644 litebox_broker_client/src/lib.rs create mode 100644 litebox_broker_client/src/negotiate.rs create mode 100644 litebox_broker_core/Cargo.toml create mode 100644 litebox_broker_core/src/connection.rs create mode 100644 litebox_broker_core/src/event.rs create mode 100644 litebox_broker_core/src/identity.rs create mode 100644 litebox_broker_core/src/lib.rs create mode 100644 litebox_broker_core/src/object.rs create mode 100644 litebox_broker_core/src/policy.rs create mode 100644 litebox_broker_core/src/types.rs create mode 100644 litebox_broker_protocol/Cargo.toml create mode 100644 litebox_broker_protocol/src/error.rs create mode 100644 litebox_broker_protocol/src/lib.rs create mode 100644 litebox_broker_protocol/src/message.rs create mode 100644 litebox_broker_protocol/src/object.rs create mode 100644 litebox_broker_server/Cargo.toml create mode 100644 litebox_broker_server/src/lib.rs create mode 100644 litebox_broker_server/src/server.rs create mode 100644 litebox_broker_transport/Cargo.toml create mode 100644 litebox_broker_transport/src/lib.rs create mode 100644 litebox_broker_unix_socket/Cargo.toml create mode 100644 litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs create mode 100644 litebox_broker_unix_socket/src/lib.rs create mode 100644 litebox_broker_unix_socket/tests/userland_broker.rs create mode 100644 litebox_broker_wire/Cargo.toml create mode 100644 litebox_broker_wire/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 286b3b24e..946f4698f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -70,44 +70,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arrayvec" @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base64" @@ -153,9 +153,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bindgen" @@ -163,10 +163,10 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -185,18 +185,18 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfield" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf79f42d21f18b5926a959280215903e659760da994835d27c3a0c5ff4f898f" +checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" dependencies = [ "bitfield-macros", ] [[package]] name = "bitfield-macros" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6115af052c7914c0cbb97195e5c72cb61c511527250074f5c041d1048b0d8b16" +checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" dependencies = [ "proc-macro2", "quote", @@ -211,9 +211,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "byteorder" @@ -275,9 +275,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.56" +version = "1.2.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" dependencies = [ "find-msvc-tools", "shlex", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -356,9 +356,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cms" @@ -386,20 +386,19 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "console" -version = "0.15.11" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" dependencies = [ "encode_unicode", "libc", - "once_cell", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -408,13 +407,20 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const_fn" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60" + [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -430,9 +436,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.9.4" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -489,9 +495,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -547,14 +553,14 @@ version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "defmt 1.0.1", + "defmt 1.1.0", ] [[package]] name = "defmt" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" +checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -562,9 +568,9 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" +checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" dependencies = [ "defmt-parser", "proc-macro-error2", @@ -673,9 +679,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -684,9 +690,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elf" @@ -702,9 +708,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "env_filter" -version = "0.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -712,9 +718,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.8" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ "anstream", "anstyle", @@ -751,9 +757,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -772,19 +778,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "filetime" -version = "0.2.27" +version = "0.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" dependencies = [ "cfg-if", "libc", - "libredox", ] [[package]] @@ -847,9 +852,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.1.3" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" dependencies = [ "autocfg", ] @@ -932,9 +937,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", @@ -949,10 +954,23 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "getset" version = "0.1.6" @@ -1004,6 +1022,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heapless" version = "0.8.0" @@ -1031,9 +1055,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1079,9 +1103,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" dependencies = [ "atomic-waker", "bytes", @@ -1092,7 +1116,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1172,12 +1195,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1185,9 +1209,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1198,9 +1222,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1212,15 +1236,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1232,15 +1256,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -1251,6 +1275,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1270,9 +1300,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -1294,15 +1324,28 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + [[package]] name = "insta" -version = "1.43.2" +version = "1.47.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" dependencies = [ "console", "once_cell", "similar", + "tempfile", ] [[package]] @@ -1323,21 +1366,11 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" -[[package]] -name = "iri-string" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -1348,17 +1381,26 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.18" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" +checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" dependencies = [ "jiff-static", "log", @@ -1369,9 +1411,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.18" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" +checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" dependencies = [ "proc-macro2", "quote", @@ -1380,28 +1422,46 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "jsonwebtoken" -version = "10.3.0" +version = "10.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64", - "getrandom 0.2.16", + "getrandom 0.2.17", "js-sys", "serde", "serde_json", "signature", + "zeroize", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", ] +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.5.0" @@ -1411,11 +1471,17 @@ dependencies = [ "spin 0.9.8", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.177" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -1429,37 +1495,25 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall", -] +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litebox" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.11.1", "buddy_system_allocator", "either", - "hashbrown", + "hashbrown 0.15.5", "litebox_util_log", "rangemap", "ringbuf", @@ -1474,12 +1528,67 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_broker_client" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", + "litebox_broker_transport", +] + +[[package]] +name = "litebox_broker_core" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", + "litebox_broker_transport", +] + +[[package]] +name = "litebox_broker_protocol" +version = "0.1.0" + +[[package]] +name = "litebox_broker_server" +version = "0.1.0" +dependencies = [ + "litebox_broker_core", + "litebox_broker_transport", +] + +[[package]] +name = "litebox_broker_transport" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + +[[package]] +name = "litebox_broker_unix_socket" +version = "0.1.0" +dependencies = [ + "litebox_broker_client", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_server", + "litebox_broker_transport", + "litebox_broker_wire", +] + +[[package]] +name = "litebox_broker_wire" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", + "litebox_broker_transport", +] + [[package]] name = "litebox_common_linux" version = "0.1.0" dependencies = [ "bitfield", - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "elf", "int-enum", @@ -1493,7 +1602,7 @@ dependencies = [ name = "litebox_common_optee" version = "0.1.0" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "elf", "litebox", "litebox_common_linux", @@ -1534,7 +1643,7 @@ version = "0.1.0" dependencies = [ "arrayvec", "bindgen", - "bitflags 2.11.0", + "bitflags 2.11.1", "litebox", "litebox_common_linux", "litebox_util_log", @@ -1571,12 +1680,12 @@ dependencies = [ "aligned-vec", "arrayvec", "authenticode", - "bitflags 2.11.0", + "bitflags 2.11.1", "cms", "const-oid", "digest", "elf", - "hashbrown", + "hashbrown 0.15.5", "libc", "litebox", "litebox_common_linux", @@ -1734,7 +1843,7 @@ name = "litebox_shim_linux" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.0", + "bitflags 2.11.1", "bitvec", "libc", "litebox", @@ -1761,7 +1870,7 @@ dependencies = [ "arrayvec", "ctr", "elf", - "hashbrown", + "hashbrown 0.15.5", "hmac", "litebox", "litebox_common_linux", @@ -1832,9 +1941,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -1847,11 +1956,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" dependencies = [ - "serde", + "serde_core", "sval", "sval_ref", "value-bag", @@ -1874,15 +1983,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" dependencies = [ "libc", ] @@ -1905,9 +2014,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi", @@ -1937,9 +2046,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -2019,9 +2128,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2029,9 +2138,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", @@ -2103,15 +2212,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "opaque-debug" @@ -2121,11 +2230,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.79" +version = "0.10.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "cfg-if", "foreign-types", "libc", @@ -2146,15 +2255,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.115" +version = "0.9.116" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" dependencies = [ "cc", "libc", @@ -2191,7 +2300,7 @@ checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", "clap", - "itertools", + "itertools 0.14.0", "proc-macro2", "quote", "rand", @@ -2199,15 +2308,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" @@ -2232,36 +2335,30 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.4" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -2309,9 +2406,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -2330,9 +2427,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -2343,6 +2440,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -2376,14 +2479,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rangemap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "raw-cpuid" @@ -2391,14 +2494,14 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", ] [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -2414,20 +2517,11 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -2437,9 +2531,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -2448,15 +2542,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2523,17 +2617,17 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys", @@ -2542,9 +2636,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "zeroize", ] @@ -2557,9 +2651,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -2572,9 +2666,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -2596,11 +2690,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "core-foundation", "core-foundation-sys", "libc", @@ -2617,6 +2711,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "seq-macro" version = "0.3.6" @@ -2664,15 +2764,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -2736,9 +2836,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "similar" @@ -2783,9 +2883,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2860,15 +2960,15 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sval" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" +checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" [[package]] name = "sval_buffer" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" +checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" dependencies = [ "sval", "sval_ref", @@ -2876,18 +2976,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" +checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" +checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" dependencies = [ "itoa", "ryu", @@ -2896,9 +2996,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" +checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" dependencies = [ "itoa", "ryu", @@ -2907,9 +3007,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" +checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" dependencies = [ "sval", "sval_buffer", @@ -2918,18 +3018,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" +checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.16.0" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" +checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" dependencies = [ "serde_core", "sval", @@ -2938,9 +3038,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -2981,9 +3081,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -2996,19 +3096,19 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac9ee8b664c9f1740cd813fea422116f8ba29997bb7c878d1940424889802897" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "log", "num-traits", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3016,18 +3116,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -3045,9 +3145,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -3055,9 +3155,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -3070,9 +3170,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -3085,9 +3185,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -3134,20 +3234,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.0", + "bitflags 2.11.1", "bytes", "futures-util", "http", "http-body", - "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -3164,9 +3264,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -3187,9 +3287,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -3208,9 +3308,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -3238,9 +3338,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicase" @@ -3250,9 +3350,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" @@ -3301,9 +3401,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -3380,18 +3480,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -3402,23 +3511,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3426,9 +3531,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -3439,13 +3544,35 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -3459,11 +3586,23 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", @@ -3537,22 +3676,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -3564,22 +3694,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -3587,34 +3701,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -3623,87 +3725,139 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.52.6" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] -name = "windows_i686_gnu" +name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" +name = "windows_i686_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] -name = "windows_i686_gnullvm" +name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] -name = "windows_i686_msvc" -version = "0.52.6" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] -name = "windows_i686_msvc" +name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] [[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" +name = "wit-bindgen-core" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "wit-bindgen-rust" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" +name = "wit-bindgen-rust-macro" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" +name = "wit-component" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] [[package]] -name = "wit-bindgen" -version = "0.46.0" +name = "wit-parser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -3727,12 +3881,13 @@ dependencies = [ [[package]] name = "x86_64" -version = "0.15.2" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae" +checksum = "f7841fa0098ceb15c567d93d3fae292c49e10a7662b4936d5f6a9728594555ba" dependencies = [ "bit_field", - "bitflags 2.11.0", + "bitflags 2.11.1", + "const_fn", "rustversion", "volatile", ] @@ -3764,9 +3919,9 @@ checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3775,9 +3930,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -3787,18 +3942,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" dependencies = [ "proc-macro2", "quote", @@ -3807,18 +3962,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -3831,12 +3986,26 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -3845,9 +4014,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -3856,11 +4025,17 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", "syn", ] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index e30b3904a..a7750211e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,14 @@ [workspace] resolver = "2" members = [ - "litebox", + "litebox", + "litebox_broker_client", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_server", + "litebox_broker_transport", + "litebox_broker_unix_socket", + "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", @@ -29,6 +36,13 @@ members = [ ] default-members = [ "litebox", + "litebox_broker_client", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_server", + "litebox_broker_transport", + "litebox_broker_unix_socket", + "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", diff --git a/docs/broker-design.md b/docs/broker-design.md index 2c7ad614f..bce9e77ff 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -124,6 +124,36 @@ PolicyEngine should not own all broker state. It consumes facts from BrokerCore Trusted backend for privileged operations: address-space control, host I/O, filesystem/device/network access, randomness/secrets, timers, scheduling hooks, host-side execution of PolicyEngine-authorized operations, and platform-specific primitives. +## Crate layout and naming + +The broker split should use crate names that make the authority boundary visible. The first proof of concept should start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. + +| Crate | Initial role | +|---|---| +| `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event object/reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | +| `litebox_broker_core` | Transport-independent authority logic: peer-credential-to-association seam, broker-owned session/process associations, object/reference registry, object type and rights authority, generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. | +| `litebox_broker_transport` | Neutral `no_std` transport-trait crate for blocking directional request/response transport contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the transport-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | +| `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream transports reuse it rather than duplicating protocol encoding. | +| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket transport crate implementing the neutral client/server transport traits for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | +| `litebox_broker_server` | Transport-neutral `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server transport. The server obtains the peer credential through `ServerTransport`, so deployment entry points do not construct credentials out-of-band, and successful termination reports whether the peer closed or BrokerCore closed with a typed reason. | +| `litebox_broker_client` | `no_std` transport-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | +| `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | + +`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected transport. + +Transport must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, or any specific IPC implementation. Shared request/response transport traits live in `litebox_broker_transport`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. + +Forward-compatible protocol probing is explicit: unknown request tags decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server loop can return `UnsupportedOperation` without closing the connection. Structurally malformed frames remain transport/wire errors. + +Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical split: + +| Future crate/layer | Role | +|---|---| +| `litebox_broker_host` | BrokerHost abstractions for user-mode execution support, trap/upcall/transport delivery, process/thread setup, and the UserLiteBox host ABI. | +| `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, BrokerServices, PolicyEngine, BrokerPlatform, and BrokerHost. | + +These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, while BrokerCore decodes, validates, dispatches, and authorizes broker requests. + ## Three interfaces There are three logical interfaces. In a broker-kernel deployment, the UserLiteBox/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding, validation, dispatch, and authorization remain broker-authority responsibilities. @@ -190,7 +220,7 @@ Each sandboxed process has exactly one authenticated broker association. That as The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. UserLiteBox does not prove identity by filling in request fields. -The protocol exposes broker-owned objects through opaque, typed object identifiers. Shims may map those identifiers to guest-visible integers, but the broker remains authoritative for object type, lifetime, generation, rights, and policy. Object rights are broker-internal; UserLiteBox cannot amplify authority by editing request fields. +The protocol exposes broker-owned objects through opaque object identifiers plus closeable broker reference identifiers and generations. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, generations, rights, and policy. Object types and rights are broker-internal for authorization; UserLiteBox cannot amplify authority by editing request fields. The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. UserLiteBox receives only broker object identifiers, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. @@ -315,8 +345,8 @@ Startup should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. 2. The runner selects a UserLiteBox profile that matches the deployment's host ABI. -3. The user side establishes an authenticated broker transport. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. -4. The broker binds the caller identity used in `BrokerRequest` to the authenticated peer. User mode does not choose its own authority identity. +3. The user side establishes an authenticated broker transport. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The first POC's Unix-socket transport returns an explicit unauthenticated placeholder credential through the same `ServerTransport` API that later authenticated transports will implement. +4. The broker binds the caller identity used for dispatch to the authenticated peer credential. User mode does not choose its own authority identity. 5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserLiteBox, BrokerHost, channel/ring, and host-syscall-profile versions. 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. @@ -396,7 +426,7 @@ The current `litebox` crate should not be split by whole module. Most modules mi | Current area | Keep in UserLiteBox | Move to BrokerCore / broker side | |---|---|---| | `LiteBox` object | user-mode facade, std-backed helpers, broker connection/session object | broker session/workload identity | -| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, generations, rights, dup/pass/close/refcounts | +| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, reference IDs, generations, rights, dup/pass/close/refcounts | | `fd::RawDescriptorStorage` | raw-int fd conversion for shim ABI | validation that a handle is live and authorized | | fd metadata | local ABI metadata and cached hints | shared open-description metadata and metadata inherited/duplicated/passed across processes | | `path.rs` | string/CStr conversion, cheap normalization helpers | authoritative path lookup, namespace traversal, permission checks | @@ -413,7 +443,7 @@ The current `litebox` crate should not be split by whole module. Most modules mi Concrete examples: -- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, rights, generation, refcount, passing, duplication, close, and revoke semantics. +- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, generations, refcount, passing, duplication, close, and revoke semantics. - `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. UserLiteBox should only marshal paths/buffers and use broker-owned data channels or rings for payloads. - `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. UserLiteBox should keep the socket facade and use broker-owned rings for data movement. - `mm::PageManager` currently owns VMA state and calls platform page-management operations. UserLiteBox can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. @@ -594,7 +624,7 @@ LiteBox's proposed design combines ideas from several systems rather than copyin | **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | | **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | | **SKernel** | split guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | -| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore handles should carry object identity, rights, and generation checks | +| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore handles should carry object identity and generation checks while BrokerCore stores authoritative rights | | **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | | **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | @@ -740,20 +770,42 @@ The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-c The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority substrate: -1. Define the component split: `Shim`, `UserLiteBox`, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. -2. Define the BrokerClient adapter, shared broker envelope, handle format, memory-grant format, service IDs, protocol versions, transport authentication, policy profile/version format, control/event/data channel schema, shared-memory ring layout, UserLiteBox/BrokerHost ABI versions, and deployment profile format. -3. Decide the Rust runtime strategy: `std` for UserLiteBox and userland broker; `no_std + alloc + traits` for kernel broker first; custom kernel `std` target only if later justified. -4. Define host syscall profiles for bootstrap, fast local mode, and strict mode. -5. Define broker-owned identity for workloads, processes, sessions, and threads. -6. Define broker-owned capability/resource IDs with generation checks. -7. Make UserLiteBox handle tables broker-backed views. -8. Add startup negotiation between runner and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. -9. Add a minimal PolicyEngine that can authorize/deny one simple broker-owned shared resource, such as an event object or pipe-like queue. -10. Add a broker wait/wakeup channel. -11. Prototype a broker-owned pipe or queue with shared-ring data path. -12. Prototype a broker-owned file object with mediated control/data-channel I/O, not host-handle delegation. -13. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. -14. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. -15. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. +The first proof of concept should use: + +```text +litebox_broker_protocol +litebox_broker_core +litebox_broker_transport +litebox_broker_wire +litebox_broker_unix_socket +litebox_broker_server +litebox_broker_client +separate userland broker process +Unix-domain-socket transport implementing neutral transport traits +control channel only +minimal PolicyEngine +broker-owned event object +``` + +Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through the broker, while unrelated operations continue through the existing local compatibility path. UserLiteBox handle entries can therefore contain either local compatibility objects or broker object/reference IDs with generations and cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. + +Then proceed incrementally: + +1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. +2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event object/reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. +3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, an explicit transport-provided peer-credential connection seam that is stored on the association and visible to policy operations, event object/reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. +4. Create `litebox_broker_transport` with neutral client/server request-response traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with a thin receive/send loop over BrokerCore connection dispatch. +5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. +6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. +7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future transports can implement the same neutral transport traits. +8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. +9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. +10. Make UserLiteBox handle tables broker-backed views for migrated object families. +11. Add a broker wait/wakeup channel. +12. Prototype a broker-owned pipe or queue with shared-ring data path. +13. Prototype a broker-owned file object with mediated control/data-channel I/O, not host-handle delegation. +14. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. +15. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. +16. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. That gives a controlled path from current single-process/single-session assumptions toward true shared-state support without rewriting every shim and platform at once. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 93b06703c..cc16460b9 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -54,17 +54,15 @@ Create a shared crate for broker protocol types. Initial contents: -- protocol version; -- session/process/workload IDs; -- broker object IDs with type tags and generations; -- request/response envelopes; +- protocol version type; +- broker event object/reference IDs with generations; +- minimal event request/response messages; +- readiness and wait outcome payloads; - ABI-neutral error categories; -- control/event/data channel frame headers; -- policy profile IDs; -- host syscall profile IDs; -- feature negotiation structures. -Prefer `no_std + alloc` for shared types so they can be reused by userland and kernel-broker deployments. +Richer request/response envelopes, control/event/data channel frame headers, policy profile IDs, host syscall profile IDs, and feature negotiation structures are added when later milestones need them. + +Prefer `no_std` for shared types, adding `alloc` only in crates that need owned buffers, so they can be reused by userland and kernel-broker deployments. Exit criteria: @@ -79,17 +77,27 @@ Implement a userland broker process first. Initial scope: - one broker process per sandbox session; -- one authenticated process association; +- at least one authenticated process association within the broker session; - control channel only; -- exact protocol version negotiation; +- major-version/minor-compatible protocol negotiation; +- neutral blocking `no_std` request/response transport traits with transport-specific error types and explicit clean-close receive semantics; +- transport-produced peer credentials returned through the server transport trait and passed into BrokerCore associations; +- reusable `no_std + alloc` request/response wire codec for byte-stream transports; +- Unix-domain-socket framing as the first concrete userland transport crate; +- a Unix-socket executable crate that wires the generic transport-neutral server to the concrete Unix transport; +- BrokerCore-owned connection negotiation, request dispatch, peer-credential association seam, and connection cleanup; - default-deny PolicyEngine; - fail-closed channel/session behavior. Exit criteria: - UserLiteBox can connect and negotiate. -- Broker binds caller identity to the authenticated transport. +- Broker binds caller identity to the authenticated transport. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. +- Userland transport code only receives/sends decoded frames and supplies peer credentials; BrokerCore owns broker request dispatch and typed dispatch outcomes, and the generic server reports successful termination as peer-close or broker-close with a reason. +- Client code does not need to depend on the userland broker server crate to use the first Unix socket transport. +- The generic broker server library does not depend on concrete Unix socket transport code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. +- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly. ## Phase 3: UserLiteBox facade @@ -106,7 +114,7 @@ Exit criteria: - Current tests can still use the local profile. - A broker-backed profile can issue a simple broker request. -- UserLiteBox handle entries can store broker object ID + generation + cached rights. +- UserLiteBox handle entries can store opaque broker object/reference IDs + generations plus local cached rights hints. ## Phase 4: First broker-owned object @@ -115,8 +123,7 @@ Start with a small event or pipe-like object, not filesystem or networking. Broker owns: - object ID and generation; -- endpoint/reference lifetime; -- close semantics; +- initial reference ID, generation, and rights; - readiness state; - wait/wakeup state. @@ -129,9 +136,8 @@ UserLiteBox owns: Exit criteria: -- Create, duplicate, close, wait, and readiness work through BrokerCore. -- Stale handles and wrong-generation handles are rejected. -- Process disconnect cleans up broker-owned references. +- Create, wait, and signal work through BrokerCore and the separate broker process. +- Duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup are added after the first end-to-end path is proven. ## Phase 5: Broker-backed fd semantics @@ -354,10 +360,11 @@ The smallest useful milestone is: ```text single process userland broker +typed broker client control channel only -default-deny PolicyEngine -broker-owned event/pipe object -UserLiteBox fd table maps guest fd -> broker object id +minimal PolicyEngine +broker-owned event object +UserLiteBox fd table maps guest fd -> broker object/reference id ``` This proves the trust boundary before taking on filesystem, networking, mapping, or multiprocess complexity. diff --git a/litebox_broker_client/Cargo.toml b/litebox_broker_client/Cargo.toml new file mode 100644 index 000000000..2ae0ddffc --- /dev/null +++ b/litebox_broker_client/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_client" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_client/src/error.rs b/litebox_broker_client/src/error.rs new file mode 100644 index 000000000..d32db0ffb --- /dev/null +++ b/litebox_broker_client/src/error.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +use litebox_broker_protocol::{BrokerResponse, ErrorCode}; + +/// Errors returned by the broker client adapter. +#[derive(Debug)] +pub enum ClientError { + /// The transport failed. + Transport(E), + /// An operation requiring an active broker session was called before negotiation. + NotNegotiated, + /// Negotiation was requested after the client was already active. + AlreadyNegotiated, + /// The broker closed the transport before returning a response. + TransportClosed, + /// The broker returned a response tag this client does not understand. + UnknownResponse { tag: u8 }, + /// BrokerCore rejected the request. + Broker(ErrorCode), + /// The broker returned a response type that does not match the request. + UnexpectedResponse(BrokerResponse), +} + +impl fmt::Display for ClientError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Transport(error) => write!(f, "broker transport failed: {error}"), + Self::NotNegotiated => write!(f, "broker client has not negotiated protocol version"), + Self::AlreadyNegotiated => f.write_str("broker client already negotiated"), + Self::TransportClosed => write!(f, "broker closed the transport"), + Self::UnknownResponse { tag } => write!(f, "unknown broker response tag {tag}"), + Self::Broker(error) => write!(f, "broker rejected request: {error}"), + Self::UnexpectedResponse(response) => { + write!(f, "broker returned unexpected response: {response:?}") + } + } + } +} + +impl core::error::Error for ClientError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Transport(error) => Some(error), + Self::Broker(error) => Some(error), + Self::NotNegotiated + | Self::AlreadyNegotiated + | Self::TransportClosed + | Self::UnknownResponse { .. } + | Self::UnexpectedResponse(_) => None, + } + } +} + +/// Broker client result type. +pub type Result = core::result::Result>; diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_client/src/event.rs new file mode 100644 index 000000000..9b4f51d1c --- /dev/null +++ b/litebox_broker_client/src/event.rs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ObjectHandle, ReadinessState, WaitOutcome, +}; + +use litebox_broker_transport::ClientTransport; + +use crate::{BrokerClient, ClientError, Result}; + +impl BrokerClient { + /// Creates a broker-owned event object. + pub fn create_event(&mut self) -> Result { + self.ensure_negotiated()?; + match self.request(BrokerRequest::CreateEvent)? { + BrokerResponse::Handle(handle) => Ok(handle), + response => Err(ClientError::UnexpectedResponse(response)), + } + } + + /// Checks whether an event wait would complete now. + pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { + self.ensure_negotiated()?; + match self.request(BrokerRequest::WaitEvent { handle })? { + BrokerResponse::Wait(outcome) => Ok(outcome), + response => Err(ClientError::UnexpectedResponse(response)), + } + } + + /// Signals a broker-owned event object. + pub fn signal_event(&mut self, handle: ObjectHandle) -> Result { + self.ensure_negotiated()?; + match self.request(BrokerRequest::SignalEvent { handle })? { + BrokerResponse::Readiness(readiness) => Ok(readiness), + response => Err(ClientError::UnexpectedResponse(response)), + } + } +} diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs new file mode 100644 index 000000000..2dbf62c66 --- /dev/null +++ b/litebox_broker_client/src/lib.rs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Typed client adapter for broker requests. +//! +//! The client owns request/response sequencing but does not own a transport. +//! Userland, kernel, or ring-buffer deployments can provide transports by +//! implementing [`litebox_broker_transport::ClientTransport`]. + +#![no_std] + +#[cfg(test)] +extern crate std; + +mod error; +mod event; +mod negotiate; + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; +use litebox_broker_transport::{ClientTransport, ReceivedResponse}; + +pub use error::{ClientError, Result}; + +/// Protocol version this client implementation requests by default. +pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); + +/// Typed client for broker operations. +pub struct BrokerClient { + transport: T, + state: ConnectionState, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active, +} + +impl BrokerClient { + /// Creates a broker client over an already-connected transport. + pub const fn new(transport: T) -> Self { + Self { + transport, + state: ConnectionState::AwaitingNegotiation, + } + } +} + +impl BrokerClient { + pub(crate) fn ensure_negotiated(&self) -> Result<(), T::Error> { + match self.state { + ConnectionState::AwaitingNegotiation => Err(ClientError::NotNegotiated), + ConnectionState::Active => Ok(()), + } + } + + pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { + self.transport + .send_request(&request) + .map_err(ClientError::Transport)?; + match self + .transport + .recv_response() + .map_err(ClientError::Transport)? + .ok_or(ClientError::TransportClosed)? + { + ReceivedResponse::Response(BrokerResponse::Error(error)) => { + Err(ClientError::Broker(error)) + } + ReceivedResponse::Response(response) => Ok(response), + ReceivedResponse::Unknown { tag } => Err(ClientError::UnknownResponse { tag }), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::convert::Infallible; + use litebox_broker_protocol::{ErrorCode, ProtocolVersion}; + + #[test] + fn event_operations_require_negotiation_without_sending() { + let transport = FakeTransport::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); + let mut client = BrokerClient::new(transport); + + assert!(matches!( + client.create_event(), + Err(ClientError::NotNegotiated) + )); + assert_eq!(client.transport.sent_request, None); + } + + #[test] + fn negotiate_version_sends_requested_version_and_activates_client() { + let requested = ProtocolVersion::new( + CLIENT_PROTOCOL_VERSION.major, + CLIENT_PROTOCOL_VERSION.minor - 1, + ); + let transport = FakeTransport::new(Some(BrokerResponse::Negotiated { + broker_protocol_version: CLIENT_PROTOCOL_VERSION, + })); + let mut client = BrokerClient::new(transport); + + assert_eq!( + client.negotiate_version(requested).unwrap(), + CLIENT_PROTOCOL_VERSION + ); + assert_eq!( + client.transport.sent_request, + Some(BrokerRequest::Negotiate { + protocol_version: requested + }) + ); + assert_eq!(client.state, ConnectionState::Active); + } + + struct FakeTransport { + sent_request: Option, + response: Option, + } + + impl FakeTransport { + const fn new(response: Option) -> Self { + Self { + sent_request: None, + response, + } + } + } + + impl ClientTransport for FakeTransport { + type Error = Infallible; + + fn send_request( + &mut self, + request: &BrokerRequest, + ) -> core::result::Result<(), Self::Error> { + self.sent_request = Some(*request); + Ok(()) + } + + fn recv_response(&mut self) -> core::result::Result, Self::Error> { + Ok(self.response.take().map(ReceivedResponse::Response)) + } + } +} diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_client/src/negotiate.rs new file mode 100644 index 000000000..dc765f774 --- /dev/null +++ b/litebox_broker_client/src/negotiate.rs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; + +use litebox_broker_transport::ClientTransport; + +use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION, ClientError, Result}; + +impl BrokerClient { + /// Negotiates the first POC protocol version. + pub fn negotiate(&mut self) -> Result { + self.negotiate_version(CLIENT_PROTOCOL_VERSION) + } + + /// Negotiates a caller-selected protocol version. + pub fn negotiate_version( + &mut self, + protocol_version: ProtocolVersion, + ) -> Result { + if self.state != crate::ConnectionState::AwaitingNegotiation { + return Err(ClientError::AlreadyNegotiated); + } + + let response = self.request(BrokerRequest::Negotiate { protocol_version })?; + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + self.state = crate::ConnectionState::Active; + Ok(broker_protocol_version) + } + response => Err(ClientError::UnexpectedResponse(response)), + } + } +} diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml new file mode 100644 index 000000000..b832a8790 --- /dev/null +++ b/litebox_broker_core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_core" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_core/src/connection.rs b/litebox_broker_core/src/connection.rs new file mode 100644 index 000000000..711d0be70 --- /dev/null +++ b/litebox_broker_core/src/connection.rs @@ -0,0 +1,492 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +use crate::identity::Association; +use crate::{BrokerCore, PolicyEngine, Result, SUPPORTED_PROTOCOL_VERSION}; +use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ErrorCode, ProtocolVersion}; +use litebox_broker_transport::PeerCredential; + +/// Broker-side state for one authenticated request/response connection. +pub struct BrokerConnection { + association: Association, + state: ConnectionState, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active, +} + +impl BrokerConnection { + pub(crate) const fn new(association: Association) -> Self { + Self { + association, + state: ConnectionState::AwaitingNegotiation, + } + } + + /// Returns the transport-authenticated peer credential for this connection. + pub const fn peer_credential(&self) -> PeerCredential { + self.association.peer_credential() + } +} + +/// Result of dispatching one broker request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BrokerDispatch { + response: BrokerResponse, + outcome: DispatchOutcome, +} + +/// Connection outcome after sending a dispatch response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DispatchOutcome { + /// Continue serving the connection. + Continue, + /// Close the connection after sending the response. + Close(CloseReason), +} + +/// Reason BrokerCore asked the server loop to close the connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CloseReason { + /// The peer requested an unsupported protocol version. + UnsupportedVersion, + /// 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::UnsupportedVersion => f.write_str("unsupported protocol version"), + Self::ProtocolViolation => f.write_str("protocol violation"), + } + } +} + +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), + } + } + + /// Response to send to the peer. + pub const fn response(&self) -> BrokerResponse { + self.response + } + + /// Connection outcome after sending the response. + pub const fn outcome(&self) -> DispatchOutcome { + self.outcome + } + + /// Reason the deployment should close the connection after sending the response. + pub const fn close_reason(&self) -> Option { + match self.outcome { + DispatchOutcome::Continue => None, + DispatchOutcome::Close(reason) => Some(reason), + } + } + + /// Whether the deployment should close the connection after sending the response. + pub const fn close_after_response(&self) -> bool { + self.close_reason().is_some() + } +} + +impl

BrokerCore

{ + /// Allocates broker state for one authenticated request/response connection. + pub fn create_connection( + &mut self, + peer_credential: PeerCredential, + ) -> Result { + self.create_association(peer_credential) + .map(BrokerConnection::new) + } + + /// Closes a broker connection and releases state owned by its association. + pub fn close_connection(&mut self, connection: BrokerConnection) { + self.close_association(connection.association); + } +} + +impl BrokerCore

{ + /// Handles one request for an authenticated broker connection. + /// + /// Transport-specific code should receive frames, pass decoded requests here, + /// send `BrokerDispatch::response`, and honor `BrokerDispatch::outcome`. + pub fn handle_connection_request( + &mut self, + connection: &mut BrokerConnection, + request: BrokerRequest, + ) -> BrokerDispatch { + if connection.state == ConnectionState::AwaitingNegotiation { + return match request { + BrokerRequest::Negotiate { protocol_version } => { + Self::handle_negotiation(connection, protocol_version) + } + _ => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + }; + } + + match request { + BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + BrokerRequest::CreateEvent => BrokerDispatch::continue_after(Self::handle_result( + self.create_event(connection.association), + BrokerResponse::Handle, + )), + BrokerRequest::WaitEvent { handle } => { + BrokerDispatch::continue_after(Self::handle_result( + self.wait_event(connection.association, handle), + BrokerResponse::Wait, + )) + } + BrokerRequest::SignalEvent { handle } => { + BrokerDispatch::continue_after(Self::handle_result( + self.signal_event(connection.association, handle), + BrokerResponse::Readiness, + )) + } + _ => BrokerDispatch::continue_after(BrokerResponse::Error( + ErrorCode::UnsupportedOperation, + )), + } + } + + /// Handles a well-formed frame with a request tag not known to this broker. + /// + /// Unknown future operations are regular feature-probing failures after + /// negotiation. Before negotiation they are protocol-state violations. + pub fn handle_unknown_connection_request( + &mut self, + connection: &mut BrokerConnection, + _tag: u8, + ) -> BrokerDispatch { + if connection.state == ConnectionState::AwaitingNegotiation { + BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ) + } else { + BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)) + } + } + + fn handle_negotiation( + connection: &mut BrokerConnection, + protocol_version: ProtocolVersion, + ) -> BrokerDispatch { + if protocol_version.is_supported_by(SUPPORTED_PROTOCOL_VERSION) { + connection.state = ConnectionState::Active; + BrokerDispatch::continue_after(BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, + }) + } else { + BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::UnsupportedVersion), + CloseReason::UnsupportedVersion, + ) + } + } + + fn handle_result( + result: Result, + into_response: impl FnOnce(T) -> BrokerResponse, + ) -> BrokerResponse { + match result { + Ok(value) => into_response(value), + Err(error) => BrokerResponse::Error(error), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BrokerCore, EventOnlyPolicy}; + use litebox_broker_protocol::{ReadinessState, WaitOutcome}; + + #[test] + fn dispatch_requires_negotiation_first() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + + let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert!(dispatch.close_after_response()); + assert_eq!( + dispatch.close_reason(), + Some(CloseReason::ProtocolViolation) + ); + assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_closes_after_post_negotiation_negotiate() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + negotiate(&mut core, &mut connection); + + let dispatch = core.handle_connection_request( + &mut connection, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert!(dispatch.close_after_response()); + assert_eq!( + dispatch.close_reason(), + Some(CloseReason::ProtocolViolation) + ); + } + + #[test] + fn dispatch_closes_after_unsupported_version() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + + let dispatch = core.handle_connection_request( + &mut connection, + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(SUPPORTED_PROTOCOL_VERSION.major + 1, 0), + }, + ); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::UnsupportedVersion) + ); + assert!(dispatch.close_after_response()); + assert_eq!( + dispatch.close_reason(), + Some(CloseReason::UnsupportedVersion) + ); + assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_accepts_supported_older_minor_version() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + let requested = ProtocolVersion::new( + SUPPORTED_PROTOCOL_VERSION.major, + SUPPORTED_PROTOCOL_VERSION.minor - 1, + ); + + let dispatch = core.handle_connection_request( + &mut connection, + BrokerRequest::Negotiate { + protocol_version: requested, + }, + ); + + assert_eq!( + dispatch.response(), + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert!(!dispatch.close_after_response()); + assert_eq!(connection.state, ConnectionState::Active); + } + + #[test] + fn dispatch_rejects_newer_minor_version() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + + let dispatch = core.handle_connection_request( + &mut connection, + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new( + SUPPORTED_PROTOCOL_VERSION.major, + SUPPORTED_PROTOCOL_VERSION.minor + 1, + ), + }, + ); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::UnsupportedVersion) + ); + assert!(dispatch.close_after_response()); + assert_eq!( + dispatch.close_reason(), + Some(CloseReason::UnsupportedVersion) + ); + assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_reports_unknown_requests_without_closing() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + negotiate(&mut core, &mut connection); + + let dispatch = core.handle_unknown_connection_request(&mut connection, 0xff); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::UnsupportedOperation) + ); + assert_eq!(dispatch.close_reason(), None); + } + + #[test] + fn dispatch_closes_unknown_requests_before_negotiation() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + + let dispatch = core.handle_unknown_connection_request(&mut connection, 0xff); + + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert_eq!( + dispatch.close_reason(), + Some(CloseReason::ProtocolViolation) + ); + } + + #[test] + fn dispatch_negotiates_then_routes_event_requests() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + + let dispatch = core.handle_connection_request( + &mut connection, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + assert_eq!( + dispatch.response(), + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert!(!dispatch.close_after_response()); + assert_eq!(connection.state, ConnectionState::Active); + + let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); + let handle = match dispatch.response() { + BrokerResponse::Handle(handle) => handle, + response => panic!("unexpected response: {response:?}"), + }; + + let dispatch = + core.handle_connection_request(&mut connection, BrokerRequest::WaitEvent { handle }); + assert_eq!( + dispatch.response(), + BrokerResponse::Wait(WaitOutcome::WouldBlock(ReadinessState::new(false, 0))) + ); + assert!(!dispatch.close_after_response()); + } + + #[test] + fn dispatch_rejects_handle_from_another_connection() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut owner = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + let mut other = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + negotiate(&mut core, &mut owner); + negotiate(&mut core, &mut other); + + let dispatch = core.handle_connection_request(&mut owner, BrokerRequest::CreateEvent); + let handle = match dispatch.response() { + BrokerResponse::Handle(handle) => handle, + response => panic!("unexpected response: {response:?}"), + }; + + let dispatch = + core.handle_connection_request(&mut other, BrokerRequest::WaitEvent { handle }); + assert_eq!( + dispatch.response(), + BrokerResponse::Error(ErrorCode::InvalidRights) + ); + assert!(!dispatch.close_after_response()); + } + + #[test] + fn close_connection_releases_owned_references_and_orphaned_objects() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut connection = core + .create_connection(PeerCredential::Unauthenticated) + .unwrap(); + negotiate(&mut core, &mut connection); + + let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); + assert!(matches!(dispatch.response(), BrokerResponse::Handle(_))); + assert_eq!(core.references.len(), 1); + assert_eq!(core.objects.len(), 1); + + core.close_connection(connection); + + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + } + + fn negotiate(core: &mut BrokerCore

, connection: &mut BrokerConnection) { + let dispatch = core.handle_connection_request( + connection, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + assert_eq!( + dispatch.response(), + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert_eq!(connection.state, ConnectionState::Active); + } +} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs new file mode 100644 index 000000000..bcac9f2b4 --- /dev/null +++ b/litebox_broker_core/src/event.rs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::identity::Association; +use crate::object::ObjectKind; +use crate::{BrokerCore, ObjectRights, ObjectType, PolicyEngine, Result}; +use litebox_broker_protocol::{ErrorCode, ObjectHandle, ReadinessState, WaitOutcome}; + +impl BrokerCore

{ + /// Creates a broker-owned event object. + pub(crate) fn create_event(&mut self, association: Association) -> Result { + self.authorize_create_object(association, ObjectType::Event)?; + + self.insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WAIT | ObjectRights::WRITE, + ) + } + + /// 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 transport-specific + /// wait primitives after BrokerCore authorizes and reports readiness state. + pub(crate) fn wait_event( + &mut self, + association: Association, + handle: ObjectHandle, + ) -> Result { + self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let state = self.event_state(handle)?; + Ok(if state.ready { + WaitOutcome::Ready(state) + } else { + WaitOutcome::WouldBlock(state) + }) + } + + /// Signals a broker-owned event object. + pub(crate) fn signal_event( + &mut self, + association: Association, + handle: ObjectHandle, + ) -> Result { + self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + match &mut self.object_mut(handle.object_id)?.kind { + ObjectKind::Event(event) => event.signal(), + } + } + + fn event_state(&self, handle: ObjectHandle) -> Result { + match &self.object(handle.object_id)?.kind { + ObjectKind::Event(event) => Ok(event.readiness_state()), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct EventObject { + ready: bool, + readiness_generation: u64, +} + +impl EventObject { + pub(crate) const fn new() -> Self { + Self { + ready: false, + readiness_generation: 0, + } + } + + pub(crate) const fn readiness_state(self) -> ReadinessState { + ReadinessState::new(self.ready, self.readiness_generation) + } + + fn signal(&mut self) -> Result { + self.ready = true; + self.readiness_generation = self + .readiness_generation + .checked_add(1) + .ok_or(ErrorCode::ResourceExhausted)?; + Ok(self.readiness_state()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{BrokerCore, EventOnlyPolicy}; + use litebox_broker_protocol::ObjectGeneration; + use litebox_broker_transport::PeerCredential; + + #[test] + fn wait_rejects_reference_without_wait_right() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let handle = core + .insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WRITE, + ) + .unwrap(); + + assert_eq!( + core.wait_event(association, handle), + Err(ErrorCode::InvalidRights) + ); + } + + #[test] + fn wait_rejects_stale_object_generation() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let mut handle = core.create_event(association).unwrap(); + handle.object_generation = ObjectGeneration::new(handle.object_generation.get() + 1); + + assert_eq!( + core.wait_event(association, handle), + Err(ErrorCode::StaleHandle) + ); + } + + #[test] + fn wait_rejects_handle_owned_by_another_association() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let owner = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let other = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let handle = core.create_event(owner).unwrap(); + + assert_eq!( + core.wait_event(other, handle), + Err(ErrorCode::InvalidRights) + ); + } +} diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs new file mode 100644 index 000000000..822eb54ff --- /dev/null +++ b/litebox_broker_core/src/identity.rs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{BrokerCore, Result, allocate_id}; +use litebox_broker_transport::PeerCredential; + +macro_rules! id_type { + ($(#[$meta:meta])* $name:ident) => { + $(#[$meta])* + #[repr(transparent)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct $name(u64); + + impl $name { + pub(crate) const fn new(raw: u64) -> Self { + Self(raw) + } + } + }; +} + +id_type! { + /// Broker-assigned sandbox session identity. + SessionId +} + +impl SessionId { + pub(crate) const FIRST: Self = Self::new(1); +} + +id_type! { + /// Broker-assigned guest process identity. + ProcessId +} + +/// Broker-assigned identity bound to one authenticated transport association. +/// +/// User mode does not choose this value. The userland broker transport or a +/// future BrokerHost authenticates the peer, then BrokerCore assigns this +/// identity for all requests received on that association. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Association { + /// Broker-assigned sandbox session identity. + session_id: SessionId, + /// Broker-assigned guest process identity. + process_id: ProcessId, + /// Transport-authenticated peer credential for this association. + peer_credential: PeerCredential, +} + +impl Association { + /// Creates an authenticated association identity. + pub(crate) const fn new( + session_id: SessionId, + process_id: ProcessId, + peer_credential: PeerCredential, + ) -> Self { + Self { + session_id, + process_id, + peer_credential, + } + } + + pub(crate) const fn peer_credential(self) -> PeerCredential { + self.peer_credential + } +} + +impl

BrokerCore

{ + /// Allocates a transport-bound broker association for one process connection. + pub(crate) fn create_association( + &mut self, + peer_credential: PeerCredential, + ) -> Result { + let process_id = allocate_id(&mut self.next_process_id)?; + // The POC models one sandbox session per BrokerCore; multi-session + // allocation belongs with the future deployment/session manager. + let association = Association::new( + SessionId::FIRST, + ProcessId::new(process_id), + peer_credential, + ); + Ok(association) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DefaultDenyPolicy; + use litebox_broker_protocol::ErrorCode; + + #[test] + fn create_association_uses_one_session_and_distinct_processes() { + let mut core = BrokerCore::new(DefaultDenyPolicy); + + let first = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let second = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + + assert_eq!(first.session_id, SessionId::FIRST); + assert_eq!(second.session_id, SessionId::FIRST); + assert_eq!(first.process_id, ProcessId::new(1)); + assert_eq!(second.process_id, ProcessId::new(2)); + assert_eq!(first.peer_credential(), PeerCredential::Unauthenticated); + assert_eq!(second.peer_credential(), PeerCredential::Unauthenticated); + } + + #[test] + fn create_association_issues_max_process_id_then_exhausts() { + let mut core = BrokerCore::new(DefaultDenyPolicy); + core.next_process_id = u64::MAX; + + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + assert_eq!(association.process_id, ProcessId::new(u64::MAX)); + assert_eq!(association.session_id, SessionId::FIRST); + assert_eq!(core.next_process_id, 0); + assert_eq!( + core.create_association(PeerCredential::Unauthenticated), + Err(ErrorCode::ResourceExhausted) + ); + assert_eq!(core.next_process_id, 0); + } +} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs new file mode 100644 index 000000000..3a644b48e --- /dev/null +++ b/litebox_broker_core/src/lib.rs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Transport-independent broker authority core. +//! +//! `litebox_broker_core` owns broker-side object identity, reference lifetime, +//! rights checks, generation checks, and policy calls. It deliberately has no +//! dependency on Unix sockets, shared-memory rings, or any other transport. + +#![no_std] + +extern crate alloc; +#[cfg(test)] +extern crate std; + +mod connection; +mod event; +mod identity; +mod object; +mod policy; +mod types; + +use alloc::collections::BTreeMap; + +pub use connection::{BrokerConnection, BrokerDispatch, CloseReason, DispatchOutcome}; +use litebox_broker_protocol::{ErrorCode, ObjectId, ObjectReferenceId, ProtocolVersion}; +use object::{ObjectEntry, ObjectReference}; +pub use policy::{ + DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyEngine, PolicyOperation, +}; +pub use types::{ObjectRights, ObjectType}; + +/// BrokerCore result type. +pub type Result = core::result::Result; + +/// Protocol version this broker core implementation supports. +pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); + +/// Transport-independent broker authority state. +pub struct BrokerCore

{ + policy: P, + next_process_id: u64, + next_object_id: u64, + next_reference_id: u64, + objects: BTreeMap, + references: BTreeMap, +} + +impl

BrokerCore

{ + /// Creates a broker core with the provided policy engine. + pub fn new(policy: P) -> Self { + Self { + policy, + 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(ErrorCode::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..78fcb11f5 --- /dev/null +++ b/litebox_broker_core/src/object.rs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::vec::Vec; + +use crate::event::EventObject; +use crate::identity::Association; +use crate::{ + BrokerCore, ObjectRights, ObjectType, PolicyEngine, PolicyOperation, Result, allocate_id, +}; +use litebox_broker_protocol::{ + ErrorCode, ObjectGeneration, ObjectHandle, ObjectId, ObjectReferenceGeneration, + ObjectReferenceId, +}; + +const FIRST_OBJECT_GENERATION: ObjectGeneration = ObjectGeneration::new(1); +const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectReference { + pub(crate) object_id: ObjectId, + /// Mirrors the referenced object's generation when this reference is minted. + /// + /// The duplicate lets stale-reference validation reject mismatched handles + /// before trusting any object-table lookup; object validation below still + /// checks the authoritative entry generation. + pub(crate) object_generation: ObjectGeneration, + pub(crate) reference_generation: ObjectReferenceGeneration, + pub(crate) owner: Association, + pub(crate) object_type: ObjectType, + pub(crate) rights: ObjectRights, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct ObjectEntry { + pub(crate) generation: ObjectGeneration, + 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 object or reference slots, so both + /// generations start at the authority-owned first generation. Any future + /// slot-reuse path must bump the corresponding generation before reissuing + /// a slot so stale handles cannot validate against a recycled entry. + pub(crate) fn insert_object_with_reference( + &mut self, + association: Association, + kind: ObjectKind, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result { + let object_id = self.allocate_object_id()?; + let reference_id = self.allocate_reference_id()?; + let object_generation = FIRST_OBJECT_GENERATION; + let reference_generation = FIRST_REFERENCE_GENERATION; + debug_assert_eq!(reference_generation, FIRST_REFERENCE_GENERATION); + + self.objects.insert( + object_id, + ObjectEntry { + generation: object_generation, + kind, + }, + ); + self.references.insert( + reference_id, + ObjectReference { + object_id, + object_generation, + reference_generation, + owner: association, + object_type, + rights, + }, + ); + + Ok(ObjectHandle::new( + object_id, + object_generation, + reference_id, + reference_generation, + )) + } + + pub(crate) fn authorize_create_object( + &mut self, + association: Association, + object_type: ObjectType, + ) -> Result<()> { + self.policy.authorize(PolicyOperation::create_object( + association.peer_credential(), + object_type, + )) + } + + pub(crate) fn authorize_object_use( + &mut self, + association: Association, + handle: ObjectHandle, + object_type: ObjectType, + rights: ObjectRights, + ) -> Result<()> { + self.validate_handle(association, handle, object_type, rights)?; + self.policy.authorize(PolicyOperation::use_object( + association.peer_credential(), + object_type, + rights, + )) + } + + pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { + self.objects.get(&object_id).ok_or(ErrorCode::UnknownObject) + } + + pub(crate) fn object_mut(&mut self, object_id: ObjectId) -> Result<&mut ObjectEntry> { + self.objects + .get_mut(&object_id) + .ok_or(ErrorCode::UnknownObject) + } + + fn validate_handle( + &self, + association: Association, + handle: ObjectHandle, + expected_type: ObjectType, + required_rights: ObjectRights, + ) -> Result<()> { + let reference = self + .references + .get(&handle.reference_id) + .ok_or(ErrorCode::UnknownObject)?; + debug_assert_eq!(reference.reference_generation, FIRST_REFERENCE_GENERATION); + if reference.owner != association { + return Err(ErrorCode::InvalidRights); + } + if reference.reference_generation != handle.reference_generation + || reference.object_generation != handle.object_generation + || reference.object_id != handle.object_id + { + return Err(ErrorCode::StaleHandle); + } + if reference.object_type != expected_type { + return Err(ErrorCode::WrongObjectType); + } + if !reference.rights.contains(required_rights) { + return Err(ErrorCode::InvalidRights); + } + + let object = self + .objects + .get(&reference.object_id) + .ok_or(ErrorCode::UnknownObject)?; + debug_assert_eq!(reference.object_generation, object.generation); + if object.generation != handle.object_generation { + return Err(ErrorCode::StaleHandle); + } + if object.kind.object_type() != expected_type { + return Err(ErrorCode::WrongObjectType); + } + + Ok(()) + } + + 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

{ + pub(crate) fn close_association(&mut self, association: Association) { + let reference_ids = self + .references + .iter() + .filter_map(|(reference_id, reference)| { + (reference.owner == association).then_some(*reference_id) + }) + .collect::>(); + + for reference_id in reference_ids { + self.references.remove(&reference_id); + } + + let object_ids = self + .objects + .keys() + .copied() + .filter(|object_id| { + !self + .references + .values() + .any(|reference| reference.object_id == *object_id) + }) + .collect::>(); + + for object_id in object_ids { + self.objects.remove(&object_id); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::DefaultDenyPolicy; + use litebox_broker_transport::PeerCredential; + + #[test] + fn object_and_reference_allocators_issue_max_id_then_exhaust() { + let mut core = BrokerCore::new(DefaultDenyPolicy); + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + core.next_object_id = u64::MAX; + core.next_reference_id = u64::MAX; + + let handle = core + .insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WAIT, + ) + .unwrap(); + + assert_eq!(handle.object_id, ObjectId::new(u64::MAX)); + assert_eq!(handle.reference_id, ObjectReferenceId::new(u64::MAX)); + assert_eq!(core.next_object_id, 0); + assert_eq!(core.next_reference_id, 0); + assert_eq!( + core.insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WAIT, + ), + Err(ErrorCode::ResourceExhausted) + ); + } + + #[test] + fn validate_handle_rejects_reference_object_generation_mismatch() { + let mut core = BrokerCore::new(DefaultDenyPolicy); + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let handle = core + .insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WAIT, + ) + .unwrap(); + + core.references + .get_mut(&handle.reference_id) + .unwrap() + .object_generation = ObjectGeneration::new(handle.object_generation.get() + 1); + + assert_eq!( + core.validate_handle(association, handle, ObjectType::Event, ObjectRights::WAIT), + Err(ErrorCode::StaleHandle) + ); + } + + #[test] + fn validate_handle_rejects_stale_reference_generation() { + let mut core = BrokerCore::new(DefaultDenyPolicy); + let association = core + .create_association(PeerCredential::Unauthenticated) + .unwrap(); + let handle = core + .insert_object_with_reference( + association, + ObjectKind::Event(EventObject::new()), + ObjectType::Event, + ObjectRights::WAIT, + ) + .unwrap(); + let stale_handle = ObjectHandle::new( + handle.object_id, + handle.object_generation, + handle.reference_id, + ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), + ); + + assert_eq!( + core.validate_handle( + association, + stale_handle, + ObjectType::Event, + ObjectRights::WAIT + ), + Err(ErrorCode::StaleHandle) + ); + } +} diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs new file mode 100644 index 000000000..53e1155ca --- /dev/null +++ b/litebox_broker_core/src/policy.rs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{ObjectRights, ObjectType}; +use litebox_broker_protocol::ErrorCode; +use litebox_broker_transport::PeerCredential; + +/// 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 { + /// Transport-authenticated peer credential for the caller. + peer_credential: PeerCredential, + object_type: ObjectType, + 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 }, +} + +impl PolicyOperation { + /// Creates a policy operation for creating a broker-owned object type. + pub const fn create_object(peer_credential: PeerCredential, object_type: ObjectType) -> Self { + Self::Object { + peer_credential, + object_type, + operation: ObjectOperation::Create, + } + } + + /// Creates a policy operation for using a broker-owned object with rights. + pub const fn use_object( + peer_credential: PeerCredential, + object_type: ObjectType, + rights: ObjectRights, + ) -> Self { + Self::Object { + peer_credential, + object_type, + operation: ObjectOperation::Use { rights }, + } + } +} + +/// Broker policy decision interface. +pub trait PolicyEngine { + /// Authorizes or denies a broker operation. + fn authorize(&mut self, operation: PolicyOperation) -> Result<(), ErrorCode>; +} + +/// Policy engine that denies every operation. +#[derive(Clone, Copy, Debug, Default)] +pub struct DefaultDenyPolicy; + +impl PolicyEngine for DefaultDenyPolicy { + fn authorize(&mut self, _operation: PolicyOperation) -> Result<(), ErrorCode> { + Err(ErrorCode::PolicyDenied) + } +} + +/// Policy engine that allows only the first POC event-object surface. +/// +/// The current event methods request exactly one operation right at a time: +/// `WAIT` for wait and `WRITE` for signal. Combined-right use requests are +/// intentionally denied until an event method that needs combined rights is +/// designed. +#[derive(Clone, Copy, Debug, Default)] +pub struct EventOnlyPolicy; + +impl PolicyEngine for EventOnlyPolicy { + fn authorize(&mut self, operation: PolicyOperation) -> Result<(), ErrorCode> { + match operation { + PolicyOperation::Object { + peer_credential: PeerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Create, + } => Ok(()), + PolicyOperation::Object { + peer_credential: PeerCredential::Unauthenticated, + object_type: ObjectType::Event, + operation: ObjectOperation::Use { rights }, + } if rights == ObjectRights::WAIT || rights == ObjectRights::WRITE => Ok(()), + PolicyOperation::Object { + object_type: ObjectType::Event, + .. + } => Err(ErrorCode::PolicyDenied), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_transport::PeerCredential; + + #[test] + fn event_only_policy_allows_only_current_event_surface() { + let mut policy = EventOnlyPolicy; + + assert_eq!( + policy.authorize(PolicyOperation::create_object( + PeerCredential::Unauthenticated, + ObjectType::Event + )), + Ok(()) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + PeerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT + )), + Ok(()) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + PeerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WRITE + )), + Ok(()) + ); + assert_eq!( + policy.authorize(PolicyOperation::use_object( + PeerCredential::Unauthenticated, + ObjectType::Event, + ObjectRights::WAIT | ObjectRights::WRITE + )), + Err(ErrorCode::PolicyDenied) + ); + } +} diff --git a/litebox_broker_core/src/types.rs b/litebox_broker_core/src/types.rs new file mode 100644 index 000000000..990123d36 --- /dev/null +++ b/litebox_broker_core/src/types.rs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::ops::BitOr; + +/// 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 { + /// Right to wait for readiness. + pub const WAIT: Self = Self(1 << 0); + /// Right to write payload data or signal an object. + pub const WRITE: Self = Self(1 << 1); + + /// Returns true when all `required` rights are present. + pub const fn contains(self, required: Self) -> bool { + (self.0 & required.0) == required.0 + } +} + +impl BitOr for ObjectRights { + type Output = Self; + + fn bitor(self, rhs: Self) -> Self::Output { + Self(self.0 | rhs.0) + } +} diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml new file mode 100644 index 000000000..cdb474acf --- /dev/null +++ b/litebox_broker_protocol/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "litebox_broker_protocol" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[lints] +workspace = true + diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs new file mode 100644 index 000000000..20de455fd --- /dev/null +++ b/litebox_broker_protocol/src/error.rs @@ -0,0 +1,88 @@ +// 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, + /// 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, + /// Error code emitted by a newer broker and not understood by this client. + Unknown(u16), +} + +impl ErrorCode { + /// 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, + 4 => Self::PolicyDenied, + 5 => Self::UnknownObject, + 6 => Self::StaleHandle, + 7 => Self::WrongObjectType, + 8 => Self::InvalidRights, + 9 => Self::ResourceExhausted, + 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::PolicyDenied => 4, + Self::UnknownObject => 5, + Self::StaleHandle => 6, + Self::WrongObjectType => 7, + Self::InvalidRights => 8, + Self::ResourceExhausted => 9, + 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::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::Unknown(raw) => write!(f, "unknown broker error code {raw}"), + } + } +} + +impl core::error::Error for ErrorCode {} diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs new file mode 100644 index 000000000..3b10b0e6c --- /dev/null +++ b/litebox_broker_protocol/src/lib.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared broker protocol types. +//! +//! This crate is intentionally transport-neutral. It describes broker-visible +//! opaque handles, errors, and versions, but does not know whether the bytes +//! move over Unix sockets, shared rings, kernel traps, or another IPC mechanism. + +#![no_std] + +mod error; +mod message; +mod object; + +pub use error::ErrorCode; +pub use message::{BrokerRequest, BrokerResponse, ReadinessState, WaitOutcome}; +pub use object::{ + ObjectGeneration, ObjectHandle, ObjectId, 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 + } +} diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs new file mode 100644 index 000000000..b3fd5fa62 --- /dev/null +++ b/litebox_broker_protocol/src/message.rs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{ErrorCode, ObjectHandle, ProtocolVersion}; + +/// Broker-authoritative readiness state for one object. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct ReadinessState { + /// Whether the object is currently ready. + pub 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(ready: bool, generation: u64) -> Self { + Self { ready, generation } + } +} + +/// Result of checking a wait condition in BrokerCore. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WaitOutcome { + /// The object is ready now. + Ready(ReadinessState), + /// The object is not ready; deployment-specific wait plumbing may block. + WouldBlock(ReadinessState), +} + +/// Broker request transported over the control channel. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerRequest { + /// Protocol negotiation request. + Negotiate { + /// Required protocol version. + protocol_version: ProtocolVersion, + }, + /// Create a broker-owned event object. + CreateEvent, + /// Check whether an event wait would complete now. + WaitEvent { + /// Event handle. + handle: ObjectHandle, + }, + /// Signal an event. + SignalEvent { + /// Event handle. + handle: ObjectHandle, + }, +} + +/// Broker response transported over the control channel. +#[derive(Clone, Copy, 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, + }, + /// Operation returned a broker object handle. + Handle(ObjectHandle), + /// Operation returned readiness state. + Readiness(ReadinessState), + /// Operation returned wait state. + Wait(WaitOutcome), + /// Operation failed with an ABI-neutral broker error. + Error(ErrorCode), +} diff --git a/litebox_broker_protocol/src/object.rs b/litebox_broker_protocol/src/object.rs new file mode 100644 index 000000000..7ce333eb6 --- /dev/null +++ b/litebox_broker_protocol/src/object.rs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// Broker-owned object identifier. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ObjectId(u64); + +impl ObjectId { + /// Creates an object 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 ObjectGeneration(u64); + +impl ObjectGeneration { + /// Creates a 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 + } +} + +/// Broker object handle returned to UserLiteBox. +/// +/// UserLiteBox may cache this value, but the broker remains authoritative for +/// object lifetime, reference lifetime, type, rights, and generation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ObjectHandle { + /// Opaque broker object identifier. + pub object_id: ObjectId, + /// Object generation used to reject stale handles after object-slot reuse. + pub object_generation: ObjectGeneration, + /// 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( + object_id: ObjectId, + object_generation: ObjectGeneration, + reference_id: ObjectReferenceId, + reference_generation: ObjectReferenceGeneration, + ) -> Self { + Self { + object_id, + object_generation, + 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_server/Cargo.toml b/litebox_broker_server/Cargo.toml new file mode 100644 index 000000000..41b0b66ff --- /dev/null +++ b/litebox_broker_server/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_server" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_server/src/lib.rs new file mode 100644 index 000000000..2ccba1630 --- /dev/null +++ b/litebox_broker_server/src/lib.rs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Transport-neutral broker server loop for the split-broker proof of concept. +//! +//! This crate wires `litebox_broker_core` to any implementation of the neutral +//! server transport trait. Concrete transports live in separate crates such as +//! `litebox_broker_unix_socket`. + +#![no_std] + +mod server; + +pub use server::{BrokerServeError, ConnectionTermination, serve_connection}; diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs new file mode 100644 index 000000000..d58e261ec --- /dev/null +++ b/litebox_broker_server/src/server.rs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +use litebox_broker_core::{ + BrokerConnection, BrokerCore, CloseReason, DispatchOutcome, PolicyEngine, +}; +use litebox_broker_transport::{ReceivedRequest, ServerTransport}; + +/// Serves one broker connection over the provided connected transport. +pub fn serve_connection( + core: &mut BrokerCore

, + transport: &mut T, +) -> Result> +where + P: PolicyEngine, + T: ServerTransport, +{ + let peer_credential = transport + .peer_credential() + .map_err(BrokerServeError::Transport)?; + let mut connection = core + .create_connection(peer_credential) + .map_err(|_error| BrokerServeError::ConnectionSetup)?; + + let result = serve_request_loop(core, transport, &mut connection); + core.close_connection(connection); + result +} + +fn serve_request_loop( + core: &mut BrokerCore

, + transport: &mut T, + connection: &mut BrokerConnection, +) -> Result> +where + P: PolicyEngine, + T: ServerTransport, +{ + loop { + let Some(received) = transport + .recv_request() + .map_err(BrokerServeError::Transport)? + else { + break; + }; + + let dispatch = match received { + ReceivedRequest::Request(request) => { + core.handle_connection_request(connection, request) + } + ReceivedRequest::Unknown { tag } => { + core.handle_unknown_connection_request(connection, tag) + } + }; + transport + .send_response(&dispatch.response()) + .map_err(BrokerServeError::Transport)?; + if let DispatchOutcome::Close(reason) = dispatch.outcome() { + return Ok(ConnectionTermination::BrokerClosed(reason)); + } + } + + Ok(ConnectionTermination::PeerClosed) +} + +/// Terminal outcome for a successfully served broker connection. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ConnectionTermination { + /// The peer cleanly closed the transport. + PeerClosed, + /// BrokerCore sent a terminal protocol response and closed the connection. + BrokerClosed(CloseReason), +} + +/// Errors returned by a broker receive/send loop. +#[derive(Debug)] +pub enum BrokerServeError { + /// BrokerCore could not allocate state for a new connection. + ConnectionSetup, + /// The concrete transport failed. + Transport(E), +} + +impl fmt::Display for BrokerServeError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::ConnectionSetup => f.write_str("broker connection setup failed"), + Self::Transport(error) => write!(f, "broker transport failed: {error}"), + } + } +} + +impl core::error::Error for BrokerServeError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::ConnectionSetup => None, + Self::Transport(error) => Some(error), + } + } +} diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml new file mode 100644 index 000000000..303581c46 --- /dev/null +++ b/litebox_broker_transport/Cargo.toml @@ -0,0 +1,11 @@ +[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..0969a914e --- /dev/null +++ b/litebox_broker_transport/src/lib.rs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared broker transport traits. +//! +//! This crate defines blocking request/response direction traits only. Concrete +//! transports own framing, buffering, and the IPC mechanism; non-blocking IPCs +//! can provide a blocking adapter at this boundary. + +#![no_std] + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; + +/// Peer identity information supplied by the transport or host layer. +/// +/// The first userland proof of concept does not authenticate Unix-socket peers, +/// but BrokerCore still accepts an explicit credential value so authenticated +/// transports can plumb identity through the same connection-creation seam. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[non_exhaustive] +pub enum PeerCredential { + /// Explicit deployment mode for the initial unauthenticated userland POC. + /// + /// Transports that are expected to authenticate peers must return an error + /// from [`ServerTransport::peer_credential`] when authentication is + /// unavailable or fails; this variant is only for deployments that + /// deliberately choose unauthenticated operation. + Unauthenticated, +} + +/// Request received from a transport. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReceivedRequest { + /// A request understood by the current protocol crate. + Request(BrokerRequest), + /// A request tag emitted by a newer peer and not understood by this process. + Unknown { tag: u8 }, +} + +impl From for ReceivedRequest { + fn from(request: BrokerRequest) -> Self { + Self::Request(request) + } +} + +/// Response received from a transport. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ReceivedResponse { + /// A response understood by the current protocol crate. + Response(BrokerResponse), + /// A response tag emitted by a newer broker and not understood by this process. + Unknown { tag: u8 }, +} + +impl From for ReceivedResponse { + fn from(response: BrokerResponse) -> Self { + Self::Response(response) + } +} + +/// Client-side request/response transport for broker calls. +pub trait ClientTransport { + /// Transport-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 transport cleanly before + /// starting another response frame. + fn recv_response(&mut self) -> Result, Self::Error>; +} + +/// Server-side request/response transport for broker calls. +pub trait ServerTransport { + /// Transport-specific error type. + type Error; + + /// Returns the peer credential authenticated for this transport endpoint. + fn peer_credential(&self) -> Result; + + /// Receives one broker request. + /// + /// Returns `Ok(None)` when the peer closed the transport 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_unix_socket/Cargo.toml b/litebox_broker_unix_socket/Cargo.toml new file mode 100644 index 000000000..20f0597f5 --- /dev/null +++ b/litebox_broker_unix_socket/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "litebox_broker_unix_socket" +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" } +litebox_broker_server = { path = "../litebox_broker_server", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_wire = { path = "../litebox_broker_wire", version = "0.1.0" } + +[[bin]] +name = "litebox-broker-userland" +path = "src/bin/litebox-broker-userland.rs" + +[dev-dependencies] +litebox_broker_client = { path = "../litebox_broker_client", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs new file mode 100644 index 000000000..5080448ff --- /dev/null +++ b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::env; +use std::io; +use std::os::unix::net::UnixListener; +use std::path::PathBuf; + +use litebox_broker_core::{BrokerCore, EventOnlyPolicy}; +use litebox_broker_server::{BrokerServeError, serve_connection}; +use litebox_broker_unix_socket::UnixStreamServerTransport; + +fn main() -> io::Result<()> { + let args = Args::parse(env::args().skip(1))?; + let listener = UnixListener::bind(&args.socket_path)?; + let (stream, _) = listener.accept()?; + let mut transport = UnixStreamServerTransport::from_accepted(stream); + let mut broker = BrokerCore::new(EventOnlyPolicy); + serve_connection(&mut broker, &mut transport) + .map(|_| ()) + .map_err(broker_error) +} + +fn broker_error(error: BrokerServeError) -> io::Error { + match error { + BrokerServeError::ConnectionSetup => io::Error::other("broker connection setup failed"), + BrokerServeError::Transport(error) => error, + } +} + +struct Args { + socket_path: PathBuf, +} + +impl Args { + fn parse(args: impl IntoIterator) -> io::Result { + let mut socket_path = None; + let mut args = args.into_iter(); + + while let Some(arg) = args.next() { + match arg.as_str() { + "--socket" => { + let path = args.next().ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidInput, "--socket requires a path") + })?; + socket_path = Some(PathBuf::from(path)); + } + _ => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "unknown command-line argument", + )); + } + } + } + + let socket_path = socket_path + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "--socket is required"))?; + Ok(Self { socket_path }) + } +} diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs new file mode 100644 index 000000000..823a4dde9 --- /dev/null +++ b/litebox_broker_unix_socket/src/lib.rs @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Unix-domain-socket broker transport for hosted userland deployments. +//! +//! This crate deliberately uses `std` because Unix-domain sockets and `std::io` +//! framing are hosted userland concerns. Portable broker interfaces live in the +//! no_std protocol, wire, transport, client, core, and server crates. + +use std::io::{self, Read, Write}; +use std::os::unix::net::UnixStream; +use std::path::Path; + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; +use litebox_broker_transport::{ + ClientTransport, PeerCredential, ReceivedRequest, ReceivedResponse, ServerTransport, +}; +use litebox_broker_wire::{decode_request, decode_response, encode_request, encode_response}; + +const MAX_FRAME_LEN: usize = 64 * 1024; + +/// Client-side Unix-domain-socket transport for the hosted userland POC. +pub struct UnixStreamClientTransport { + stream: UnixStream, +} + +impl UnixStreamClientTransport { + /// Creates a client transport from an already-connected Unix stream. + pub const fn from_connected(stream: UnixStream) -> Self { + Self { stream } + } + + /// Connects to a userland broker Unix socket. + pub fn connect(path: impl AsRef) -> io::Result { + UnixStream::connect(path).map(Self::from_connected) + } +} + +/// Server-side Unix-domain-socket transport for the hosted userland POC. +pub struct UnixStreamServerTransport { + stream: UnixStream, +} + +impl UnixStreamServerTransport { + /// Creates a server transport from an accepted Unix stream. + pub const fn from_accepted(stream: UnixStream) -> Self { + Self { stream } + } +} + +impl ClientTransport for UnixStreamClientTransport { + type Error = io::Error; + + fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { + write_frame( + &mut self.stream, + &encode_request(*request).map_err(wire_error)?, + ) + } + + fn recv_response(&mut self) -> io::Result> { + let Some(frame) = read_frame(&mut self.stream)? else { + return Ok(None); + }; + decode_response(&frame).map(Some).map_err(wire_error) + } +} + +impl ServerTransport for UnixStreamServerTransport { + type Error = io::Error; + + fn peer_credential(&self) -> io::Result { + Ok(PeerCredential::Unauthenticated) + } + + fn recv_request(&mut self) -> io::Result> { + let Some(frame) = read_frame(&mut self.stream)? else { + return Ok(None); + }; + decode_request(&frame).map(Some).map_err(wire_error) + } + + fn send_response(&mut self, response: &BrokerResponse) -> io::Result<()> { + write_frame( + &mut self.stream, + &encode_response(*response).map_err(wire_error)?, + ) + } +} + +fn read_frame(stream: &mut UnixStream) -> io::Result>> { + let mut len_buf = [0; 4]; + let mut read = 0; + while read < len_buf.len() { + 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]; + stream.read_exact(&mut frame).map_err(|error| { + if error.kind() == io::ErrorKind::UnexpectedEof { + invalid_data("truncated broker frame") + } else { + error + } + })?; + Ok(Some(frame)) +} + +fn write_frame(stream: &mut UnixStream, frame: &[u8]) -> 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"))?; + stream.write_all(&len.to_le_bytes())?; + stream.write_all(frame) +} + +fn invalid_data(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidData, message) +} + +fn wire_error(error: litebox_broker_wire::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(&mut writer, &[1, 2, 3]).unwrap(); + + assert_eq!(read_frame(&mut reader).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(&mut reader).unwrap().is_none()); + } + + #[test] + fn truncated_length_prefix_is_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&[1, 0]).unwrap(); + drop(writer); + + let error = read_frame(&mut reader).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn zero_frame_length_is_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&0u32.to_le_bytes()).unwrap(); + + let error = read_frame(&mut reader).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn oversize_frame_length_is_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer + .write_all(&u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_le_bytes()) + .unwrap(); + + let error = read_frame(&mut reader).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn truncated_frame_body_is_invalid() { + let (mut writer, mut reader) = UnixStream::pair().unwrap(); + writer.write_all(&4u32.to_le_bytes()).unwrap(); + writer.write_all(&[1, 2]).unwrap(); + drop(writer); + + let error = read_frame(&mut reader).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + } +} diff --git a/litebox_broker_unix_socket/tests/userland_broker.rs b/litebox_broker_unix_socket/tests/userland_broker.rs new file mode 100644 index 000000000..27e91d1fe --- /dev/null +++ b/litebox_broker_unix_socket/tests/userland_broker.rs @@ -0,0 +1,82 @@ +// 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}; +use std::thread; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use litebox_broker_client::BrokerClient; +use litebox_broker_core::SUPPORTED_PROTOCOL_VERSION; +use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_unix_socket::UnixStreamClientTransport; + +#[test] +fn separate_process_broker_serves_event_object_requests() { + let socket_path = unique_socket_path(); + let mut child = spawn_broker(&socket_path); + let transport = connect_with_retry(&socket_path).unwrap(); + let mut client = BrokerClient::new(transport); + + assert_eq!(client.negotiate().unwrap(), SUPPORTED_PROTOCOL_VERSION); + + let handle = client.create_event().unwrap(); + assert_eq!( + client.wait_event(handle).unwrap(), + WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + ); + + assert_eq!( + client.signal_event(handle).unwrap(), + ReadinessState::new(true, 1) + ); + + assert_eq!( + client.wait_event(handle).unwrap(), + WaitOutcome::Ready(ReadinessState::new(true, 1)) + ); + + drop(client); + assert!(child.wait().unwrap().success()); + let _ = fs::remove_file(socket_path); +} + +fn spawn_broker(socket_path: &Path) -> Child { + Command::new(env!("CARGO_BIN_EXE_litebox-broker-userland")) + .arg("--socket") + .arg(socket_path) + .spawn() + .unwrap() +} + +fn connect_with_retry(socket_path: &Path) -> io::Result { + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match UnixStreamClientTransport::connect(socket_path) { + Ok(transport) => return Ok(transport), + 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_broker_wire/Cargo.toml b/litebox_broker_wire/Cargo.toml new file mode 100644 index 000000000..fb1ef8b67 --- /dev/null +++ b/litebox_broker_wire/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "litebox_broker_wire" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } +litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs new file mode 100644 index 000000000..35242116d --- /dev/null +++ b/litebox_broker_wire/src/lib.rs @@ -0,0 +1,421 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Reusable byte codec for broker request/response control-channel messages. + +#![no_std] + +extern crate alloc; + +use core::fmt; + +use alloc::vec::Vec; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ErrorCode, ObjectGeneration, ObjectHandle, ObjectId, + ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, ReadinessState, WaitOutcome, +}; +use litebox_broker_transport::{ReceivedRequest, ReceivedResponse}; + +const REQUEST_TAG_NEGOTIATE: u8 = 0; +const REQUEST_TAG_CREATE_EVENT: u8 = 1; +const REQUEST_TAG_WAIT_EVENT: u8 = 2; +const REQUEST_TAG_SIGNAL_EVENT: u8 = 3; + +const RESPONSE_TAG_NEGOTIATED: u8 = 0; +const RESPONSE_TAG_HANDLE: u8 = 1; +const RESPONSE_TAG_READINESS: u8 = 2; +const RESPONSE_TAG_WAIT: u8 = 3; +const RESPONSE_TAG_ERROR: u8 = 4; + +const WAIT_OUTCOME_TAG_READY: u8 = 1; +const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; + +/// Error produced while encoding or decoding a broker wire message. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WireError { + /// The encoder was asked to emit a request tag this codec does not own. + EncodeUnknownRequestTag, + /// The encoder was asked to emit a response tag this codec does not own. + EncodeUnknownResponseTag, + /// The encoder was asked to emit a wait-outcome tag this codec does not own. + EncodeUnknownWaitOutcome, + /// 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, + /// The wait-outcome tag is unknown. + UnknownWaitOutcome, + /// 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::EncodeUnknownRequestTag => { + f.write_str("cannot encode unknown broker request tag") + } + Self::EncodeUnknownResponseTag => { + f.write_str("cannot encode unknown broker response tag") + } + Self::EncodeUnknownWaitOutcome => { + f.write_str("cannot encode unknown broker wait outcome tag") + } + Self::TrailingBytes => f.write_str("trailing broker wire bytes"), + Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), + Self::UnknownWaitOutcome => f.write_str("unknown broker wait outcome"), + 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) -> Result, WireError> { + let mut encoder = Encoder::default(); + match request { + BrokerRequest::Negotiate { protocol_version } => { + encoder.u8(REQUEST_TAG_NEGOTIATE); + encoder.u16(protocol_version.major); + encoder.u16(protocol_version.minor); + } + BrokerRequest::CreateEvent => { + encoder.u8(REQUEST_TAG_CREATE_EVENT); + } + BrokerRequest::WaitEvent { handle } => { + encoder.u8(REQUEST_TAG_WAIT_EVENT); + encoder.handle(handle); + } + BrokerRequest::SignalEvent { handle } => { + encoder.u8(REQUEST_TAG_SIGNAL_EVENT); + encoder.handle(handle); + } + _ => return Err(WireError::EncodeUnknownRequestTag), + } + Ok(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: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + }, + REQUEST_TAG_CREATE_EVENT => BrokerRequest::CreateEvent, + REQUEST_TAG_WAIT_EVENT => BrokerRequest::WaitEvent { + handle: decoder.handle()?, + }, + REQUEST_TAG_SIGNAL_EVENT => BrokerRequest::SignalEvent { + handle: decoder.handle()?, + }, + _ => return Ok(ReceivedRequest::Unknown { tag }), + }; + decoder.finish()?; + Ok(ReceivedRequest::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) -> Result, WireError> { + let mut encoder = Encoder::default(); + match response { + BrokerResponse::Negotiated { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_NEGOTIATED); + encoder.u16(broker_protocol_version.major); + encoder.u16(broker_protocol_version.minor); + } + BrokerResponse::Handle(handle) => { + encoder.u8(RESPONSE_TAG_HANDLE); + encoder.handle(handle); + } + BrokerResponse::Readiness(readiness) => { + encoder.u8(RESPONSE_TAG_READINESS); + encoder.readiness(readiness); + } + BrokerResponse::Wait(outcome) => { + encoder.u8(RESPONSE_TAG_WAIT); + match outcome { + WaitOutcome::Ready(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_READY); + encoder.readiness(readiness); + } + WaitOutcome::WouldBlock(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); + encoder.readiness(readiness); + } + _ => return Err(WireError::EncodeUnknownWaitOutcome), + } + } + BrokerResponse::Error(error) => { + encoder.u8(RESPONSE_TAG_ERROR); + encoder.u16(error.as_raw()); + } + _ => return Err(WireError::EncodeUnknownResponseTag), + } + Ok(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: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + }, + RESPONSE_TAG_HANDLE => BrokerResponse::Handle(decoder.handle()?), + RESPONSE_TAG_READINESS => BrokerResponse::Readiness(decoder.readiness()?), + RESPONSE_TAG_WAIT => { + let outcome = match decoder.u8()? { + WAIT_OUTCOME_TAG_READY => WaitOutcome::Ready(decoder.readiness()?), + WAIT_OUTCOME_TAG_WOULD_BLOCK => WaitOutcome::WouldBlock(decoder.readiness()?), + _ => return Err(WireError::UnknownWaitOutcome), + }; + BrokerResponse::Wait(outcome) + } + RESPONSE_TAG_ERROR => { + let error = ErrorCode::from_raw(decoder.u16()?); + BrokerResponse::Error(error) + } + _ => return Ok(ReceivedResponse::Unknown { tag }), + }; + decoder.finish()?; + Ok(ReceivedResponse::Response(response)) +} + +#[derive(Default)] +struct Encoder { + bytes: Vec, +} + +impl Encoder { + fn finish(self) -> Vec { + self.bytes + } + + fn bool(&mut self, value: bool) { + self.u8(u8::from(value)); + } + + fn u8(&mut self, value: u8) { + self.bytes.push(value); + } + + fn u16(&mut self, value: u16) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + fn u64(&mut self, value: u64) { + self.bytes.extend_from_slice(&value.to_le_bytes()); + } + + fn handle(&mut self, handle: ObjectHandle) { + self.u64(handle.object_id.get()); + self.u64(handle.object_generation.get()); + self.u64(handle.reference_id.get()); + self.u64(handle.reference_generation.get()); + } + + fn readiness(&mut self, readiness: ReadinessState) { + self.bool(readiness.ready); + self.u64(readiness.generation); + } +} + +struct Decoder<'a> { + bytes: &'a [u8], + offset: usize, +} + +impl<'a> Decoder<'a> { + const fn new(bytes: &'a [u8]) -> Self { + Self { bytes, offset: 0 } + } + + fn finish(&self) -> Result<(), WireError> { + if self.offset == self.bytes.len() { + Ok(()) + } else { + Err(WireError::TrailingBytes) + } + } + + fn bool(&mut self) -> Result { + match self.u8()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(WireError::InvalidBoolean), + } + } + + fn u8(&mut self) -> Result { + let bytes = self.take(1)?; + Ok(bytes[0]) + } + + fn u16(&mut self) -> Result { + let bytes = self.take(2)?; + Ok(u16::from_le_bytes([bytes[0], bytes[1]])) + } + + 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], + ])) + } + + fn handle(&mut self) -> Result { + let object_id = ObjectId::new(self.u64()?); + let object_generation = ObjectGeneration::new(self.u64()?); + let reference_id = ObjectReferenceId::new(self.u64()?); + let reference_generation = ObjectReferenceGeneration::new(self.u64()?); + + Ok(ObjectHandle::new( + object_id, + object_generation, + reference_id, + reference_generation, + )) + } + + fn readiness(&mut self) -> Result { + Ok(ReadinessState::new(self.bool()?, self.u64()?)) + } + + 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) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_codec_round_trips_all_variants() { + let handle = sample_handle(); + let requests = [ + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(1, 0), + }, + BrokerRequest::CreateEvent, + BrokerRequest::WaitEvent { handle }, + BrokerRequest::SignalEvent { handle }, + ]; + + for request in requests { + assert_eq!( + decode_request(&encode_request(request).unwrap()).unwrap(), + ReceivedRequest::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::Handle(handle), + BrokerResponse::Readiness(ReadinessState::new(false, 7)), + BrokerResponse::Wait(WaitOutcome::Ready(ReadinessState::new(true, 8))), + BrokerResponse::Wait(WaitOutcome::WouldBlock(ReadinessState::new(false, 9))), + BrokerResponse::Error(ErrorCode::PolicyDenied), + ]; + + for response in responses { + assert_eq!( + decode_response(&encode_response(response).unwrap()).unwrap(), + ReceivedResponse::Response(response) + ); + } + } + + #[test] + fn decode_rejects_malformed_request_frames() { + assert_eq!( + decode_request(&[0xff, 1, 2, 3]), + Ok(ReceivedRequest::Unknown { tag: 0xff }) + ); + assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); + let mut frame = encode_request(BrokerRequest::CreateEvent).unwrap(); + 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(ReceivedResponse::Unknown { tag: 0xff }) + ); + assert_eq!( + decode_response(&[3, 0xff]), + Err(WireError::UnknownWaitOutcome) + ); + assert_eq!( + decode_response(&[4, 0xff, 0xff]), + Ok(ReceivedResponse::Response(BrokerResponse::Error( + ErrorCode::Unknown(0xffff) + ))) + ); + + let mut invalid_bool = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]; + assert_eq!( + decode_response(&invalid_bool), + Err(WireError::InvalidBoolean) + ); + + invalid_bool[1] = 1; + invalid_bool[9] = 1; + let mut frame = invalid_bool.to_vec(); + frame.push(0xff); + assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); + } + + #[test] + fn readiness_response_wire_shape_is_pinned() { + assert_eq!( + encode_response(BrokerResponse::Readiness(ReadinessState::new( + true, + 0x0102_0304_0506_0708 + ))) + .unwrap(), + [2, 1, 8, 7, 6, 5, 4, 3, 2, 1] + ); + } + + const fn sample_handle() -> ObjectHandle { + ObjectHandle::new( + ObjectId::new(11), + ObjectGeneration::new(12), + ObjectReferenceId::new(13), + ObjectReferenceGeneration::new(14), + ) + } +} From ec6163c7a00a4b15af5919ca5efd1c461b2d5d09 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 10:46:25 -0700 Subject: [PATCH 07/66] fix cargo lock --- Cargo.lock | 972 +++++++++++++++++++++++------------------------------ 1 file changed, 426 insertions(+), 546 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 946f4698f..3169ccc32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,9 +22,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "anstream" -version = "1.0.0" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -70,44 +70,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.14" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "1.0.0" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.5" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.11" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayvec" @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "base64" @@ -153,9 +153,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.3" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" @@ -163,10 +163,10 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "log", "prettyplease", "proc-macro2", @@ -185,18 +185,18 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitfield" -version = "0.19.4" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21ba6517c6b0f2bf08be60e187ab64b038438f22dd755614d8fe4d4098c46419" +checksum = "6bf79f42d21f18b5926a959280215903e659760da994835d27c3a0c5ff4f898f" dependencies = [ "bitfield-macros", ] [[package]] name = "bitfield-macros" -version = "0.19.4" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f48d6ace212fdf1b45fd6b566bb40808415344642b76c3224c07c8df9da81e97" +checksum = "6115af052c7914c0cbb97195e5c72cb61c511527250074f5c041d1048b0d8b16" dependencies = [ "proc-macro2", "quote", @@ -211,9 +211,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.3" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" @@ -275,9 +275,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "chrono" @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.1" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" dependencies = [ "clap_builder", "clap_derive", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" dependencies = [ "anstream", "anstyle", @@ -356,9 +356,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.1" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.1.0" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "cms" @@ -386,19 +386,20 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.5" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "console" -version = "0.16.3" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" dependencies = [ "encode_unicode", "libc", - "windows-sys 0.61.2", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -407,20 +408,13 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const_fn" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60" - [[package]] name = "const_format" -version = "0.2.36" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", - "konst", ] [[package]] @@ -436,9 +430,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -495,9 +489,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -553,14 +547,14 @@ version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0963443817029b2024136fc4dd07a5107eb8f977eaf18fcd1fdeb11306b64ad" dependencies = [ - "defmt 1.1.0", + "defmt 1.0.1", ] [[package]] name = "defmt" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f" +checksum = "548d977b6da32fa1d1fda2876453da1e7df63ad0304c8b3dae4dbe7b96f39b78" dependencies = [ "bitflags 1.3.2", "defmt-macros", @@ -568,9 +562,9 @@ dependencies = [ [[package]] name = "defmt-macros" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b" +checksum = "3d4fc12a85bcf441cfe44344c4b72d58493178ce635338a3f3b78943aceb258e" dependencies = [ "defmt-parser", "proc-macro-error2", @@ -679,9 +673,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", @@ -690,9 +684,9 @@ dependencies = [ [[package]] name = "either" -version = "1.16.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "elf" @@ -708,9 +702,9 @@ checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "env_filter" -version = "1.0.1" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", "regex", @@ -718,9 +712,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.10" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", @@ -757,9 +751,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.10" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -778,18 +772,19 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "filetime" -version = "0.2.29" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", + "libredox", ] [[package]] @@ -852,9 +847,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.0" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +checksum = "6ad492b2cf1d89d568a43508ab24f98501fe03f2f31c01e1d0fe7366a71745d2" dependencies = [ "autocfg", ] @@ -937,9 +932,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -954,23 +949,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "getset" version = "0.1.6" @@ -1022,12 +1004,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - [[package]] name = "heapless" version = "0.8.0" @@ -1055,9 +1031,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", "itoa", @@ -1103,9 +1079,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.10.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -1116,6 +1092,7 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1195,13 +1172,12 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", - "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -1209,9 +1185,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1222,9 +1198,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1236,15 +1212,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1256,15 +1232,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", @@ -1275,12 +1251,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -1300,9 +1270,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.2" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1324,28 +1294,15 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - [[package]] name = "insta" -version = "1.47.2" +version = "1.43.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" dependencies = [ "console", "once_cell", "similar", - "tempfile", ] [[package]] @@ -1367,40 +1324,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] -name = "is_terminal_polyfill" -version = "1.70.2" +name = "iri-string" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] [[package]] -name = "itertools" -version = "0.13.0" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" -version = "0.14.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.18" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.27" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "392c70591e8749fe235ddaf513e6f58b26bce3dcc16524cecc8936f75afa161e" +checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50" dependencies = [ "jiff-static", "log", @@ -1411,9 +1369,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.27" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b605b0c050d845fc355bb11eb3f9a8deddc218ea60c76e61aa1f2adfb2c96a" +checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78" dependencies = [ "proc-macro2", "quote", @@ -1422,46 +1380,28 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ - "cfg-if", - "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "jsonwebtoken" -version = "10.4.0" +version = "10.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +checksum = "0529410abe238729a60b108898784df8984c87f6054c9c4fcacc47e4803c1ce1" dependencies = [ "base64", - "getrandom 0.2.17", + "getrandom 0.2.16", "js-sys", "serde", "serde_json", "signature", - "zeroize", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", ] -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - [[package]] name = "lazy_static" version = "1.5.0" @@ -1471,17 +1411,11 @@ dependencies = [ "spin 0.9.8", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" -version = "0.2.186" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" @@ -1495,25 +1429,37 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.16" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall", +] [[package]] name = "linux-raw-sys" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litebox" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.11.0", "buddy_system_allocator", "either", - "hashbrown 0.15.5", + "hashbrown", "litebox_util_log", "rangemap", "ringbuf", @@ -1588,7 +1534,7 @@ name = "litebox_common_linux" version = "0.1.0" dependencies = [ "bitfield", - "bitflags 2.11.1", + "bitflags 2.11.0", "cfg-if", "elf", "int-enum", @@ -1602,7 +1548,7 @@ dependencies = [ name = "litebox_common_optee" version = "0.1.0" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "elf", "litebox", "litebox_common_linux", @@ -1643,7 +1589,7 @@ version = "0.1.0" dependencies = [ "arrayvec", "bindgen", - "bitflags 2.11.1", + "bitflags 2.11.0", "litebox", "litebox_common_linux", "litebox_util_log", @@ -1680,12 +1626,12 @@ dependencies = [ "aligned-vec", "arrayvec", "authenticode", - "bitflags 2.11.1", + "bitflags 2.11.0", "cms", "const-oid", "digest", "elf", - "hashbrown 0.15.5", + "hashbrown", "libc", "litebox", "litebox_common_linux", @@ -1843,7 +1789,7 @@ name = "litebox_shim_linux" version = "0.1.0" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.11.0", "bitvec", "libc", "litebox", @@ -1870,7 +1816,7 @@ dependencies = [ "arrayvec", "ctr", "elf", - "hashbrown 0.15.5", + "hashbrown", "hmac", "litebox", "litebox_common_linux", @@ -1941,9 +1887,9 @@ dependencies = [ [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -1956,11 +1902,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ - "serde_core", + "serde", "sval", "sval_ref", "value-bag", @@ -1983,15 +1929,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.1" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.10" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", ] @@ -2014,9 +1960,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "wasi", @@ -2046,9 +1992,9 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.18" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", @@ -2128,9 +2074,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.6" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", "rustversion", @@ -2138,9 +2084,9 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.6" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro2", "quote", @@ -2212,15 +2158,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.4" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.2" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "opaque-debug" @@ -2230,11 +2176,11 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.80" +version = "0.10.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -2255,15 +2201,15 @@ dependencies = [ [[package]] name = "openssl-probe" -version = "0.2.1" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.116" +version = "0.9.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" dependencies = [ "cc", "libc", @@ -2300,7 +2246,7 @@ checksum = "9cd31dcfdbbd7431a807ef4df6edd6473228e94d5c805e8cf671227a21bad068" dependencies = [ "anyhow", "clap", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", "rand", @@ -2308,9 +2254,15 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.17" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" @@ -2335,30 +2287,36 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" -version = "0.2.7" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -2406,9 +2364,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2427,9 +2385,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -2440,12 +2398,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "radium" version = "0.7.0" @@ -2479,14 +2431,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.2.16", ] [[package]] name = "rangemap" -version = "1.7.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" +checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" [[package]] name = "raw-cpuid" @@ -2494,14 +2446,14 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", ] [[package]] name = "rayon" -version = "1.12.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -2517,11 +2469,20 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "regex" -version = "1.12.3" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", @@ -2531,9 +2492,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", @@ -2542,15 +2503,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", @@ -2617,17 +2578,17 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.2" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.1.4" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -2636,9 +2597,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] @@ -2651,9 +2612,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.23" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -2666,9 +2627,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.29" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ "windows-sys 0.61.2", ] @@ -2690,11 +2651,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.7.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "core-foundation", "core-foundation-sys", "libc", @@ -2711,12 +2672,6 @@ dependencies = [ "libc", ] -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - [[package]] name = "seq-macro" version = "0.3.6" @@ -2764,15 +2719,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", + "ryu", "serde", "serde_core", - "zmij", ] [[package]] @@ -2836,9 +2791,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "similar" @@ -2883,9 +2838,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2960,15 +2915,15 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sval" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb9efbae90f97301f4d25f3be63dfd99d6b7af9d088228a52ec960d649b2e7d" +checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" [[package]] name = "sval_buffer" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff5c0280ea0af40b3a1fd0b532680b5067482ffb81412ee66ec04b1d9952b49a" +checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" dependencies = [ "sval", "sval_ref", @@ -2976,18 +2931,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b9067f2e68f58e110cf8019a268057e3791cf74b0fdb1b3c7c6e49104f44e6" +checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ebdbf0e4b175884aa587fcf551c16dabe245ea04235abdd8cbbd160b27ed5a" +checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" dependencies = [ "itoa", "ryu", @@ -2996,9 +2951,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e448d9fa216a6c16670b28d624fbbcf5c04e41eb187bd7c52e01222ffa72a12" +checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" dependencies = [ "itoa", "ryu", @@ -3007,9 +2962,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021de5b5c26efd544c694cef9b8a9abe8633481bf3be1ea145a18a740818b291" +checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" dependencies = [ "sval", "sval_buffer", @@ -3018,18 +2973,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ef5ffec8bc52ded04ee424ab8d959e25e64fd40a48a16d21f8991ce824c1bb" +checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.20.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b983833a8a2390f89ebcf9b9acd06883017014b4ffd72ee28e0c9de6852039f5" +checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" dependencies = [ "serde_core", "sval", @@ -3038,9 +2993,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3081,9 +3036,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.46" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -3096,19 +3051,19 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac9ee8b664c9f1740cd813fea422116f8ba29997bb7c878d1940424889802897" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "log", "num-traits", ] [[package]] name = "tempfile" -version = "3.27.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3116,18 +3071,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -3145,9 +3100,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -3155,9 +3110,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.11.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3170,9 +3125,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -3185,9 +3140,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", @@ -3234,20 +3189,20 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.11" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.11.0", "bytes", "futures-util", "http", "http-body", + "iri-string", "pin-project-lite", "tower", "tower-layer", "tower-service", - "url", ] [[package]] @@ -3264,9 +3219,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.44" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -3287,9 +3242,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.36" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -3308,9 +3263,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.23" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -3338,9 +3293,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.20.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicase" @@ -3350,9 +3305,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-normalization" @@ -3401,9 +3356,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "value-bag" -version = "1.12.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -3480,27 +3435,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -3511,19 +3457,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ + "cfg-if", + "futures-util", "js-sys", + "once_cell", "wasm-bindgen", + "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3531,9 +3481,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -3544,35 +3494,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -3586,23 +3514,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -3676,13 +3592,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets", + "windows-targets 0.53.5", ] [[package]] @@ -3694,6 +3619,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + [[package]] name = "windows-targets" version = "0.53.5" @@ -3701,22 +3642,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_aarch64_msvc" version = "0.53.1" @@ -3725,139 +3678,87 @@ checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" -version = "0.53.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] -name = "windows_i686_gnullvm" +name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] -name = "windows_i686_msvc" -version = "0.53.1" +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] -name = "windows_x86_64_gnu" +name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" +name = "windows_i686_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] -name = "windows_x86_64_msvc" +name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] -name = "wit-bindgen" -version = "0.51.0" +name = "windows_x86_64_gnu" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] -name = "wit-bindgen" -version = "0.57.1" +name = "windows_x86_64_gnu" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] -name = "wit-bindgen-core" -version = "0.51.0" +name = "windows_x86_64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] -name = "wit-bindgen-rust" -version = "0.51.0" +name = "windows_x86_64_gnullvm" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" +name = "windows_x86_64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "wit-component" -version = "0.244.0" +name = "windows_x86_64_msvc" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] -name = "wit-parser" -version = "0.244.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wyz" @@ -3881,13 +3782,12 @@ dependencies = [ [[package]] name = "x86_64" -version = "0.15.4" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7841fa0098ceb15c567d93d3fae292c49e10a7662b4936d5f6a9728594555ba" +checksum = "0f042214de98141e9c8706e8192b73f56494087cc55ebec28ce10f26c5c364ae" dependencies = [ "bit_field", - "bitflags 2.11.1", - "const_fn", + "bitflags 2.11.0", "rustversion", "volatile", ] @@ -3919,9 +3819,9 @@ checksum = "32ac00cd3f8ec9c1d33fb3e7958a82df6989c42d747bd326c822b1d625283547" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -3930,9 +3830,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", @@ -3942,18 +3842,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.49" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.49" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -3962,18 +3862,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.8" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", @@ -3986,26 +3886,12 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -4014,9 +3900,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -4025,17 +3911,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", "syn", ] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" From 551ca42761d56ad625794948d952833b78c4a0e9 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 11:10:08 -0700 Subject: [PATCH 08/66] Decouple broker core from protocol layers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 5 +- docs/broker-design.md | 37 +- docs/impl-plan.md | 15 +- litebox_broker_core/Cargo.toml | 4 - litebox_broker_core/src/connection.rs | 455 +-------------- litebox_broker_core/src/error.rs | 36 ++ litebox_broker_core/src/event.rs | 81 ++- litebox_broker_core/src/identity.rs | 59 +- litebox_broker_core/src/lib.rs | 22 +- litebox_broker_core/src/object.rs | 122 +++- litebox_broker_core/src/policy.rs | 44 +- litebox_broker_server/Cargo.toml | 1 + litebox_broker_server/src/lib.rs | 5 +- litebox_broker_server/src/server.rs | 552 +++++++++++++++++- litebox_broker_transport/src/lib.rs | 4 +- .../tests/userland_broker.rs | 2 +- 16 files changed, 853 insertions(+), 591 deletions(-) create mode 100644 litebox_broker_core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 3169ccc32..0595e672e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1485,10 +1485,6 @@ dependencies = [ [[package]] name = "litebox_broker_core" version = "0.1.0" -dependencies = [ - "litebox_broker_protocol", - "litebox_broker_transport", -] [[package]] name = "litebox_broker_protocol" @@ -1499,6 +1495,7 @@ name = "litebox_broker_server" version = "0.1.0" dependencies = [ "litebox_broker_core", + "litebox_broker_protocol", "litebox_broker_transport", ] diff --git a/docs/broker-design.md b/docs/broker-design.md index bce9e77ff..b7b82a376 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -18,7 +18,7 @@ Host support for UserLiteBox: broker kernel: BrokerHost Authority domain: - BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker entry/server + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform ``` The authority domain differs by deployment: @@ -49,7 +49,10 @@ Broker-kernel deployment: UserLiteBox <-> BrokerHost Authority domain: - broker authority interface -> BrokerCore + optional BrokerServices + broker authority interface -> broker entry/server + | + v + BrokerCore + optional BrokerServices | | | consult | execute after authorization v v @@ -94,7 +97,7 @@ BrokerHost provides the user-mode execution substrate that UserLiteBox expects: BrokerHost is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is the host-side component that lets user-mode LiteBox processes run and reach the broker. It may be trusted kernel code, but it should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. -BrokerHost may carry broker authority traffic as transport, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry layer in BrokerCore, not BrokerHost. +BrokerHost may carry broker authority traffic as transport, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry/server layer, not BrokerHost or BrokerCore. In broker-kernel deployments, BrokerHost shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; BrokerHost code is in the TCB and must be audited accordingly. @@ -131,19 +134,19 @@ The broker split should use crate names that make the authority boundary visible | Crate | Initial role | |---|---| | `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event object/reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | -| `litebox_broker_core` | Transport-independent authority logic: peer-credential-to-association seam, broker-owned session/process associations, object/reference registry, object type and rights authority, generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. | +| `litebox_broker_core` | Protocol- and transport-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | | `litebox_broker_transport` | Neutral `no_std` transport-trait crate for blocking directional request/response transport contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the transport-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | | `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream transports reuse it rather than duplicating protocol encoding. | | `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket transport crate implementing the neutral client/server transport traits for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | -| `litebox_broker_server` | Transport-neutral `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server transport. The server obtains the peer credential through `ServerTransport`, so deployment entry points do not construct credentials out-of-band, and successful termination reports whether the peer closed or BrokerCore closed with a typed reason. | +| `litebox_broker_server` | Protocol- and transport-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server transport. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_client` | `no_std` transport-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | | `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | `litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected transport. -Transport must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, or any specific IPC implementation. Shared request/response transport traits live in `litebox_broker_transport`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +Transport must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_transport`; the broker server adapts protocol and transport concepts into direct BrokerCore domain calls. Shared request/response transport traits live in `litebox_broker_transport`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. -Forward-compatible protocol probing is explicit: unknown request tags decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server loop can return `UnsupportedOperation` without closing the connection. Structurally malformed frames remain transport/wire errors. +Forward-compatible protocol probing is explicit: unknown request tags decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection. Structurally malformed frames remain transport/wire errors. BrokerCore only sees supported, already-adapted domain operations. Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical split: @@ -152,11 +155,11 @@ Kernel/trusted deployments will likely link the host-support and broker-authorit | `litebox_broker_host` | BrokerHost abstractions for user-mode execution support, trap/upcall/transport delivery, process/thread setup, and the UserLiteBox host ABI. | | `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, BrokerServices, PolicyEngine, BrokerPlatform, and BrokerHost. | -These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, while BrokerCore decodes, validates, dispatches, and authorizes broker requests. +These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, broker server/entry code, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, broker server/entry code decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. ## Three interfaces -There are three logical interfaces. In a broker-kernel deployment, the UserLiteBox/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding, validation, dispatch, and authorization remain broker-authority responsibilities. +There are three logical interfaces. In a broker-kernel deployment, the UserLiteBox/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding and sequencing remain broker server/entry responsibilities, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. | Interface | Userland deployment | Kernel deployment | Purpose | |---|---|---|---| @@ -193,7 +196,7 @@ External broker authority APIs: | API | Shape | |---|---| -| `UserLiteBox -> BrokerCore` | generic capability/resource protocol | +| `UserLiteBox -> broker server/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | | `shim-specific user client -> BrokerService` | shim/domain-specific protocol | Internal broker-authority APIs: @@ -331,7 +334,7 @@ Shared spec crates should define: - the broker envelope and handle/memory-grant formats; - broker transport authentication and peer identity binding; -- BrokerCore protocol versions; +- broker authority protocol versions; - BrokerService IDs, protocol versions, request/response types, and feature requirements; - PolicyEngine policy versions, policy profile IDs, and audit requirements; - broker capability names and profiles; @@ -685,7 +688,7 @@ Future mapping: | platform trait surface | internal UserLiteBox host-support layer, BrokerHost, and BrokerPlatform surfaces | | shim/domain-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | -The important change is that the current core API should not become the cross-boundary ABI. UserLiteBox can keep ergonomic Rust APIs, but BrokerCore needs an explicit handle/capability protocol. +The important change is that the current core API should not become the cross-boundary ABI. UserLiteBox can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker server/entry code adapts that protocol into BrokerCore domain calls. ### `litebox_platform_lvbs` @@ -697,7 +700,8 @@ Future mapping: |---|---| | page-table and address-space management | BrokerPlatform | | VTL/trusted-domain trap and transport mechanism | BrokerHost | -| broker request decode, validation, and dispatch | broker entry layer in BrokerCore | +| broker request decode and dispatch | broker server/entry layer | +| domain validation and authorization | BrokerCore + PolicyEngine | | normal-world memory mapping and validation | BrokerPlatform | | host I/O, network backend hooks, root key/secrets | BrokerPlatform executing PolicyEngine-authorized operations | | user-mode shim/platform helpers | UserLiteBox internals, not the current crate wholesale | @@ -714,7 +718,8 @@ Future mapping: |---|---| | early boot and trusted-domain initialization | broker bootstrap | | VTL trap/transport dispatch | BrokerHost | -| broker request decode, validation, and dispatch | broker entry layer in BrokerCore | +| broker request decode and dispatch | broker server/entry layer | +| domain validation and authorization | BrokerCore + PolicyEngine | | session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | | shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + UserLiteBox | | authoritative TA/session/object identity currently held by the runner | BrokerCore + OP-TEE BrokerService + PolicyEngine | @@ -793,8 +798,8 @@ Then proceed incrementally: 1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event object/reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, an explicit transport-provided peer-credential connection seam that is stored on the association and visible to policy operations, event object/reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. -4. Create `litebox_broker_transport` with neutral client/server request-response traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with a thin receive/send loop over BrokerCore connection dispatch. +3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, event object/reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and transport-neutral. +4. Create `litebox_broker_transport` with neutral client/server request-response traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from transport/protocol requests to direct BrokerCore domain calls. 5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future transports can implement the same neutral transport traits. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index cc16460b9..e26226074 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -11,7 +11,7 @@ User mode: Shim + UserLiteBox + optional shim-specific user clients Authority domain: - BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker entry/server + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform Kernel-broker deployments: BrokerHost supports user-mode UserLiteBox execution and broker transport @@ -24,6 +24,7 @@ The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation - Build vertical slices, not a big-bang refactor. - Keep UserLiteBox untrusted and broker authority explicit. - Keep BrokerCore shim-neutral. +- Keep BrokerCore protocol-neutral and transport-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/server code adapts protocol requests and transport credentials before calling it. - Put domain-specific authority in BrokerServices. - Put final allow/deny/audit decisions in PolicyEngine. - Keep BrokerPlatform as authorized backend execution, not a policy owner. @@ -81,11 +82,12 @@ Initial scope: - control channel only; - major-version/minor-compatible protocol negotiation; - neutral blocking `no_std` request/response transport traits with transport-specific error types and explicit clean-close receive semantics; -- transport-produced peer credentials returned through the server transport trait and passed into BrokerCore associations; +- transport-produced peer credentials returned through the server transport trait and mapped by the broker server into BrokerCore caller credentials; - reusable `no_std + alloc` request/response wire codec for byte-stream transports; - Unix-domain-socket framing as the first concrete userland transport crate; -- a Unix-socket executable crate that wires the generic transport-neutral server to the concrete Unix transport; -- BrokerCore-owned connection negotiation, request dispatch, peer-credential association seam, and connection cleanup; +- a Unix-socket executable that wires the generic transport-neutral server to the concrete Unix transport; +- server-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; +- BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and connection cleanup; - default-deny PolicyEngine; - fail-closed channel/session behavior. @@ -93,7 +95,8 @@ Exit criteria: - UserLiteBox can connect and negotiate. - Broker binds caller identity to the authenticated transport. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. -- Userland transport code only receives/sends decoded frames and supplies peer credentials; BrokerCore owns broker request dispatch and typed dispatch outcomes, and the generic server reports successful termination as peer-close or broker-close with a reason. +- Userland transport code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. +- BrokerCore has no dependency on `litebox_broker_protocol`, `litebox_broker_transport`, wire codecs, or concrete IPC crates. - Client code does not need to depend on the userland broker server crate to use the first Unix socket transport. - The generic broker server library does not depend on concrete Unix socket transport code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. @@ -329,7 +332,7 @@ Split trusted deployment code into: Exit criteria: - BrokerHost does not decode or authorize BrokerRequest. -- BrokerCore protocol and object semantics are reused. +- Broker server/entry protocol semantics and BrokerCore object semantics are reused through the same protocol-to-core adapter boundary. - Kernel-broker deployment passes the same broker-object conformance tests as userland broker. ## Phase 13: OP-TEE BrokerService diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index b832a8790..42b5c700c 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -3,9 +3,5 @@ name = "litebox_broker_core" version = "0.1.0" edition = "2024" -[dependencies] -litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } - [lints] workspace = true diff --git a/litebox_broker_core/src/connection.rs b/litebox_broker_core/src/connection.rs index 711d0be70..071f151db 100644 --- a/litebox_broker_core/src/connection.rs +++ b/litebox_broker_core/src/connection.rs @@ -1,119 +1,39 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::fmt; - use crate::identity::Association; -use crate::{BrokerCore, PolicyEngine, Result, SUPPORTED_PROTOCOL_VERSION}; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ErrorCode, ProtocolVersion}; -use litebox_broker_transport::PeerCredential; +use crate::{BrokerCore, CallerCredential, Result}; -/// Broker-side state for one authenticated request/response connection. +/// Broker-side authority state for one caller connection. +/// +/// This type intentionally carries only BrokerCore association identity. Protocol +/// negotiation, request sequencing, and transport state live in the server layer. pub struct BrokerConnection { association: Association, - state: ConnectionState, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ConnectionState { - AwaitingNegotiation, - Active, } impl BrokerConnection { pub(crate) const fn new(association: Association) -> Self { - Self { - association, - state: ConnectionState::AwaitingNegotiation, - } + Self { association } } - /// Returns the transport-authenticated peer credential for this connection. - pub const fn peer_credential(&self) -> PeerCredential { - self.association.peer_credential() + pub(crate) const fn association(&self) -> Association { + self.association } -} -/// Result of dispatching one broker request. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BrokerDispatch { - response: BrokerResponse, - outcome: DispatchOutcome, -} - -/// Connection outcome after sending a dispatch response. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum DispatchOutcome { - /// Continue serving the connection. - Continue, - /// Close the connection after sending the response. - Close(CloseReason), -} - -/// Reason BrokerCore asked the server loop to close the connection. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CloseReason { - /// The peer requested an unsupported protocol version. - UnsupportedVersion, - /// 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::UnsupportedVersion => f.write_str("unsupported protocol version"), - Self::ProtocolViolation => f.write_str("protocol violation"), - } - } -} - -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), - } - } - - /// Response to send to the peer. - pub const fn response(&self) -> BrokerResponse { - self.response - } - - /// Connection outcome after sending the response. - pub const fn outcome(&self) -> DispatchOutcome { - self.outcome - } - - /// Reason the deployment should close the connection after sending the response. - pub const fn close_reason(&self) -> Option { - match self.outcome { - DispatchOutcome::Continue => None, - DispatchOutcome::Close(reason) => Some(reason), - } - } - - /// Whether the deployment should close the connection after sending the response. - pub const fn close_after_response(&self) -> bool { - self.close_reason().is_some() + /// Returns the broker-entry-authenticated caller credential for this connection. + pub const fn caller_credential(&self) -> CallerCredential { + self.association.caller_credential() } } impl

BrokerCore

{ - /// Allocates broker state for one authenticated request/response connection. + /// Allocates broker authority state for one caller connection. pub fn create_connection( &mut self, - peer_credential: PeerCredential, + caller_credential: CallerCredential, ) -> Result { - self.create_association(peer_credential) + self.create_association(caller_credential) .map(BrokerConnection::new) } @@ -123,348 +43,33 @@ impl

BrokerCore

{ } } -impl BrokerCore

{ - /// Handles one request for an authenticated broker connection. - /// - /// Transport-specific code should receive frames, pass decoded requests here, - /// send `BrokerDispatch::response`, and honor `BrokerDispatch::outcome`. - pub fn handle_connection_request( - &mut self, - connection: &mut BrokerConnection, - request: BrokerRequest, - ) -> BrokerDispatch { - if connection.state == ConnectionState::AwaitingNegotiation { - return match request { - BrokerRequest::Negotiate { protocol_version } => { - Self::handle_negotiation(connection, protocol_version) - } - _ => BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ), - }; - } - - match request { - BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ), - BrokerRequest::CreateEvent => BrokerDispatch::continue_after(Self::handle_result( - self.create_event(connection.association), - BrokerResponse::Handle, - )), - BrokerRequest::WaitEvent { handle } => { - BrokerDispatch::continue_after(Self::handle_result( - self.wait_event(connection.association, handle), - BrokerResponse::Wait, - )) - } - BrokerRequest::SignalEvent { handle } => { - BrokerDispatch::continue_after(Self::handle_result( - self.signal_event(connection.association, handle), - BrokerResponse::Readiness, - )) - } - _ => BrokerDispatch::continue_after(BrokerResponse::Error( - ErrorCode::UnsupportedOperation, - )), - } - } - - /// Handles a well-formed frame with a request tag not known to this broker. - /// - /// Unknown future operations are regular feature-probing failures after - /// negotiation. Before negotiation they are protocol-state violations. - pub fn handle_unknown_connection_request( - &mut self, - connection: &mut BrokerConnection, - _tag: u8, - ) -> BrokerDispatch { - if connection.state == ConnectionState::AwaitingNegotiation { - BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::ProtocolState), - CloseReason::ProtocolViolation, - ) - } else { - BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)) - } - } - - fn handle_negotiation( - connection: &mut BrokerConnection, - protocol_version: ProtocolVersion, - ) -> BrokerDispatch { - if protocol_version.is_supported_by(SUPPORTED_PROTOCOL_VERSION) { - connection.state = ConnectionState::Active; - BrokerDispatch::continue_after(BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, - }) - } else { - BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::UnsupportedVersion), - CloseReason::UnsupportedVersion, - ) - } - } - - fn handle_result( - result: Result, - into_response: impl FnOnce(T) -> BrokerResponse, - ) -> BrokerResponse { - match result { - Ok(value) => into_response(value), - Err(error) => BrokerResponse::Error(error), - } - } -} - #[cfg(test)] mod tests { use super::*; use crate::{BrokerCore, EventOnlyPolicy}; - use litebox_broker_protocol::{ReadinessState, WaitOutcome}; - - #[test] - fn dispatch_requires_negotiation_first() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - - let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); - - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert!(dispatch.close_after_response()); - assert_eq!( - dispatch.close_reason(), - Some(CloseReason::ProtocolViolation) - ); - assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); - } - - #[test] - fn dispatch_closes_after_post_negotiation_negotiate() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - negotiate(&mut core, &mut connection); - - let dispatch = core.handle_connection_request( - &mut connection, - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ); - - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert!(dispatch.close_after_response()); - assert_eq!( - dispatch.close_reason(), - Some(CloseReason::ProtocolViolation) - ); - } - - #[test] - fn dispatch_closes_after_unsupported_version() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - - let dispatch = core.handle_connection_request( - &mut connection, - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(SUPPORTED_PROTOCOL_VERSION.major + 1, 0), - }, - ); - - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::UnsupportedVersion) - ); - assert!(dispatch.close_after_response()); - assert_eq!( - dispatch.close_reason(), - Some(CloseReason::UnsupportedVersion) - ); - assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); - } - - #[test] - fn dispatch_accepts_supported_older_minor_version() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - let requested = ProtocolVersion::new( - SUPPORTED_PROTOCOL_VERSION.major, - SUPPORTED_PROTOCOL_VERSION.minor - 1, - ); - - let dispatch = core.handle_connection_request( - &mut connection, - BrokerRequest::Negotiate { - protocol_version: requested, - }, - ); - - assert_eq!( - dispatch.response(), - BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } - ); - assert!(!dispatch.close_after_response()); - assert_eq!(connection.state, ConnectionState::Active); - } - - #[test] - fn dispatch_rejects_newer_minor_version() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - - let dispatch = core.handle_connection_request( - &mut connection, - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new( - SUPPORTED_PROTOCOL_VERSION.major, - SUPPORTED_PROTOCOL_VERSION.minor + 1, - ), - }, - ); - - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::UnsupportedVersion) - ); - assert!(dispatch.close_after_response()); - assert_eq!( - dispatch.close_reason(), - Some(CloseReason::UnsupportedVersion) - ); - assert_eq!(connection.state, ConnectionState::AwaitingNegotiation); - } - - #[test] - fn dispatch_reports_unknown_requests_without_closing() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - negotiate(&mut core, &mut connection); - - let dispatch = core.handle_unknown_connection_request(&mut connection, 0xff); - - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::UnsupportedOperation) - ); - assert_eq!(dispatch.close_reason(), None); - } #[test] - fn dispatch_closes_unknown_requests_before_negotiation() { + fn create_connection_records_caller_credential() { let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - - let dispatch = core.handle_unknown_connection_request(&mut connection, 0xff); - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.close_reason(), - Some(CloseReason::ProtocolViolation) - ); - } - - #[test] - fn dispatch_negotiates_then_routes_event_requests() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) + let connection = core + .create_connection(CallerCredential::Unauthenticated) .unwrap(); - let dispatch = core.handle_connection_request( - &mut connection, - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ); assert_eq!( - dispatch.response(), - BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } + connection.caller_credential(), + CallerCredential::Unauthenticated ); - assert!(!dispatch.close_after_response()); - assert_eq!(connection.state, ConnectionState::Active); - - let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); - let handle = match dispatch.response() { - BrokerResponse::Handle(handle) => handle, - response => panic!("unexpected response: {response:?}"), - }; - - let dispatch = - core.handle_connection_request(&mut connection, BrokerRequest::WaitEvent { handle }); - assert_eq!( - dispatch.response(), - BrokerResponse::Wait(WaitOutcome::WouldBlock(ReadinessState::new(false, 0))) - ); - assert!(!dispatch.close_after_response()); - } - - #[test] - fn dispatch_rejects_handle_from_another_connection() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut owner = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - let mut other = core - .create_connection(PeerCredential::Unauthenticated) - .unwrap(); - negotiate(&mut core, &mut owner); - negotiate(&mut core, &mut other); - - let dispatch = core.handle_connection_request(&mut owner, BrokerRequest::CreateEvent); - let handle = match dispatch.response() { - BrokerResponse::Handle(handle) => handle, - response => panic!("unexpected response: {response:?}"), - }; - - let dispatch = - core.handle_connection_request(&mut other, BrokerRequest::WaitEvent { handle }); - assert_eq!( - dispatch.response(), - BrokerResponse::Error(ErrorCode::InvalidRights) - ); - assert!(!dispatch.close_after_response()); } #[test] fn close_connection_releases_owned_references_and_orphaned_objects() { let mut core = BrokerCore::new(EventOnlyPolicy); - let mut connection = core - .create_connection(PeerCredential::Unauthenticated) + let connection = core + .create_connection(CallerCredential::Unauthenticated) .unwrap(); - negotiate(&mut core, &mut connection); - let dispatch = core.handle_connection_request(&mut connection, BrokerRequest::CreateEvent); - assert!(matches!(dispatch.response(), BrokerResponse::Handle(_))); + let _handle = core.create_event(&connection).unwrap(); assert_eq!(core.references.len(), 1); assert_eq!(core.objects.len(), 1); @@ -473,20 +78,4 @@ mod tests { assert!(core.references.is_empty()); assert!(core.objects.is_empty()); } - - fn negotiate(core: &mut BrokerCore

, connection: &mut BrokerConnection) { - let dispatch = core.handle_connection_request( - connection, - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ); - assert_eq!( - dispatch.response(), - BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } - ); - assert_eq!(connection.state, ConnectionState::Active); - } } diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs new file mode 100644 index 000000000..8600d5895 --- /dev/null +++ b/litebox_broker_core/src/error.rs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; + +/// Broker authority error category. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +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, +} + +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"), + } + } +} + +impl core::error::Error for BrokerError {} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index bcac9f2b4..033fd199b 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -1,14 +1,41 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::identity::Association; use crate::object::ObjectKind; -use crate::{BrokerCore, ObjectRights, ObjectType, PolicyEngine, Result}; -use litebox_broker_protocol::{ErrorCode, ObjectHandle, ReadinessState, WaitOutcome}; +use crate::{ + BrokerConnection, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, + PolicyEngine, Result, +}; + +/// Event readiness snapshot. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ReadinessState { + /// Whether the event is currently ready. + pub ready: bool, + /// Monotonic generation incremented on readiness changes. + pub generation: u64, +} + +impl ReadinessState { + /// Creates a readiness snapshot. + pub const fn new(ready: bool, generation: u64) -> Self { + Self { ready, generation } + } +} + +/// Result of checking an event wait. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum WaitOutcome { + /// The event was ready and a nonblocking wait would complete. + Ready(ReadinessState), + /// The event was not ready and a blocking wait would sleep. + WouldBlock(ReadinessState), +} impl BrokerCore

{ /// Creates a broker-owned event object. - pub(crate) fn create_event(&mut self, association: Association) -> Result { + pub fn create_event(&mut self, connection: &BrokerConnection) -> Result { + let association = connection.association(); self.authorize_create_object(association, ObjectType::Event)?; self.insert_object_with_reference( @@ -24,11 +51,12 @@ impl BrokerCore

{ /// Blocking is intentionally outside BrokerCore for the first proof of /// concept. Userland or kernel deployments can block on transport-specific /// wait primitives after BrokerCore authorizes and reports readiness state. - pub(crate) fn wait_event( + pub fn wait_event( &mut self, - association: Association, + connection: &BrokerConnection, handle: ObjectHandle, ) -> Result { + let association = connection.association(); self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WAIT)?; let state = self.event_state(handle)?; Ok(if state.ready { @@ -39,11 +67,12 @@ impl BrokerCore

{ } /// Signals a broker-owned event object. - pub(crate) fn signal_event( + pub fn signal_event( &mut self, - association: Association, + connection: &BrokerConnection, handle: ObjectHandle, ) -> Result { + let association = connection.association(); self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WRITE)?; match &mut self.object_mut(handle.object_id)?.kind { ObjectKind::Event(event) => event.signal(), @@ -80,7 +109,7 @@ impl EventObject { self.readiness_generation = self .readiness_generation .checked_add(1) - .ok_or(ErrorCode::ResourceExhausted)?; + .ok_or(BrokerError::ResourceExhausted)?; Ok(self.readiness_state()) } } @@ -88,19 +117,17 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerCore, EventOnlyPolicy}; - use litebox_broker_protocol::ObjectGeneration; - use litebox_broker_transport::PeerCredential; + use crate::{BrokerCore, CallerCredential, EventOnlyPolicy, ObjectGeneration}; #[test] fn wait_rejects_reference_without_wait_right() { let mut core = BrokerCore::new(EventOnlyPolicy); - let association = core - .create_association(PeerCredential::Unauthenticated) + let connection = core + .create_connection(CallerCredential::Unauthenticated) .unwrap(); let handle = core .insert_object_with_reference( - association, + connection.association(), ObjectKind::Event(EventObject::new()), ObjectType::Event, ObjectRights::WRITE, @@ -108,23 +135,23 @@ mod tests { .unwrap(); assert_eq!( - core.wait_event(association, handle), - Err(ErrorCode::InvalidRights) + core.wait_event(&connection, handle), + Err(BrokerError::InvalidRights) ); } #[test] fn wait_rejects_stale_object_generation() { let mut core = BrokerCore::new(EventOnlyPolicy); - let association = core - .create_association(PeerCredential::Unauthenticated) + let connection = core + .create_connection(CallerCredential::Unauthenticated) .unwrap(); - let mut handle = core.create_event(association).unwrap(); + let mut handle = core.create_event(&connection).unwrap(); handle.object_generation = ObjectGeneration::new(handle.object_generation.get() + 1); assert_eq!( - core.wait_event(association, handle), - Err(ErrorCode::StaleHandle) + core.wait_event(&connection, handle), + Err(BrokerError::StaleHandle) ); } @@ -132,16 +159,16 @@ mod tests { fn wait_rejects_handle_owned_by_another_association() { let mut core = BrokerCore::new(EventOnlyPolicy); let owner = core - .create_association(PeerCredential::Unauthenticated) + .create_connection(CallerCredential::Unauthenticated) .unwrap(); let other = core - .create_association(PeerCredential::Unauthenticated) + .create_connection(CallerCredential::Unauthenticated) .unwrap(); - let handle = core.create_event(owner).unwrap(); + let handle = core.create_event(&owner).unwrap(); assert_eq!( - core.wait_event(other, handle), - Err(ErrorCode::InvalidRights) + core.wait_event(&other, handle), + Err(BrokerError::InvalidRights) ); } } diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 822eb54ff..f76b56fd6 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -2,7 +2,18 @@ // Licensed under the MIT license. use crate::{BrokerCore, Result, allocate_id}; -use litebox_broker_transport::PeerCredential; + +/// 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 connection-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, +} macro_rules! id_type { ($(#[$meta:meta])* $name:ident) => { @@ -33,19 +44,19 @@ id_type! { ProcessId } -/// Broker-assigned identity bound to one authenticated transport association. +/// Broker-assigned identity bound to one authenticated caller association. /// -/// User mode does not choose this value. The userland broker transport or a -/// future BrokerHost authenticates the peer, then BrokerCore assigns this -/// identity for all requests received on that 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(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct Association { /// Broker-assigned sandbox session identity. session_id: SessionId, /// Broker-assigned guest process identity. process_id: ProcessId, - /// Transport-authenticated peer credential for this association. - peer_credential: PeerCredential, + /// Broker-entry-authenticated caller credential for this association. + caller_credential: CallerCredential, } impl Association { @@ -53,25 +64,25 @@ impl Association { pub(crate) const fn new( session_id: SessionId, process_id: ProcessId, - peer_credential: PeerCredential, + caller_credential: CallerCredential, ) -> Self { Self { session_id, process_id, - peer_credential, + caller_credential, } } - pub(crate) const fn peer_credential(self) -> PeerCredential { - self.peer_credential + pub(crate) const fn caller_credential(self) -> CallerCredential { + self.caller_credential } } impl

BrokerCore

{ - /// Allocates a transport-bound broker association for one process connection. + /// Allocates a broker association for one process connection. pub(crate) fn create_association( &mut self, - peer_credential: PeerCredential, + caller_credential: CallerCredential, ) -> Result { let process_id = allocate_id(&mut self.next_process_id)?; // The POC models one sandbox session per BrokerCore; multi-session @@ -79,7 +90,7 @@ impl

BrokerCore

{ let association = Association::new( SessionId::FIRST, ProcessId::new(process_id), - peer_credential, + caller_credential, ); Ok(association) } @@ -88,26 +99,28 @@ impl

BrokerCore

{ #[cfg(test)] mod tests { use super::*; - use crate::DefaultDenyPolicy; - use litebox_broker_protocol::ErrorCode; + use crate::{BrokerError, DefaultDenyPolicy}; #[test] fn create_association_uses_one_session_and_distinct_processes() { let mut core = BrokerCore::new(DefaultDenyPolicy); let first = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); let second = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); assert_eq!(first.session_id, SessionId::FIRST); assert_eq!(second.session_id, SessionId::FIRST); assert_eq!(first.process_id, ProcessId::new(1)); assert_eq!(second.process_id, ProcessId::new(2)); - assert_eq!(first.peer_credential(), PeerCredential::Unauthenticated); - assert_eq!(second.peer_credential(), PeerCredential::Unauthenticated); + assert_eq!(first.caller_credential(), CallerCredential::Unauthenticated); + assert_eq!( + second.caller_credential(), + CallerCredential::Unauthenticated + ); } #[test] @@ -116,14 +129,14 @@ mod tests { core.next_process_id = u64::MAX; let association = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); assert_eq!(association.process_id, ProcessId::new(u64::MAX)); assert_eq!(association.session_id, SessionId::FIRST); assert_eq!(core.next_process_id, 0); assert_eq!( - core.create_association(PeerCredential::Unauthenticated), - Err(ErrorCode::ResourceExhausted) + core.create_association(CallerCredential::Unauthenticated), + Err(BrokerError::ResourceExhausted) ); assert_eq!(core.next_process_id, 0); } diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 3a644b48e..d4ab05bb8 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -1,11 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Transport-independent broker authority core. +//! Protocol- and transport-independent broker authority core. //! //! `litebox_broker_core` owns broker-side object identity, reference lifetime, //! rights checks, generation checks, and policy calls. It deliberately has no -//! dependency on Unix sockets, shared-memory rings, or any other transport. +//! dependency on protocol request/response types, Unix sockets, shared-memory +//! rings, or any other transport. #![no_std] @@ -14,6 +15,7 @@ extern crate alloc; extern crate std; mod connection; +mod error; mod event; mod identity; mod object; @@ -22,19 +24,21 @@ mod types; use alloc::collections::BTreeMap; -pub use connection::{BrokerConnection, BrokerDispatch, CloseReason, DispatchOutcome}; -use litebox_broker_protocol::{ErrorCode, ObjectId, ObjectReferenceId, ProtocolVersion}; +pub use connection::BrokerConnection; +pub use error::BrokerError; +pub use event::{ReadinessState, WaitOutcome}; +pub use identity::CallerCredential; use object::{ObjectEntry, ObjectReference}; +pub use object::{ + ObjectGeneration, ObjectHandle, ObjectId, ObjectReferenceGeneration, ObjectReferenceId, +}; pub use policy::{ DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyEngine, PolicyOperation, }; pub use types::{ObjectRights, ObjectType}; /// BrokerCore result type. -pub type Result = core::result::Result; - -/// Protocol version this broker core implementation supports. -pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); +pub type Result = core::result::Result; /// Transport-independent broker authority state. pub struct BrokerCore

{ @@ -64,7 +68,7 @@ const EXHAUSTED_ID: u64 = 0; fn allocate_id(next_id: &mut u64) -> Result { if *next_id == EXHAUSTED_ID { - return Err(ErrorCode::ResourceExhausted); + return Err(BrokerError::ResourceExhausted); } let id = *next_id; diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 78fcb11f5..5caba7044 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -6,13 +6,84 @@ use alloc::vec::Vec; use crate::event::EventObject; use crate::identity::Association; use crate::{ - BrokerCore, ObjectRights, ObjectType, PolicyEngine, PolicyOperation, Result, allocate_id, -}; -use litebox_broker_protocol::{ - ErrorCode, ObjectGeneration, ObjectHandle, ObjectId, ObjectReferenceGeneration, - ObjectReferenceId, + BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, PolicyOperation, Result, + allocate_id, }; +macro_rules! id_type { + ($(#[$meta:meta])* $name:ident) => { + $(#[$meta])* + #[repr(transparent)] + #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct $name(u64); + + impl $name { + /// Creates an identifier from its raw value. + pub const fn new(raw: u64) -> Self { + Self(raw) + } + + /// Returns the raw identifier value. + pub const fn get(self) -> u64 { + self.0 + } + } + }; +} + +id_type! { + /// Broker-owned object identifier. + ObjectId +} + +id_type! { + /// Generation attached to a broker object. + ObjectGeneration +} + +id_type! { + /// Broker-owned object reference identifier. + ObjectReferenceId +} + +id_type! { + /// Generation attached to a broker object reference. + ObjectReferenceGeneration +} + +/// Broker-owned object handle returned by BrokerCore. +/// +/// UserLiteBox may cache this value, but the broker remains authoritative for +/// object lifetime, reference lifetime, type, rights, and generation. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct ObjectHandle { + /// Opaque broker object identifier. + pub object_id: ObjectId, + /// Object generation used to reject stale handles after object-slot reuse. + pub object_generation: ObjectGeneration, + /// 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( + object_id: ObjectId, + object_generation: ObjectGeneration, + reference_id: ObjectReferenceId, + reference_generation: ObjectReferenceGeneration, + ) -> Self { + Self { + object_id, + object_generation, + reference_id, + reference_generation, + } + } +} + const FIRST_OBJECT_GENERATION: ObjectGeneration = ObjectGeneration::new(1); const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); @@ -103,7 +174,7 @@ impl BrokerCore

{ object_type: ObjectType, ) -> Result<()> { self.policy.authorize(PolicyOperation::create_object( - association.peer_credential(), + association.caller_credential(), object_type, )) } @@ -117,20 +188,22 @@ impl BrokerCore

{ ) -> Result<()> { self.validate_handle(association, handle, object_type, rights)?; self.policy.authorize(PolicyOperation::use_object( - association.peer_credential(), + association.caller_credential(), object_type, rights, )) } pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { - self.objects.get(&object_id).ok_or(ErrorCode::UnknownObject) + 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(ErrorCode::UnknownObject) + .ok_or(BrokerError::UnknownObject) } fn validate_handle( @@ -143,34 +216,34 @@ impl BrokerCore

{ let reference = self .references .get(&handle.reference_id) - .ok_or(ErrorCode::UnknownObject)?; + .ok_or(BrokerError::UnknownObject)?; debug_assert_eq!(reference.reference_generation, FIRST_REFERENCE_GENERATION); if reference.owner != association { - return Err(ErrorCode::InvalidRights); + return Err(BrokerError::InvalidRights); } if reference.reference_generation != handle.reference_generation || reference.object_generation != handle.object_generation || reference.object_id != handle.object_id { - return Err(ErrorCode::StaleHandle); + return Err(BrokerError::StaleHandle); } if reference.object_type != expected_type { - return Err(ErrorCode::WrongObjectType); + return Err(BrokerError::WrongObjectType); } if !reference.rights.contains(required_rights) { - return Err(ErrorCode::InvalidRights); + return Err(BrokerError::InvalidRights); } let object = self .objects .get(&reference.object_id) - .ok_or(ErrorCode::UnknownObject)?; + .ok_or(BrokerError::UnknownObject)?; debug_assert_eq!(reference.object_generation, object.generation); if object.generation != handle.object_generation { - return Err(ErrorCode::StaleHandle); + return Err(BrokerError::StaleHandle); } if object.kind.object_type() != expected_type { - return Err(ErrorCode::WrongObjectType); + return Err(BrokerError::WrongObjectType); } Ok(()) @@ -220,14 +293,13 @@ impl

BrokerCore

{ #[cfg(test)] mod tests { use super::*; - use crate::DefaultDenyPolicy; - use litebox_broker_transport::PeerCredential; + use crate::{BrokerError, CallerCredential, DefaultDenyPolicy}; #[test] fn object_and_reference_allocators_issue_max_id_then_exhaust() { let mut core = BrokerCore::new(DefaultDenyPolicy); let association = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); core.next_object_id = u64::MAX; core.next_reference_id = u64::MAX; @@ -252,7 +324,7 @@ mod tests { ObjectType::Event, ObjectRights::WAIT, ), - Err(ErrorCode::ResourceExhausted) + Err(BrokerError::ResourceExhausted) ); } @@ -260,7 +332,7 @@ mod tests { fn validate_handle_rejects_reference_object_generation_mismatch() { let mut core = BrokerCore::new(DefaultDenyPolicy); let association = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = core .insert_object_with_reference( @@ -278,7 +350,7 @@ mod tests { assert_eq!( core.validate_handle(association, handle, ObjectType::Event, ObjectRights::WAIT), - Err(ErrorCode::StaleHandle) + Err(BrokerError::StaleHandle) ); } @@ -286,7 +358,7 @@ mod tests { fn validate_handle_rejects_stale_reference_generation() { let mut core = BrokerCore::new(DefaultDenyPolicy); let association = core - .create_association(PeerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = core .insert_object_with_reference( @@ -310,7 +382,7 @@ mod tests { ObjectType::Event, ObjectRights::WAIT ), - Err(ErrorCode::StaleHandle) + Err(BrokerError::StaleHandle) ); } } diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 53e1155ca..17c4836d2 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -1,9 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{ObjectRights, ObjectType}; -use litebox_broker_protocol::ErrorCode; -use litebox_broker_transport::PeerCredential; +use crate::{BrokerError, CallerCredential, ObjectRights, ObjectType}; /// Broker operation submitted to the policy engine. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -11,8 +9,8 @@ use litebox_broker_transport::PeerCredential; pub enum PolicyOperation { /// Perform an operation on a broker-owned object type. Object { - /// Transport-authenticated peer credential for the caller. - peer_credential: PeerCredential, + /// Broker-entry-authenticated credential for the caller. + caller_credential: CallerCredential, object_type: ObjectType, operation: ObjectOperation, }, @@ -30,9 +28,12 @@ pub enum ObjectOperation { impl PolicyOperation { /// Creates a policy operation for creating a broker-owned object type. - pub const fn create_object(peer_credential: PeerCredential, object_type: ObjectType) -> Self { + pub const fn create_object( + caller_credential: CallerCredential, + object_type: ObjectType, + ) -> Self { Self::Object { - peer_credential, + caller_credential, object_type, operation: ObjectOperation::Create, } @@ -40,12 +41,12 @@ impl PolicyOperation { /// Creates a policy operation for using a broker-owned object with rights. pub const fn use_object( - peer_credential: PeerCredential, + caller_credential: CallerCredential, object_type: ObjectType, rights: ObjectRights, ) -> Self { Self::Object { - peer_credential, + caller_credential, object_type, operation: ObjectOperation::Use { rights }, } @@ -55,7 +56,7 @@ impl PolicyOperation { /// Broker policy decision interface. pub trait PolicyEngine { /// Authorizes or denies a broker operation. - fn authorize(&mut self, operation: PolicyOperation) -> Result<(), ErrorCode>; + fn authorize(&mut self, operation: PolicyOperation) -> Result<(), BrokerError>; } /// Policy engine that denies every operation. @@ -63,8 +64,8 @@ pub trait PolicyEngine { pub struct DefaultDenyPolicy; impl PolicyEngine for DefaultDenyPolicy { - fn authorize(&mut self, _operation: PolicyOperation) -> Result<(), ErrorCode> { - Err(ErrorCode::PolicyDenied) + fn authorize(&mut self, _operation: PolicyOperation) -> Result<(), BrokerError> { + Err(BrokerError::PolicyDenied) } } @@ -78,22 +79,22 @@ impl PolicyEngine for DefaultDenyPolicy { pub struct EventOnlyPolicy; impl PolicyEngine for EventOnlyPolicy { - fn authorize(&mut self, operation: PolicyOperation) -> Result<(), ErrorCode> { + fn authorize(&mut self, operation: PolicyOperation) -> Result<(), BrokerError> { match operation { PolicyOperation::Object { - peer_credential: PeerCredential::Unauthenticated, + caller_credential: CallerCredential::Unauthenticated, object_type: ObjectType::Event, operation: ObjectOperation::Create, } => Ok(()), PolicyOperation::Object { - peer_credential: PeerCredential::Unauthenticated, + caller_credential: CallerCredential::Unauthenticated, object_type: ObjectType::Event, operation: ObjectOperation::Use { rights }, } if rights == ObjectRights::WAIT || rights == ObjectRights::WRITE => Ok(()), PolicyOperation::Object { object_type: ObjectType::Event, .. - } => Err(ErrorCode::PolicyDenied), + } => Err(BrokerError::PolicyDenied), } } } @@ -101,7 +102,6 @@ impl PolicyEngine for EventOnlyPolicy { #[cfg(test)] mod tests { use super::*; - use litebox_broker_transport::PeerCredential; #[test] fn event_only_policy_allows_only_current_event_surface() { @@ -109,14 +109,14 @@ mod tests { assert_eq!( policy.authorize(PolicyOperation::create_object( - PeerCredential::Unauthenticated, + CallerCredential::Unauthenticated, ObjectType::Event )), Ok(()) ); assert_eq!( policy.authorize(PolicyOperation::use_object( - PeerCredential::Unauthenticated, + CallerCredential::Unauthenticated, ObjectType::Event, ObjectRights::WAIT )), @@ -124,7 +124,7 @@ mod tests { ); assert_eq!( policy.authorize(PolicyOperation::use_object( - PeerCredential::Unauthenticated, + CallerCredential::Unauthenticated, ObjectType::Event, ObjectRights::WRITE )), @@ -132,11 +132,11 @@ mod tests { ); assert_eq!( policy.authorize(PolicyOperation::use_object( - PeerCredential::Unauthenticated, + CallerCredential::Unauthenticated, ObjectType::Event, ObjectRights::WAIT | ObjectRights::WRITE )), - Err(ErrorCode::PolicyDenied) + Err(BrokerError::PolicyDenied) ); } } diff --git a/litebox_broker_server/Cargo.toml b/litebox_broker_server/Cargo.toml index 41b0b66ff..311d07d05 100644 --- a/litebox_broker_server/Cargo.toml +++ b/litebox_broker_server/Cargo.toml @@ -5,6 +5,7 @@ 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" } litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } [lints] diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_server/src/lib.rs index 2ccba1630..a6edeabfc 100644 --- a/litebox_broker_server/src/lib.rs +++ b/litebox_broker_server/src/lib.rs @@ -11,4 +11,7 @@ mod server; -pub use server::{BrokerServeError, ConnectionTermination, serve_connection}; +pub use server::{ + BrokerServeError, CloseReason, ConnectionTermination, SUPPORTED_PROTOCOL_VERSION, + serve_connection, +}; diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index d58e261ec..fd97903ee 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -4,9 +4,23 @@ use core::fmt; use litebox_broker_core::{ - BrokerConnection, BrokerCore, CloseReason, DispatchOutcome, PolicyEngine, + BrokerConnection, BrokerCore, BrokerError, CallerCredential, + ObjectGeneration as CoreObjectGeneration, ObjectHandle as CoreObjectHandle, + ObjectId as CoreObjectId, ObjectReferenceGeneration as CoreObjectReferenceGeneration, + ObjectReferenceId as CoreObjectReferenceId, PolicyEngine, ReadinessState as CoreReadinessState, + WaitOutcome as CoreWaitOutcome, }; -use litebox_broker_transport::{ReceivedRequest, ServerTransport}; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ErrorCode, ObjectGeneration as ProtocolObjectGeneration, + ObjectHandle as ProtocolObjectHandle, ObjectId as ProtocolObjectId, + ObjectReferenceGeneration as ProtocolObjectReferenceGeneration, + ObjectReferenceId as ProtocolObjectReferenceId, ProtocolVersion, + ReadinessState as ProtocolReadinessState, WaitOutcome as ProtocolWaitOutcome, +}; +use litebox_broker_transport::{PeerCredential, ReceivedRequest, ServerTransport}; + +/// Protocol version this broker server implementation supports. +pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); /// Serves one broker connection over the provided connected transport. pub fn serve_connection( @@ -20,11 +34,13 @@ where let peer_credential = transport .peer_credential() .map_err(BrokerServeError::Transport)?; - let mut connection = core - .create_connection(peer_credential) + let caller_credential = caller_credential_from_peer(peer_credential) + .map_err(|()| BrokerServeError::ConnectionSetup)?; + let connection = core + .create_connection(caller_credential) .map_err(|_error| BrokerServeError::ConnectionSetup)?; - let result = serve_request_loop(core, transport, &mut connection); + let result = serve_request_loop(core, transport, &connection); core.close_connection(connection); result } @@ -32,12 +48,13 @@ where fn serve_request_loop( core: &mut BrokerCore

, transport: &mut T, - connection: &mut BrokerConnection, + connection: &BrokerConnection, ) -> Result> where P: PolicyEngine, T: ServerTransport, { + let mut state = ConnectionState::AwaitingNegotiation; loop { let Some(received) = transport .recv_request() @@ -46,18 +63,11 @@ where break; }; - let dispatch = match received { - ReceivedRequest::Request(request) => { - core.handle_connection_request(connection, request) - } - ReceivedRequest::Unknown { tag } => { - core.handle_unknown_connection_request(connection, tag) - } - }; + let dispatch = handle_received_request(core, connection, &mut state, received); transport - .send_response(&dispatch.response()) + .send_response(&dispatch.response) .map_err(BrokerServeError::Transport)?; - if let DispatchOutcome::Close(reason) = dispatch.outcome() { + if let DispatchOutcome::Close(reason) = dispatch.outcome { return Ok(ConnectionTermination::BrokerClosed(reason)); } } @@ -65,19 +75,214 @@ where Ok(ConnectionTermination::PeerClosed) } +fn caller_credential_from_peer(peer_credential: PeerCredential) -> Result { + if peer_credential == PeerCredential::Unauthenticated { + Ok(CallerCredential::Unauthenticated) + } else { + Err(()) + } +} + +fn handle_received_request( + core: &mut BrokerCore

, + connection: &BrokerConnection, + state: &mut ConnectionState, + received: ReceivedRequest, +) -> BrokerDispatch { + match received { + ReceivedRequest::Request(request) => handle_request(core, connection, state, request), + ReceivedRequest::Unknown { tag: _ } => handle_unknown_request(*state), + } +} + +fn handle_request( + core: &mut BrokerCore

, + connection: &BrokerConnection, + state: &mut ConnectionState, + request: BrokerRequest, +) -> BrokerDispatch { + if *state == ConnectionState::AwaitingNegotiation { + return match request { + BrokerRequest::Negotiate { protocol_version } => { + handle_negotiation(state, protocol_version) + } + _ => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + }; + } + + match request { + BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::ProtocolState), + CloseReason::ProtocolViolation, + ), + BrokerRequest::CreateEvent => BrokerDispatch::continue_after(handle_core_result( + core.create_event(connection), + |handle| BrokerResponse::Handle(protocol_handle(handle)), + )), + BrokerRequest::WaitEvent { handle } => BrokerDispatch::continue_after(handle_core_result( + core.wait_event(connection, core_handle(handle)), + |outcome| BrokerResponse::Wait(protocol_wait_outcome(outcome)), + )), + BrokerRequest::SignalEvent { handle } => { + BrokerDispatch::continue_after(handle_core_result( + core.signal_event(connection, core_handle(handle)), + |readiness| BrokerResponse::Readiness(protocol_readiness_state(readiness)), + )) + } + _ => BrokerDispatch::continue_after(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 handle_negotiation( + state: &mut ConnectionState, + protocol_version: ProtocolVersion, +) -> BrokerDispatch { + if protocol_version.is_supported_by(SUPPORTED_PROTOCOL_VERSION) { + *state = ConnectionState::Active; + BrokerDispatch::continue_after(BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, + }) + } else { + BrokerDispatch::close_after( + BrokerResponse::Error(ErrorCode::UnsupportedVersion), + CloseReason::UnsupportedVersion, + ) + } +} + +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(protocol_error(error)), + } +} + +fn 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, + } +} + +fn core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { + CoreObjectHandle::new( + CoreObjectId::new(handle.object_id.get()), + CoreObjectGeneration::new(handle.object_generation.get()), + CoreObjectReferenceId::new(handle.reference_id.get()), + CoreObjectReferenceGeneration::new(handle.reference_generation.get()), + ) +} + +fn protocol_handle(handle: CoreObjectHandle) -> ProtocolObjectHandle { + ProtocolObjectHandle::new( + ProtocolObjectId::new(handle.object_id.get()), + ProtocolObjectGeneration::new(handle.object_generation.get()), + ProtocolObjectReferenceId::new(handle.reference_id.get()), + ProtocolObjectReferenceGeneration::new(handle.reference_generation.get()), + ) +} + +fn protocol_readiness_state(readiness: CoreReadinessState) -> ProtocolReadinessState { + ProtocolReadinessState::new(readiness.ready, readiness.generation) +} + +fn protocol_wait_outcome(outcome: CoreWaitOutcome) -> ProtocolWaitOutcome { + match outcome { + CoreWaitOutcome::Ready(readiness) => { + ProtocolWaitOutcome::Ready(protocol_readiness_state(readiness)) + } + CoreWaitOutcome::WouldBlock(readiness) => { + ProtocolWaitOutcome::WouldBlock(protocol_readiness_state(readiness)) + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum ConnectionState { + AwaitingNegotiation, + Active, +} + +#[derive(Clone, Copy, 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 server closed the connection after sending a response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CloseReason { + /// The peer requested an unsupported protocol version. + UnsupportedVersion, + /// 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::UnsupportedVersion => f.write_str("unsupported protocol version"), + Self::ProtocolViolation => f.write_str("protocol violation"), + } + } +} + /// Terminal outcome for a successfully served broker connection. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ConnectionTermination { /// The peer cleanly closed the transport. PeerClosed, - /// BrokerCore sent a terminal protocol response and closed the connection. + /// The server sent a terminal protocol response and closed the connection. BrokerClosed(CloseReason), } /// Errors returned by a broker receive/send loop. #[derive(Debug)] pub enum BrokerServeError { - /// BrokerCore could not allocate state for a new connection. + /// The server could not allocate or map state for a new connection. ConnectionSetup, /// The concrete transport failed. Transport(E), @@ -103,3 +308,314 @@ where } } } + +#[cfg(test)] +mod tests { + use super::*; + use litebox_broker_core::EventOnlyPolicy; + + #[test] + fn dispatch_requires_negotiation_first() { + let (mut core, connection, mut state) = new_connection(); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::CreateEvent, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::ProtocolViolation) + ); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_closes_after_post_negotiation_negotiate() { + let (mut core, connection, mut state) = new_connection(); + negotiate(&mut core, &connection, &mut state); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::ProtocolViolation) + ); + } + + #[test] + fn dispatch_closes_after_unsupported_version() { + let (mut core, connection, mut state) = new_connection(); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(SUPPORTED_PROTOCOL_VERSION.major + 1, 0), + }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::UnsupportedVersion) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::UnsupportedVersion) + ); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_accepts_supported_older_minor_version() { + let (mut core, connection, mut state) = new_connection(); + let requested = ProtocolVersion::new( + SUPPORTED_PROTOCOL_VERSION.major, + SUPPORTED_PROTOCOL_VERSION.minor - 1, + ); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::Negotiate { + protocol_version: requested, + }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + assert_eq!(state, ConnectionState::Active); + } + + #[test] + fn dispatch_rejects_newer_minor_version() { + let (mut core, connection, mut state) = new_connection(); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new( + SUPPORTED_PROTOCOL_VERSION.major, + SUPPORTED_PROTOCOL_VERSION.minor + 1, + ), + }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::UnsupportedVersion) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::UnsupportedVersion) + ); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + } + + #[test] + fn dispatch_reports_unknown_requests_without_closing() { + let (mut core, connection, mut state) = new_connection(); + negotiate(&mut core, &connection, &mut state); + + let dispatch = handle_received_request( + &mut core, + &connection, + &mut state, + ReceivedRequest::Unknown { tag: 0xff }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::UnsupportedOperation) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + } + + #[test] + fn dispatch_closes_unknown_requests_before_negotiation() { + let (mut core, connection, mut state) = new_connection(); + + let dispatch = handle_received_request( + &mut core, + &connection, + &mut state, + ReceivedRequest::Unknown { tag: 0xff }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::ProtocolViolation) + ); + } + + #[test] + fn dispatch_negotiates_then_routes_event_requests() { + let (mut core, connection, mut state) = new_connection(); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + assert_eq!( + dispatch.response, + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + assert_eq!(state, ConnectionState::Active); + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::CreateEvent, + ); + let handle = match dispatch.response { + BrokerResponse::Handle(handle) => handle, + response => panic!("unexpected response: {response:?}"), + }; + + let dispatch = handle_request( + &mut core, + &connection, + &mut state, + BrokerRequest::WaitEvent { handle }, + ); + assert_eq!( + dispatch.response, + BrokerResponse::Wait(ProtocolWaitOutcome::WouldBlock( + ProtocolReadinessState::new(false, 0) + )) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + } + + #[test] + fn dispatch_rejects_handle_from_another_connection() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let owner = core + .create_connection(CallerCredential::Unauthenticated) + .unwrap(); + let other = core + .create_connection(CallerCredential::Unauthenticated) + .unwrap(); + let mut owner_state = ConnectionState::AwaitingNegotiation; + let mut other_state = ConnectionState::AwaitingNegotiation; + negotiate(&mut core, &owner, &mut owner_state); + negotiate(&mut core, &other, &mut other_state); + + let dispatch = handle_request( + &mut core, + &owner, + &mut owner_state, + BrokerRequest::CreateEvent, + ); + let handle = match dispatch.response { + BrokerResponse::Handle(handle) => handle, + response => panic!("unexpected response: {response:?}"), + }; + + let dispatch = handle_request( + &mut core, + &other, + &mut other_state, + BrokerRequest::WaitEvent { handle }, + ); + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::InvalidRights) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + } + + #[test] + fn protocol_adapters_preserve_handle_fields() { + let protocol = ProtocolObjectHandle::new( + ProtocolObjectId::new(10), + ProtocolObjectGeneration::new(11), + ProtocolObjectReferenceId::new(12), + ProtocolObjectReferenceGeneration::new(13), + ); + + assert_eq!(protocol_handle(core_handle(protocol)), protocol); + } + + #[test] + fn protocol_adapters_preserve_wait_outcome() { + let outcome = CoreWaitOutcome::Ready(CoreReadinessState::new(true, 42)); + + assert_eq!( + protocol_wait_outcome(outcome), + ProtocolWaitOutcome::Ready(ProtocolReadinessState::new(true, 42)) + ); + } + + fn new_connection() -> ( + BrokerCore, + BrokerConnection, + ConnectionState, + ) { + let mut core = BrokerCore::new(EventOnlyPolicy); + let connection = core + .create_connection(CallerCredential::Unauthenticated) + .unwrap(); + (core, connection, ConnectionState::AwaitingNegotiation) + } + + fn negotiate( + core: &mut BrokerCore

, + connection: &BrokerConnection, + state: &mut ConnectionState, + ) { + let dispatch = handle_request( + core, + connection, + state, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + assert_eq!( + dispatch.response, + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + assert_eq!(*state, ConnectionState::Active); + } +} diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index 0969a914e..84f1e868d 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -14,8 +14,8 @@ use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; /// Peer identity information supplied by the transport or host layer. /// /// The first userland proof of concept does not authenticate Unix-socket peers, -/// but BrokerCore still accepts an explicit credential value so authenticated -/// transports can plumb identity through the same connection-creation seam. +/// but transports still return an explicit credential value so the server layer +/// can map authenticated peer identity into BrokerCore caller identity. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] #[non_exhaustive] pub enum PeerCredential { diff --git a/litebox_broker_unix_socket/tests/userland_broker.rs b/litebox_broker_unix_socket/tests/userland_broker.rs index 27e91d1fe..fa5d738a0 100644 --- a/litebox_broker_unix_socket/tests/userland_broker.rs +++ b/litebox_broker_unix_socket/tests/userland_broker.rs @@ -10,8 +10,8 @@ use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use litebox_broker_client::BrokerClient; -use litebox_broker_core::SUPPORTED_PROTOCOL_VERSION; use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_server::SUPPORTED_PROTOCOL_VERSION; use litebox_broker_unix_socket::UnixStreamClientTransport; #[test] From c90b69ec9f430571a56e7f2d47aa65237ad20043 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 11:17:44 -0700 Subject: [PATCH 09/66] Hide foreign broker object references Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/event.rs | 4 ++-- litebox_broker_core/src/object.rs | 2 +- litebox_broker_server/src/server.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 033fd199b..071441429 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -156,7 +156,7 @@ mod tests { } #[test] - fn wait_rejects_handle_owned_by_another_association() { + fn wait_hides_handle_owned_by_another_association() { let mut core = BrokerCore::new(EventOnlyPolicy); let owner = core .create_connection(CallerCredential::Unauthenticated) @@ -168,7 +168,7 @@ mod tests { assert_eq!( core.wait_event(&other, handle), - Err(BrokerError::InvalidRights) + Err(BrokerError::UnknownObject) ); } } diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 5caba7044..85c20d9dc 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -219,7 +219,7 @@ impl BrokerCore

{ .ok_or(BrokerError::UnknownObject)?; debug_assert_eq!(reference.reference_generation, FIRST_REFERENCE_GENERATION); if reference.owner != association { - return Err(BrokerError::InvalidRights); + return Err(BrokerError::UnknownObject); } if reference.reference_generation != handle.reference_generation || reference.object_generation != handle.object_generation diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index fd97903ee..2210aab55 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -558,7 +558,7 @@ mod tests { ); assert_eq!( dispatch.response, - BrokerResponse::Error(ErrorCode::InvalidRights) + BrokerResponse::Error(ErrorCode::UnknownObject) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } From 4845aa5583056db2196459749a758256ca2cf33c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 11:58:59 -0700 Subject: [PATCH 10/66] Harden split broker interfaces Track effective protocol negotiation in client and server state, expose version mismatch responses for downgrade retries, and make public broker interface enums forward-compatible. Map future core outcomes to an Internal protocol error and keep wire tag details contained in the wire layer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 4 +- docs/impl-plan.md | 3 +- litebox_broker_client/src/error.rs | 41 +++++- litebox_broker_client/src/lib.rs | 80 +++++++++-- litebox_broker_client/src/negotiate.rs | 26 +++- litebox_broker_core/src/error.rs | 1 + litebox_broker_core/src/event.rs | 1 + litebox_broker_protocol/src/error.rs | 5 + litebox_broker_protocol/src/message.rs | 9 ++ litebox_broker_server/src/server.rs | 126 +++++++++++------- litebox_broker_transport/src/lib.rs | 10 +- .../src/bin/litebox-broker-userland.rs | 1 + litebox_broker_wire/src/lib.rs | 24 +++- 13 files changed, 258 insertions(+), 73 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index b7b82a376..dd08b272a 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -146,7 +146,9 @@ The broker split should use crate names that make the authority boundary visible Transport must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_transport`; the broker server adapts protocol and transport concepts into direct BrokerCore domain calls. Shared request/response transport traits live in `litebox_broker_transport`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. -Forward-compatible protocol probing is explicit: unknown request tags decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection. Structurally malformed frames remain transport/wire errors. BrokerCore only sees supported, already-adapted domain operations. +Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection without exposing wire tag width through the transport interface. Structurally malformed frames remain transport/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. + +Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while client and server connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so clients can retry with a compatible version on expensive or credentialed transports instead of reconnecting and guessing. Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical split: diff --git a/docs/impl-plan.md b/docs/impl-plan.md index e26226074..4bf2b6b6e 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -100,7 +100,8 @@ Exit criteria: - Client code does not need to depend on the userland broker server crate to use the first Unix socket transport. - The generic broker server library does not depend on concrete Unix socket transport code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. -- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly. +- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly; newer core error categories or wait outcomes that the server adapter cannot represent return `Internal`. +- Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so clients can downgrade without reconnecting or guessing. ## Phase 3: UserLiteBox facade diff --git a/litebox_broker_client/src/error.rs b/litebox_broker_client/src/error.rs index d32db0ffb..95b0181b6 100644 --- a/litebox_broker_client/src/error.rs +++ b/litebox_broker_client/src/error.rs @@ -3,10 +3,11 @@ use core::fmt; -use litebox_broker_protocol::{BrokerResponse, ErrorCode}; +use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; /// Errors returned by the broker client adapter. #[derive(Debug)] +#[non_exhaustive] pub enum ClientError { /// The transport failed. Transport(E), @@ -16,8 +17,22 @@ pub enum ClientError { AlreadyNegotiated, /// The broker closed the transport before returning a response. TransportClosed, - /// The broker returned a response tag this client does not understand. - UnknownResponse { tag: u8 }, + /// The broker returned a response this client does not understand. + UnknownResponse, + /// The broker accepted negotiation with a version that cannot serve the request. + IncompatibleNegotiation { + /// Protocol version requested by this client. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, + /// The broker does not support the requested protocol version. + UnsupportedVersion { + /// Protocol version requested by this client. + requested: ProtocolVersion, + /// Protocol version advertised by the broker. + broker_protocol_version: ProtocolVersion, + }, /// BrokerCore rejected the request. Broker(ErrorCode), /// The broker returned a response type that does not match the request. @@ -31,7 +46,21 @@ impl fmt::Display for ClientError { Self::NotNegotiated => write!(f, "broker client has not negotiated protocol version"), Self::AlreadyNegotiated => f.write_str("broker client already negotiated"), Self::TransportClosed => write!(f, "broker closed the transport"), - Self::UnknownResponse { tag } => write!(f, "unknown broker response tag {tag}"), + 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::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:?}") @@ -51,7 +80,9 @@ where Self::NotNegotiated | Self::AlreadyNegotiated | Self::TransportClosed - | Self::UnknownResponse { .. } + | Self::UnknownResponse + | Self::IncompatibleNegotiation { .. } + | Self::UnsupportedVersion { .. } | Self::UnexpectedResponse(_) => None, } } diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index 2dbf62c66..401d4f4c8 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -33,7 +33,9 @@ pub struct BrokerClient { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConnectionState { AwaitingNegotiation, - Active, + Active { + negotiated_protocol_version: ProtocolVersion, + }, } impl BrokerClient { @@ -47,10 +49,25 @@ impl BrokerClient { } impl BrokerClient { - pub(crate) fn ensure_negotiated(&self) -> Result<(), T::Error> { + /// 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 client 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(ClientError::NotNegotiated), - ConnectionState::Active => Ok(()), + ConnectionState::Active { + negotiated_protocol_version, + } => Ok(negotiated_protocol_version), } } @@ -68,7 +85,7 @@ impl BrokerClient { Err(ClientError::Broker(error)) } ReceivedResponse::Response(response) => Ok(response), - ReceivedResponse::Unknown { tag } => Err(ClientError::UnknownResponse { tag }), + _ => Err(ClientError::UnknownResponse), } } } @@ -102,17 +119,62 @@ mod tests { })); let mut client = BrokerClient::new(transport); - assert_eq!( - client.negotiate_version(requested).unwrap(), - CLIENT_PROTOCOL_VERSION - ); + assert_eq!(client.negotiate_version(requested).unwrap(), requested); assert_eq!( client.transport.sent_request, Some(BrokerRequest::Negotiate { protocol_version: requested }) ); - assert_eq!(client.state, ConnectionState::Active); + assert_eq!(client.negotiated_protocol_version(), Some(requested)); + } + + #[test] + fn negotiate_version_rejects_incompatible_broker_response() { + let requested = ProtocolVersion::new(1, 1); + let transport = FakeTransport::new(Some(BrokerResponse::Negotiated { + broker_protocol_version: ProtocolVersion::new(1, 0), + })); + let mut client = BrokerClient::new(transport); + + assert!(matches!( + client.negotiate_version(requested), + Err(ClientError::IncompatibleNegotiation { + requested: actual_requested, + broker_protocol_version + }) if actual_requested == requested + && broker_protocol_version == ProtocolVersion::new(1, 0) + )); + assert_eq!(client.negotiated_protocol_version(), None); + } + + #[test] + fn negotiate_version_reports_supported_version_and_allows_retry() { + let too_new = ProtocolVersion::new( + CLIENT_PROTOCOL_VERSION.major, + CLIENT_PROTOCOL_VERSION.minor + 1, + ); + let fallback = CLIENT_PROTOCOL_VERSION; + let transport = FakeTransport::new(Some(BrokerResponse::VersionMismatch { + broker_protocol_version: fallback, + })); + let mut client = BrokerClient::new(transport); + + assert!(matches!( + client.negotiate_version(too_new), + Err(ClientError::UnsupportedVersion { + requested, + broker_protocol_version + }) if requested == too_new && broker_protocol_version == fallback + )); + assert_eq!(client.negotiated_protocol_version(), None); + + client.transport.response = Some(BrokerResponse::Negotiated { + broker_protocol_version: fallback, + }); + + assert_eq!(client.negotiate_version(fallback).unwrap(), fallback); + assert_eq!(client.negotiated_protocol_version(), Some(fallback)); } struct FakeTransport { diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_client/src/negotiate.rs index dc765f774..d6bdd2859 100644 --- a/litebox_broker_client/src/negotiate.rs +++ b/litebox_broker_client/src/negotiate.rs @@ -8,12 +8,18 @@ use litebox_broker_transport::ClientTransport; use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION, ClientError, Result}; impl BrokerClient { - /// Negotiates the first POC protocol version. + /// Negotiates the default client protocol version. + /// + /// Returns the effective protocol version this connection will speak. pub fn negotiate(&mut self) -> Result { self.negotiate_version(CLIENT_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, @@ -27,9 +33,23 @@ impl BrokerClient { BrokerResponse::Negotiated { broker_protocol_version, } => { - self.state = crate::ConnectionState::Active; - Ok(broker_protocol_version) + if !protocol_version.is_supported_by(broker_protocol_version) { + return Err(ClientError::IncompatibleNegotiation { + requested: protocol_version, + broker_protocol_version, + }); + } + self.state = crate::ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; + Ok(protocol_version) } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => Err(ClientError::UnsupportedVersion { + requested: protocol_version, + broker_protocol_version, + }), response => Err(ClientError::UnexpectedResponse(response)), } } diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 8600d5895..31c7e67b9 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -5,6 +5,7 @@ use core::fmt; /// Broker authority error category. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[non_exhaustive] pub enum BrokerError { /// Policy denied the operation. PolicyDenied, diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 071441429..22894e2c2 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -25,6 +25,7 @@ impl ReadinessState { /// Result of checking an event wait. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum WaitOutcome { /// The event was ready and a nonblocking wait would complete. Ready(ReadinessState), diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 20de455fd..9be9ee281 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -15,6 +15,8 @@ pub enum ErrorCode { 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. @@ -39,6 +41,7 @@ impl ErrorCode { 3 => Self::MalformedRequest, 10 => Self::ProtocolState, 11 => Self::UnsupportedOperation, + 12 => Self::Internal, 4 => Self::PolicyDenied, 5 => Self::UnknownObject, 6 => Self::StaleHandle, @@ -56,6 +59,7 @@ impl ErrorCode { Self::MalformedRequest => 3, Self::ProtocolState => 10, Self::UnsupportedOperation => 11, + Self::Internal => 12, Self::PolicyDenied => 4, Self::UnknownObject => 5, Self::StaleHandle => 6, @@ -74,6 +78,7 @@ impl fmt::Display for ErrorCode { 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"), diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index b3fd5fa62..6f6cd8dc8 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -65,6 +65,15 @@ pub enum BrokerResponse { /// [`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 client 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, + }, /// Operation returned a broker object handle. Handle(ObjectHandle), /// Operation returned readiness state. diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 2210aab55..aec5c5096 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -91,7 +91,7 @@ fn handle_received_request( ) -> BrokerDispatch { match received { ReceivedRequest::Request(request) => handle_request(core, connection, state, request), - ReceivedRequest::Unknown { tag: _ } => handle_unknown_request(*state), + _ => handle_unknown_request(*state), } } @@ -101,8 +101,8 @@ fn handle_request( state: &mut ConnectionState, request: BrokerRequest, ) -> BrokerDispatch { - if *state == ConnectionState::AwaitingNegotiation { - return match request { + match *state { + ConnectionState::AwaitingNegotiation => match request { BrokerRequest::Negotiate { protocol_version } => { handle_negotiation(state, protocol_version) } @@ -110,9 +110,19 @@ fn handle_request( BrokerResponse::Error(ErrorCode::ProtocolState), CloseReason::ProtocolViolation, ), - }; + }, + ConnectionState::Active { + negotiated_protocol_version, + } => handle_active_request(core, connection, negotiated_protocol_version, request), } +} +fn handle_active_request( + core: &mut BrokerCore

, + connection: &BrokerConnection, + _negotiated_protocol_version: ProtocolVersion, + request: BrokerRequest, +) -> BrokerDispatch { match request { BrokerRequest::Negotiate { .. } => BrokerDispatch::close_after( BrokerResponse::Error(ErrorCode::ProtocolState), @@ -122,9 +132,8 @@ fn handle_request( core.create_event(connection), |handle| BrokerResponse::Handle(protocol_handle(handle)), )), - BrokerRequest::WaitEvent { handle } => BrokerDispatch::continue_after(handle_core_result( + BrokerRequest::WaitEvent { handle } => BrokerDispatch::continue_after(handle_wait_result( core.wait_event(connection, core_handle(handle)), - |outcome| BrokerResponse::Wait(protocol_wait_outcome(outcome)), )), BrokerRequest::SignalEvent { handle } => { BrokerDispatch::continue_after(handle_core_result( @@ -152,15 +161,16 @@ fn handle_negotiation( protocol_version: ProtocolVersion, ) -> BrokerDispatch { if protocol_version.is_supported_by(SUPPORTED_PROTOCOL_VERSION) { - *state = ConnectionState::Active; + *state = ConnectionState::Active { + negotiated_protocol_version: protocol_version, + }; BrokerDispatch::continue_after(BrokerResponse::Negotiated { broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, }) } else { - BrokerDispatch::close_after( - BrokerResponse::Error(ErrorCode::UnsupportedVersion), - CloseReason::UnsupportedVersion, - ) + BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, + }) } } @@ -174,6 +184,16 @@ fn handle_core_result( } } +fn handle_wait_result(result: litebox_broker_core::Result) -> BrokerResponse { + match result { + Ok(outcome) => match protocol_wait_outcome(outcome) { + Some(outcome) => BrokerResponse::Wait(outcome), + None => BrokerResponse::Error(ErrorCode::Internal), + }, + Err(error) => BrokerResponse::Error(protocol_error(error)), + } +} + fn protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, @@ -182,6 +202,7 @@ fn protocol_error(error: BrokerError) -> ErrorCode { BrokerError::WrongObjectType => ErrorCode::WrongObjectType, BrokerError::InvalidRights => ErrorCode::InvalidRights, BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, + _ => ErrorCode::Internal, } } @@ -207,21 +228,24 @@ fn protocol_readiness_state(readiness: CoreReadinessState) -> ProtocolReadinessS ProtocolReadinessState::new(readiness.ready, readiness.generation) } -fn protocol_wait_outcome(outcome: CoreWaitOutcome) -> ProtocolWaitOutcome { +fn protocol_wait_outcome(outcome: CoreWaitOutcome) -> Option { match outcome { - CoreWaitOutcome::Ready(readiness) => { - ProtocolWaitOutcome::Ready(protocol_readiness_state(readiness)) - } - CoreWaitOutcome::WouldBlock(readiness) => { - ProtocolWaitOutcome::WouldBlock(protocol_readiness_state(readiness)) - } + CoreWaitOutcome::Ready(readiness) => Some(ProtocolWaitOutcome::Ready( + protocol_readiness_state(readiness), + )), + CoreWaitOutcome::WouldBlock(readiness) => Some(ProtocolWaitOutcome::WouldBlock( + protocol_readiness_state(readiness), + )), + _ => None, } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConnectionState { AwaitingNegotiation, - Active, + Active { + negotiated_protocol_version: ProtocolVersion, + }, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -254,6 +278,7 @@ impl BrokerDispatch { /// Reason the broker server closed the connection after sending a response. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum CloseReason { /// The peer requested an unsupported protocol version. UnsupportedVersion, @@ -272,6 +297,7 @@ impl fmt::Display for CloseReason { /// Terminal outcome for a successfully served broker connection. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ConnectionTermination { /// The peer cleanly closed the transport. PeerClosed, @@ -281,6 +307,7 @@ pub enum ConnectionTermination { /// Errors returned by a broker receive/send loop. #[derive(Debug)] +#[non_exhaustive] pub enum BrokerServeError { /// The server could not allocate or map state for a new connection. ConnectionSetup, @@ -361,7 +388,7 @@ mod tests { } #[test] - fn dispatch_closes_after_unsupported_version() { + fn dispatch_reports_supported_version_after_unsupported_major() { let (mut core, connection, mut state) = new_connection(); let dispatch = handle_request( @@ -375,12 +402,11 @@ mod tests { assert_eq!( dispatch.response, - BrokerResponse::Error(ErrorCode::UnsupportedVersion) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::UnsupportedVersion) + BrokerResponse::VersionMismatch { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); assert_eq!(state, ConnectionState::AwaitingNegotiation); } @@ -408,7 +434,12 @@ mod tests { } ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!(state, ConnectionState::Active); + assert_eq!( + state, + ConnectionState::Active { + negotiated_protocol_version: requested + } + ); } #[test] @@ -429,12 +460,11 @@ mod tests { assert_eq!( dispatch.response, - BrokerResponse::Error(ErrorCode::UnsupportedVersion) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::UnsupportedVersion) + BrokerResponse::VersionMismatch { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); assert_eq!(state, ConnectionState::AwaitingNegotiation); } @@ -443,12 +473,8 @@ mod tests { let (mut core, connection, mut state) = new_connection(); negotiate(&mut core, &connection, &mut state); - let dispatch = handle_received_request( - &mut core, - &connection, - &mut state, - ReceivedRequest::Unknown { tag: 0xff }, - ); + let dispatch = + handle_received_request(&mut core, &connection, &mut state, ReceivedRequest::Unknown); assert_eq!( dispatch.response, @@ -461,12 +487,8 @@ mod tests { fn dispatch_closes_unknown_requests_before_negotiation() { let (mut core, connection, mut state) = new_connection(); - let dispatch = handle_received_request( - &mut core, - &connection, - &mut state, - ReceivedRequest::Unknown { tag: 0xff }, - ); + let dispatch = + handle_received_request(&mut core, &connection, &mut state, ReceivedRequest::Unknown); assert_eq!( dispatch.response, @@ -497,7 +519,12 @@ mod tests { } ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!(state, ConnectionState::Active); + assert_eq!( + state, + ConnectionState::Active { + negotiated_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); let dispatch = handle_request( &mut core, @@ -581,7 +608,9 @@ mod tests { assert_eq!( protocol_wait_outcome(outcome), - ProtocolWaitOutcome::Ready(ProtocolReadinessState::new(true, 42)) + Some(ProtocolWaitOutcome::Ready(ProtocolReadinessState::new( + true, 42 + ))) ); } @@ -616,6 +645,11 @@ mod tests { broker_protocol_version: SUPPORTED_PROTOCOL_VERSION } ); - assert_eq!(*state, ConnectionState::Active); + assert_eq!( + *state, + ConnectionState::Active { + negotiated_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); } } diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs index 84f1e868d..aaf693622 100644 --- a/litebox_broker_transport/src/lib.rs +++ b/litebox_broker_transport/src/lib.rs @@ -30,11 +30,12 @@ pub enum PeerCredential { /// Request received from a transport. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ReceivedRequest { /// A request understood by the current protocol crate. Request(BrokerRequest), - /// A request tag emitted by a newer peer and not understood by this process. - Unknown { tag: u8 }, + /// A request emitted by a newer peer and not understood by this process. + Unknown, } impl From for ReceivedRequest { @@ -45,11 +46,12 @@ impl From for ReceivedRequest { /// Response received from a transport. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum ReceivedResponse { /// A response understood by the current protocol crate. Response(BrokerResponse), - /// A response tag emitted by a newer broker and not understood by this process. - Unknown { tag: u8 }, + /// A response emitted by a newer broker and not understood by this process. + Unknown, } impl From for ReceivedResponse { diff --git a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs index 5080448ff..1f25899ad 100644 --- a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs +++ b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs @@ -25,6 +25,7 @@ fn broker_error(error: BrokerServeError) -> io::Error { match error { BrokerServeError::ConnectionSetup => io::Error::other("broker connection setup failed"), BrokerServeError::Transport(error) => error, + error => io::Error::other(error.to_string()), } } diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index 35242116d..151c21cf0 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -26,12 +26,14 @@ const RESPONSE_TAG_HANDLE: u8 = 1; const RESPONSE_TAG_READINESS: u8 = 2; const RESPONSE_TAG_WAIT: u8 = 3; const RESPONSE_TAG_ERROR: u8 = 4; +const RESPONSE_TAG_VERSION_MISMATCH: u8 = 5; const WAIT_OUTCOME_TAG_READY: u8 = 1; const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum WireError { /// The encoder was asked to emit a request tag this codec does not own. EncodeUnknownRequestTag, @@ -117,7 +119,7 @@ pub fn decode_request(frame: &[u8]) -> Result { REQUEST_TAG_SIGNAL_EVENT => BrokerRequest::SignalEvent { handle: decoder.handle()?, }, - _ => return Ok(ReceivedRequest::Unknown { tag }), + _ => return Ok(ReceivedRequest::Unknown), }; decoder.finish()?; Ok(ReceivedRequest::Request(request)) @@ -137,6 +139,13 @@ pub fn encode_response(response: BrokerResponse) -> Result, WireError> { encoder.u16(broker_protocol_version.major); encoder.u16(broker_protocol_version.minor); } + BrokerResponse::VersionMismatch { + broker_protocol_version, + } => { + encoder.u8(RESPONSE_TAG_VERSION_MISMATCH); + encoder.u16(broker_protocol_version.major); + encoder.u16(broker_protocol_version.minor); + } BrokerResponse::Handle(handle) => { encoder.u8(RESPONSE_TAG_HANDLE); encoder.handle(handle); @@ -176,6 +185,9 @@ pub fn decode_response(frame: &[u8]) -> Result { RESPONSE_TAG_NEGOTIATED => BrokerResponse::Negotiated { broker_protocol_version: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), }, + RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { + broker_protocol_version: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + }, RESPONSE_TAG_HANDLE => BrokerResponse::Handle(decoder.handle()?), RESPONSE_TAG_READINESS => BrokerResponse::Readiness(decoder.readiness()?), RESPONSE_TAG_WAIT => { @@ -190,7 +202,7 @@ pub fn decode_response(frame: &[u8]) -> Result { let error = ErrorCode::from_raw(decoder.u16()?); BrokerResponse::Error(error) } - _ => return Ok(ReceivedResponse::Unknown { tag }), + _ => return Ok(ReceivedResponse::Unknown), }; decoder.finish()?; Ok(ReceivedResponse::Response(response)) @@ -341,11 +353,15 @@ mod tests { BrokerResponse::Negotiated { broker_protocol_version: ProtocolVersion::new(1, 0), }, + BrokerResponse::VersionMismatch { + broker_protocol_version: ProtocolVersion::new(1, 0), + }, BrokerResponse::Handle(handle), BrokerResponse::Readiness(ReadinessState::new(false, 7)), BrokerResponse::Wait(WaitOutcome::Ready(ReadinessState::new(true, 8))), BrokerResponse::Wait(WaitOutcome::WouldBlock(ReadinessState::new(false, 9))), BrokerResponse::Error(ErrorCode::PolicyDenied), + BrokerResponse::Error(ErrorCode::Internal), ]; for response in responses { @@ -360,7 +376,7 @@ mod tests { fn decode_rejects_malformed_request_frames() { assert_eq!( decode_request(&[0xff, 1, 2, 3]), - Ok(ReceivedRequest::Unknown { tag: 0xff }) + Ok(ReceivedRequest::Unknown) ); assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); let mut frame = encode_request(BrokerRequest::CreateEvent).unwrap(); @@ -372,7 +388,7 @@ mod tests { fn decode_rejects_malformed_response_frames() { assert_eq!( decode_response(&[0xff, 1, 2, 3]), - Ok(ReceivedResponse::Unknown { tag: 0xff }) + Ok(ReceivedResponse::Unknown) ); assert_eq!( decode_response(&[3, 0xff]), From fb982cc9a0b17274e6c8be613f6ae15e797d2ea9 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 12:22:52 -0700 Subject: [PATCH 11/66] Simplify broker object handles Expose broker handles as reference capabilities containing only reference ID and reference generation. Keep object IDs internal to BrokerCore and remove ObjectGeneration from the protocol, wire codec, and server adapters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 22 ++--- docs/impl-plan.md | 16 ++-- litebox_broker_core/src/event.rs | 23 +++--- litebox_broker_core/src/lib.rs | 8 +- litebox_broker_core/src/object.rs | 115 ++++++-------------------- litebox_broker_protocol/src/lib.rs | 4 +- litebox_broker_protocol/src/object.rs | 47 +---------- litebox_broker_server/src/server.rs | 14 +--- litebox_broker_wire/src/lib.rs | 17 +--- 9 files changed, 71 insertions(+), 195 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index dd08b272a..d89ee33f0 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -133,8 +133,8 @@ The broker split should use crate names that make the authority boundary visible | Crate | Initial role | |---|---| -| `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event object/reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | -| `litebox_broker_core` | Protocol- and transport-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | +| `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | +| `litebox_broker_core` | Protocol- and transport-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | | `litebox_broker_transport` | Neutral `no_std` transport-trait crate for blocking directional request/response transport contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the transport-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | | `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream transports reuse it rather than duplicating protocol encoding. | | `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket transport crate implementing the neutral client/server transport traits for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | @@ -225,9 +225,9 @@ Each sandboxed process has exactly one authenticated broker association. That as The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. UserLiteBox does not prove identity by filling in request fields. -The protocol exposes broker-owned objects through opaque object identifiers plus closeable broker reference identifiers and generations. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, generations, rights, and policy. Object types and rights are broker-internal for authorization; UserLiteBox cannot amplify authority by editing request fields. +The protocol exposes broker-owned objects through opaque per-association reference handles: a reference identifier plus a reference generation. Object identifiers stay broker-internal. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, reference generations, rights, and policy. Object types and rights are broker-internal for authorization; UserLiteBox cannot amplify authority by editing request fields. -The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. UserLiteBox receives only broker object identifiers, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. +The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. UserLiteBox receives only broker reference handles, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. The control channel authorizes an operation and binds it to an object and request identifier; the data channel carries bytes for that authorized operation. Shared memory is an optimization, not an authority transfer, and all shared-memory contents remain untrusted. @@ -431,7 +431,7 @@ The current `litebox` crate should not be split by whole module. Most modules mi | Current area | Keep in UserLiteBox | Move to BrokerCore / broker side | |---|---|---| | `LiteBox` object | user-mode facade, std-backed helpers, broker connection/session object | broker session/workload identity | -| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, reference IDs, generations, rights, dup/pass/close/refcounts | +| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, reference IDs, reference generations, rights, dup/pass/close/refcounts | | `fd::RawDescriptorStorage` | raw-int fd conversion for shim ABI | validation that a handle is live and authorized | | fd metadata | local ABI metadata and cached hints | shared open-description metadata and metadata inherited/duplicated/passed across processes | | `path.rs` | string/CStr conversion, cheap normalization helpers | authoritative path lookup, namespace traversal, permission checks | @@ -448,7 +448,7 @@ The current `litebox` crate should not be split by whole module. Most modules mi Concrete examples: -- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, generations, refcount, passing, duplication, close, and revoke semantics. +- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, reference generations, refcount, passing, duplication, close, and revoke semantics. - `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. UserLiteBox should only marshal paths/buffers and use broker-owned data channels or rings for payloads. - `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. UserLiteBox should keep the socket facade and use broker-owned rings for data movement. - `mm::PageManager` currently owns VMA state and calls platform page-management operations. UserLiteBox can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. @@ -498,7 +498,7 @@ UserLiteBox asks broker: open(path, flags) BrokerCore resolves object/capability PolicyEngine authorizes path/flags/caller BrokerPlatform opens host object and stores host handle privately -Broker returns broker object id, not host fd/HANDLE +Broker returns broker reference handle, not host fd/HANDLE UserLiteBox uses control/data channels for operations ``` @@ -629,7 +629,7 @@ LiteBox's proposed design combines ideas from several systems rather than copyin | **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | | **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | | **SKernel** | split guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | -| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore handles should carry object identity and generation checks while BrokerCore stores authoritative rights | +| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore should keep object identity internal and expose reference capabilities with generation checks while storing authoritative rights | | **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | | **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | @@ -794,13 +794,13 @@ minimal PolicyEngine broker-owned event object ``` -Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through the broker, while unrelated operations continue through the existing local compatibility path. UserLiteBox handle entries can therefore contain either local compatibility objects or broker object/reference IDs with generations and cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. +Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through the broker, while unrelated operations continue through the existing local compatibility path. UserLiteBox handle entries can therefore contain either local compatibility objects or broker reference handles with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. Then proceed incrementally: 1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. -2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event object/reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, event object/reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and transport-neutral. +2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. +3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and transport-neutral. 4. Create `litebox_broker_transport` with neutral client/server request-response traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from transport/protocol requests to direct BrokerCore domain calls. 5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 4bf2b6b6e..2b837feb6 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -56,7 +56,7 @@ Create a shared crate for broker protocol types. Initial contents: - protocol version type; -- broker event object/reference IDs with generations; +- broker event reference handles with reference generations; - minimal event request/response messages; - readiness and wait outcome payloads; - ABI-neutral error categories; @@ -118,7 +118,7 @@ Exit criteria: - Current tests can still use the local profile. - A broker-backed profile can issue a simple broker request. -- UserLiteBox handle entries can store opaque broker object/reference IDs + generations plus local cached rights hints. +- UserLiteBox handle entries can store opaque broker reference handles plus local cached rights hints. ## Phase 4: First broker-owned object @@ -126,8 +126,8 @@ Start with a small event or pipe-like object, not filesystem or networking. Broker owns: -- object ID and generation; -- initial reference ID, generation, and rights; +- broker-internal object ID and lifetime; +- initial reference ID, reference generation, and rights; - readiness state; - wait/wakeup state. @@ -151,7 +151,7 @@ BrokerCore owns: - object refs; - rights; -- generations; +- reference generations; - refcounts; - dup/pass/close; - inherited object tables; @@ -368,7 +368,7 @@ typed broker client control channel only minimal PolicyEngine broker-owned event object -UserLiteBox fd table maps guest fd -> broker object/reference id +UserLiteBox fd table maps guest fd -> broker reference handle ``` This proves the trust boundary before taking on filesystem, networking, mapping, or multiprocess complexity. @@ -380,8 +380,8 @@ Add conformance tests at each layer: - protocol parsing rejects malformed frames; - policy default-denies unknown operations; - caller identity is transport-bound; -- stale object IDs fail; -- wrong generation fails; +- stale reference IDs fail; +- wrong reference generation fails; - process disconnect cleans up refs; - UserLiteBox local handle edits cannot create authority; - shared-memory cursor/frame corruption fails closed; diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 22894e2c2..16aac07c4 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::object::ObjectKind; +use crate::object::{ObjectId, ObjectKind}; use crate::{ BrokerConnection, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, PolicyEngine, Result, @@ -58,8 +58,9 @@ impl BrokerCore

{ handle: ObjectHandle, ) -> Result { let association = connection.association(); - self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WAIT)?; - let state = self.event_state(handle)?; + let object_id = + self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + let state = self.event_state(object_id)?; Ok(if state.ready { WaitOutcome::Ready(state) } else { @@ -74,14 +75,15 @@ impl BrokerCore

{ handle: ObjectHandle, ) -> Result { let association = connection.association(); - self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WRITE)?; - match &mut self.object_mut(handle.object_id)?.kind { + let object_id = + self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + match &mut self.object_mut(object_id)?.kind { ObjectKind::Event(event) => event.signal(), } } - fn event_state(&self, handle: ObjectHandle) -> Result { - match &self.object(handle.object_id)?.kind { + fn event_state(&self, object_id: ObjectId) -> Result { + match &self.object(object_id)?.kind { ObjectKind::Event(event) => Ok(event.readiness_state()), } } @@ -118,7 +120,7 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerCore, CallerCredential, EventOnlyPolicy, ObjectGeneration}; + use crate::{BrokerCore, CallerCredential, EventOnlyPolicy, ObjectReferenceGeneration}; #[test] fn wait_rejects_reference_without_wait_right() { @@ -142,13 +144,14 @@ mod tests { } #[test] - fn wait_rejects_stale_object_generation() { + fn wait_rejects_stale_reference_generation() { let mut core = BrokerCore::new(EventOnlyPolicy); let connection = core .create_connection(CallerCredential::Unauthenticated) .unwrap(); let mut handle = core.create_event(&connection).unwrap(); - handle.object_generation = ObjectGeneration::new(handle.object_generation.get() + 1); + handle.reference_generation = + ObjectReferenceGeneration::new(handle.reference_generation.get() + 1); assert_eq!( core.wait_event(&connection, handle), diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index d4ab05bb8..9dbc5f75f 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -4,7 +4,7 @@ //! Protocol- and transport-independent broker authority core. //! //! `litebox_broker_core` owns broker-side object identity, reference lifetime, -//! rights checks, generation checks, and policy calls. It deliberately has no +//! rights checks, reference generation checks, and policy calls. It deliberately has no //! dependency on protocol request/response types, Unix sockets, shared-memory //! rings, or any other transport. @@ -28,10 +28,8 @@ pub use connection::BrokerConnection; pub use error::BrokerError; pub use event::{ReadinessState, WaitOutcome}; pub use identity::CallerCredential; -use object::{ObjectEntry, ObjectReference}; -pub use object::{ - ObjectGeneration, ObjectHandle, ObjectId, ObjectReferenceGeneration, ObjectReferenceId, -}; +use object::{ObjectEntry, ObjectId, ObjectReference}; +pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; pub use policy::{ DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyEngine, PolicyOperation, }; diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 85c20d9dc..383a3c213 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -31,14 +31,16 @@ macro_rules! id_type { }; } -id_type! { - /// Broker-owned object identifier. - ObjectId -} - -id_type! { - /// Generation attached to a broker object. - ObjectGeneration +/// 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) + } } id_type! { @@ -51,16 +53,13 @@ id_type! { ObjectReferenceGeneration } -/// Broker-owned object handle returned by BrokerCore. +/// Broker-owned reference handle returned by BrokerCore. /// /// UserLiteBox may cache this value, but the broker remains authoritative for -/// object lifetime, reference lifetime, type, rights, and generation. +/// object identity, object lifetime, reference lifetime, type, rights, and +/// reference generation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ObjectHandle { - /// Opaque broker object identifier. - pub object_id: ObjectId, - /// Object generation used to reject stale handles after object-slot reuse. - pub object_generation: ObjectGeneration, /// 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. @@ -70,32 +69,21 @@ pub struct ObjectHandle { impl ObjectHandle { /// Creates an object handle. pub const fn new( - object_id: ObjectId, - object_generation: ObjectGeneration, reference_id: ObjectReferenceId, reference_generation: ObjectReferenceGeneration, ) -> Self { Self { - object_id, - object_generation, reference_id, reference_generation, } } } -const FIRST_OBJECT_GENERATION: ObjectGeneration = ObjectGeneration::new(1); const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ObjectReference { pub(crate) object_id: ObjectId, - /// Mirrors the referenced object's generation when this reference is minted. - /// - /// The duplicate lets stale-reference validation reject mismatched handles - /// before trusting any object-table lookup; object validation below still - /// checks the authoritative entry generation. - pub(crate) object_generation: ObjectGeneration, pub(crate) reference_generation: ObjectReferenceGeneration, pub(crate) owner: Association, pub(crate) object_type: ObjectType, @@ -104,7 +92,6 @@ pub(crate) struct ObjectReference { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct ObjectEntry { - pub(crate) generation: ObjectGeneration, pub(crate) kind: ObjectKind, } @@ -124,10 +111,10 @@ impl ObjectKind { impl BrokerCore

{ /// Inserts a broker object and mints its first owned reference. /// - /// The current POC never reuses object or reference slots, so both - /// generations start at the authority-owned first generation. Any future - /// slot-reuse path must bump the corresponding generation before reissuing - /// a slot so stale handles cannot validate against a recycled entry. + /// 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: Association, @@ -137,22 +124,13 @@ impl BrokerCore

{ ) -> Result { let object_id = self.allocate_object_id()?; let reference_id = self.allocate_reference_id()?; - let object_generation = FIRST_OBJECT_GENERATION; let reference_generation = FIRST_REFERENCE_GENERATION; - debug_assert_eq!(reference_generation, FIRST_REFERENCE_GENERATION); - self.objects.insert( - object_id, - ObjectEntry { - generation: object_generation, - kind, - }, - ); + self.objects.insert(object_id, ObjectEntry { kind }); self.references.insert( reference_id, ObjectReference { object_id, - object_generation, reference_generation, owner: association, object_type, @@ -160,12 +138,7 @@ impl BrokerCore

{ }, ); - Ok(ObjectHandle::new( - object_id, - object_generation, - reference_id, - reference_generation, - )) + Ok(ObjectHandle::new(reference_id, reference_generation)) } pub(crate) fn authorize_create_object( @@ -185,13 +158,14 @@ impl BrokerCore

{ handle: ObjectHandle, object_type: ObjectType, rights: ObjectRights, - ) -> Result<()> { - self.validate_handle(association, handle, object_type, rights)?; + ) -> Result { + let object_id = self.validate_handle(association, handle, object_type, rights)?; self.policy.authorize(PolicyOperation::use_object( association.caller_credential(), object_type, rights, - )) + ))?; + Ok(object_id) } pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { @@ -212,19 +186,15 @@ impl BrokerCore

{ handle: ObjectHandle, expected_type: ObjectType, required_rights: ObjectRights, - ) -> Result<()> { + ) -> Result { let reference = self .references .get(&handle.reference_id) .ok_or(BrokerError::UnknownObject)?; - debug_assert_eq!(reference.reference_generation, FIRST_REFERENCE_GENERATION); if reference.owner != association { return Err(BrokerError::UnknownObject); } - if reference.reference_generation != handle.reference_generation - || reference.object_generation != handle.object_generation - || reference.object_id != handle.object_id - { + if reference.reference_generation != handle.reference_generation { return Err(BrokerError::StaleHandle); } if reference.object_type != expected_type { @@ -238,15 +208,11 @@ impl BrokerCore

{ .objects .get(&reference.object_id) .ok_or(BrokerError::UnknownObject)?; - debug_assert_eq!(reference.object_generation, object.generation); - if object.generation != handle.object_generation { - return Err(BrokerError::StaleHandle); - } if object.kind.object_type() != expected_type { return Err(BrokerError::WrongObjectType); } - Ok(()) + Ok(reference.object_id) } fn allocate_object_id(&mut self) -> Result { @@ -313,7 +279,6 @@ mod tests { ) .unwrap(); - assert_eq!(handle.object_id, ObjectId::new(u64::MAX)); assert_eq!(handle.reference_id, ObjectReferenceId::new(u64::MAX)); assert_eq!(core.next_object_id, 0); assert_eq!(core.next_reference_id, 0); @@ -328,32 +293,6 @@ mod tests { ); } - #[test] - fn validate_handle_rejects_reference_object_generation_mismatch() { - let mut core = BrokerCore::new(DefaultDenyPolicy); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core - .insert_object_with_reference( - association, - ObjectKind::Event(EventObject::new()), - ObjectType::Event, - ObjectRights::WAIT, - ) - .unwrap(); - - core.references - .get_mut(&handle.reference_id) - .unwrap() - .object_generation = ObjectGeneration::new(handle.object_generation.get() + 1); - - assert_eq!( - core.validate_handle(association, handle, ObjectType::Event, ObjectRights::WAIT), - Err(BrokerError::StaleHandle) - ); - } - #[test] fn validate_handle_rejects_stale_reference_generation() { let mut core = BrokerCore::new(DefaultDenyPolicy); @@ -369,8 +308,6 @@ mod tests { ) .unwrap(); let stale_handle = ObjectHandle::new( - handle.object_id, - handle.object_generation, handle.reference_id, ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), ); diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 3b10b0e6c..f28cfcaff 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -15,9 +15,7 @@ mod object; pub use error::ErrorCode; pub use message::{BrokerRequest, BrokerResponse, ReadinessState, WaitOutcome}; -pub use object::{ - ObjectGeneration, ObjectHandle, ObjectId, ObjectReferenceGeneration, ObjectReferenceId, -}; +pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; /// Major/minor broker protocol version. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] diff --git a/litebox_broker_protocol/src/object.rs b/litebox_broker_protocol/src/object.rs index 7ce333eb6..78ab33c6f 100644 --- a/litebox_broker_protocol/src/object.rs +++ b/litebox_broker_protocol/src/object.rs @@ -1,50 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -/// Broker-owned object identifier. -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ObjectId(u64); - -impl ObjectId { - /// Creates an object 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 ObjectGeneration(u64); - -impl ObjectGeneration { - /// Creates a 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 - } -} - -/// Broker object handle returned to UserLiteBox. +/// Broker object reference handle returned to UserLiteBox. /// /// UserLiteBox may cache this value, but the broker remains authoritative for -/// object lifetime, reference lifetime, type, rights, and generation. +/// object identity, object lifetime, reference lifetime, type, rights, and +/// reference generation. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ObjectHandle { - /// Opaque broker object identifier. - pub object_id: ObjectId, - /// Object generation used to reject stale handles after object-slot reuse. - pub object_generation: ObjectGeneration, /// 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. @@ -54,14 +17,10 @@ pub struct ObjectHandle { impl ObjectHandle { /// Creates an object handle. pub const fn new( - object_id: ObjectId, - object_generation: ObjectGeneration, reference_id: ObjectReferenceId, reference_generation: ObjectReferenceGeneration, ) -> Self { Self { - object_id, - object_generation, reference_id, reference_generation, } diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index aec5c5096..8b25f0553 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -4,15 +4,13 @@ use core::fmt; use litebox_broker_core::{ - BrokerConnection, BrokerCore, BrokerError, CallerCredential, - ObjectGeneration as CoreObjectGeneration, ObjectHandle as CoreObjectHandle, - ObjectId as CoreObjectId, ObjectReferenceGeneration as CoreObjectReferenceGeneration, + BrokerConnection, BrokerCore, BrokerError, CallerCredential, ObjectHandle as CoreObjectHandle, + ObjectReferenceGeneration as CoreObjectReferenceGeneration, ObjectReferenceId as CoreObjectReferenceId, PolicyEngine, ReadinessState as CoreReadinessState, WaitOutcome as CoreWaitOutcome, }; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ErrorCode, ObjectGeneration as ProtocolObjectGeneration, - ObjectHandle as ProtocolObjectHandle, ObjectId as ProtocolObjectId, + BrokerRequest, BrokerResponse, ErrorCode, ObjectHandle as ProtocolObjectHandle, ObjectReferenceGeneration as ProtocolObjectReferenceGeneration, ObjectReferenceId as ProtocolObjectReferenceId, ProtocolVersion, ReadinessState as ProtocolReadinessState, WaitOutcome as ProtocolWaitOutcome, @@ -208,8 +206,6 @@ fn protocol_error(error: BrokerError) -> ErrorCode { fn core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { CoreObjectHandle::new( - CoreObjectId::new(handle.object_id.get()), - CoreObjectGeneration::new(handle.object_generation.get()), CoreObjectReferenceId::new(handle.reference_id.get()), CoreObjectReferenceGeneration::new(handle.reference_generation.get()), ) @@ -217,8 +213,6 @@ fn core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { fn protocol_handle(handle: CoreObjectHandle) -> ProtocolObjectHandle { ProtocolObjectHandle::new( - ProtocolObjectId::new(handle.object_id.get()), - ProtocolObjectGeneration::new(handle.object_generation.get()), ProtocolObjectReferenceId::new(handle.reference_id.get()), ProtocolObjectReferenceGeneration::new(handle.reference_generation.get()), ) @@ -593,8 +587,6 @@ mod tests { #[test] fn protocol_adapters_preserve_handle_fields() { let protocol = ProtocolObjectHandle::new( - ProtocolObjectId::new(10), - ProtocolObjectGeneration::new(11), ProtocolObjectReferenceId::new(12), ProtocolObjectReferenceGeneration::new(13), ); diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index 151c21cf0..536dc1b37 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -11,8 +11,8 @@ use core::fmt; use alloc::vec::Vec; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ErrorCode, ObjectGeneration, ObjectHandle, ObjectId, - ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, ReadinessState, WaitOutcome, + BrokerRequest, BrokerResponse, ErrorCode, ObjectHandle, ObjectReferenceGeneration, + ObjectReferenceId, ProtocolVersion, ReadinessState, WaitOutcome, }; use litebox_broker_transport::{ReceivedRequest, ReceivedResponse}; @@ -235,8 +235,6 @@ impl Encoder { } fn handle(&mut self, handle: ObjectHandle) { - self.u64(handle.object_id.get()); - self.u64(handle.object_generation.get()); self.u64(handle.reference_id.get()); self.u64(handle.reference_generation.get()); } @@ -291,17 +289,10 @@ impl<'a> Decoder<'a> { } fn handle(&mut self) -> Result { - let object_id = ObjectId::new(self.u64()?); - let object_generation = ObjectGeneration::new(self.u64()?); let reference_id = ObjectReferenceId::new(self.u64()?); let reference_generation = ObjectReferenceGeneration::new(self.u64()?); - Ok(ObjectHandle::new( - object_id, - object_generation, - reference_id, - reference_generation, - )) + Ok(ObjectHandle::new(reference_id, reference_generation)) } fn readiness(&mut self) -> Result { @@ -428,8 +419,6 @@ mod tests { const fn sample_handle() -> ObjectHandle { ObjectHandle::new( - ObjectId::new(11), - ObjectGeneration::new(12), ObjectReferenceId::new(13), ObjectReferenceGeneration::new(14), ) From f0fb8248540ac4eb5fe1caea7732a7521ce5777e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 12:29:27 -0700 Subject: [PATCH 12/66] Allow Unix broker transport in no_std CI Exclude litebox_broker_unix_socket from the confirm_no_std sweep because it is the concrete hosted Unix-domain-socket transport and broker executable for the split-broker POC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cdd2a5f15..5458ce0fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,6 +230,10 @@ jobs: # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # + # - `litebox_broker_unix_socket` is allowed to have `std` access, + # since it is the concrete Unix-domain-socket transport and hosted + # broker executable for the userland split-broker proof of concept. + # # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. # @@ -285,6 +289,7 @@ jobs: # can safely use std. find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ + -not -path './litebox_broker_unix_socket/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' \ From 3deef1cf1d1554d86b7d664afe6bff84b0eb53cd Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 14:12:02 -0700 Subject: [PATCH 13/66] Refine broker channel interfaces Rename the broker transport boundary to a channel boundary and tighten the protocol/channel naming so future IPC implementations can share the same semantic broker contract. Document the protocol, channel, wire, and core separation, including the future notification lane and kernel broker relationship. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 22 +- Cargo.toml | 4 +- docs/broker-design.md | 66 ++-- docs/impl-plan.md | 36 ++- .../Cargo.toml | 3 +- litebox_broker_channel/src/lib.rs | 102 +++++++ litebox_broker_client/Cargo.toml | 2 +- litebox_broker_client/src/error.rs | 16 +- litebox_broker_client/src/event.rs | 29 +- litebox_broker_client/src/lib.rs | 67 ++-- litebox_broker_client/src/negotiate.rs | 4 +- litebox_broker_core/src/lib.rs | 6 +- litebox_broker_protocol/src/lib.rs | 7 +- litebox_broker_protocol/src/message.rs | 56 +++- litebox_broker_server/Cargo.toml | 2 +- litebox_broker_server/src/lib.rs | 4 +- litebox_broker_server/src/server.rs | 133 ++++---- litebox_broker_transport/src/lib.rs | 94 ------ litebox_broker_unix_socket/Cargo.toml | 2 +- .../src/bin/litebox-broker-userland.rs | 8 +- litebox_broker_unix_socket/src/lib.rs | 35 +-- .../tests/userland_broker.rs | 12 +- litebox_broker_wire/Cargo.toml | 2 +- litebox_broker_wire/src/lib.rs | 288 +++++++++++++----- 24 files changed, 610 insertions(+), 390 deletions(-) rename {litebox_broker_transport => litebox_broker_channel}/Cargo.toml (83%) create mode 100644 litebox_broker_channel/src/lib.rs delete mode 100644 litebox_broker_transport/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 0595e672e..7026261c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1474,12 +1474,19 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "litebox_broker_channel" +version = "0.1.0" +dependencies = [ + "litebox_broker_protocol", +] + [[package]] name = "litebox_broker_client" version = "0.1.0" dependencies = [ + "litebox_broker_channel", "litebox_broker_protocol", - "litebox_broker_transport", ] [[package]] @@ -1494,27 +1501,20 @@ version = "0.1.0" name = "litebox_broker_server" version = "0.1.0" dependencies = [ + "litebox_broker_channel", "litebox_broker_core", "litebox_broker_protocol", - "litebox_broker_transport", -] - -[[package]] -name = "litebox_broker_transport" -version = "0.1.0" -dependencies = [ - "litebox_broker_protocol", ] [[package]] name = "litebox_broker_unix_socket" version = "0.1.0" dependencies = [ + "litebox_broker_channel", "litebox_broker_client", "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_transport", "litebox_broker_wire", ] @@ -1522,8 +1522,8 @@ dependencies = [ name = "litebox_broker_wire" version = "0.1.0" dependencies = [ + "litebox_broker_channel", "litebox_broker_protocol", - "litebox_broker_transport", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a7750211e..ebbd3fa10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ members = [ "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_transport", + "litebox_broker_channel", "litebox_broker_unix_socket", "litebox_broker_wire", "litebox_common_linux", @@ -40,7 +40,7 @@ default-members = [ "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_transport", + "litebox_broker_channel", "litebox_broker_unix_socket", "litebox_broker_wire", "litebox_common_linux", diff --git a/docs/broker-design.md b/docs/broker-design.md index d89ee33f0..0c2f1b048 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -26,7 +26,7 @@ The authority domain differs by deployment: | Deployment | Broker location | UserLiteBox host support | |---|---|---| | **Userland broker** | privileged broker process | existing host OS/user ABI | -| **Kernel broker** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry transport without decoding broker requests | +| **Kernel broker** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry channel traffic without decoding broker requests | ## Component model @@ -39,7 +39,7 @@ User mode: | v UserLiteBox + optional shim-specific user clients - -- via BrokerClient adapter over UserLiteBox transport --> + -- via BrokerClient adapter over UserLiteBox channel --> broker authority interface Hosted userland: @@ -87,17 +87,17 @@ Because UserLiteBox always runs in user mode, it can use Rust `std` heavily in d Thin in-process adapter used by UserLiteBox and shim-specific user clients to call the broker authority interface. -BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the transport supplied by UserLiteBox's host-support layer. +BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the control channel supplied by UserLiteBox's host-support layer. ### BrokerHost Kernel-side host support for UserLiteBox in broker-kernel deployments. -BrokerHost provides the user-mode execution substrate that UserLiteBox expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker transport endpoints, and possibly a compatibility ABI subset. +BrokerHost provides the user-mode execution substrate that UserLiteBox expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker channel endpoints, and possibly a compatibility ABI subset. BrokerHost is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is the host-side component that lets user-mode LiteBox processes run and reach the broker. It may be trusted kernel code, but it should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. -BrokerHost may carry broker authority traffic as transport, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry/server layer, not BrokerHost or BrokerCore. +BrokerHost may carry broker authority traffic, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry/server layer, not BrokerHost or BrokerCore. In broker-kernel deployments, BrokerHost shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; BrokerHost code is in the TCB and must be audited accordingly. @@ -134,27 +134,31 @@ The broker split should use crate names that make the authority boundary visible | Crate | Initial role | |---|---| | `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | -| `litebox_broker_core` | Protocol- and transport-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_transport` | Neutral `no_std` transport-trait crate for blocking directional request/response transport contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the transport-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | -| `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream transports reuse it rather than duplicating protocol encoding. | -| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket transport crate implementing the neutral client/server transport traits for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | -| `litebox_broker_server` | Protocol- and transport-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server transport. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | -| `litebox_broker_client` | `no_std` transport-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | +| `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | +| `litebox_broker_channel` | Neutral `no_std` channel-contract crate for blocking broker authority control-channel contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the channel-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | +| `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream channel implementations reuse it rather than duplicating protocol encoding. | +| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket channel implementation for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | +| `litebox_broker_server` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | +| `litebox_broker_client` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | | `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | -`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected transport. +`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. -Transport must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_transport`; the broker server adapts protocol and transport concepts into direct BrokerCore domain calls. Shared request/response transport traits live in `litebox_broker_transport`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_channel`; the broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. -Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in transport-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection without exposing wire tag width through the transport interface. Structurally malformed frames remain transport/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. +Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. -Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while client and server connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so clients can retry with a compatible version on expensive or credentialed transports instead of reconnecting and guessing. +Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection without exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. + +Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while client and server connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so clients can retry with a compatible version on expensive or credentialed channels instead of reconnecting and guessing. + +The known broker protocol keeps the outer envelope intentionally small. Connection-level messages such as negotiation and common errors stay at the broker layer; BrokerCore/object operations are grouped below that layer by authority domain and object family, for example `BrokerRequest::Core(CoreRequest::Event(EventRequest::Wait { .. }))`. New object families should add a nested request/response family instead of growing a flat top-level `BrokerRequest`/`BrokerResponse` operation list. The wire codec may encode those nested families as layered tags, but tag widths and unknown-tag handling remain private to the codec. Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical split: | Future crate/layer | Role | |---|---| -| `litebox_broker_host` | BrokerHost abstractions for user-mode execution support, trap/upcall/transport delivery, process/thread setup, and the UserLiteBox host ABI. | +| `litebox_broker_host` | BrokerHost abstractions for user-mode execution support, trap/upcall/channel delivery, process/thread setup, and the UserLiteBox host ABI. | | `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, BrokerServices, PolicyEngine, BrokerPlatform, and BrokerHost. | These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, broker server/entry code, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, broker server/entry code decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. @@ -192,7 +196,7 @@ BrokerRequest { PolicyEngine is not a user-callable broker target. BrokerCore and BrokerServices call it inside the authority domain before granting authority, mutating protected state, or invoking BrokerPlatform for host-visible effects. -The negotiated policy profile is bound to the authenticated broker session or deployment profile, not chosen by each request. It only needs an explicit request field if a future design supports multiple simultaneous policy profiles on one authenticated transport. +The negotiated policy profile is bound to the authenticated broker session or deployment profile, not chosen by each request. It only needs an explicit request field if a future design supports multiple simultaneous policy profiles on one authenticated channel. External broker authority APIs: @@ -261,7 +265,7 @@ UserLiteBox may use local host/kernel calls, but only for local mechanics that d |---|---|---| | private memory allocation, TLS, logging, local scratch mappings | yes | must not grant guest-visible authority | | private locks/futex-like synchronization | yes | must not represent broker-owned shared state | -| broker transport notification | yes | broker validates every request | +| broker notification channel | yes | broker validates every request | | broker-owned shared-ring data movement | yes | ring ownership, cursor movement, and frames are validated by the broker | | direct host file, network, or device access for guest-visible resources | no | must be mediated by broker-owned objects and PolicyEngine-authorized broker policy | | guest-visible mappings or executable/shared memory | only through broker/UserLiteBox mediation | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform/BrokerHost applies | @@ -335,7 +339,7 @@ The user-side runner and global broker should match through a shared deployment Shared spec crates should define: - the broker envelope and handle/memory-grant formats; -- broker transport authentication and peer identity binding; +- broker channel authentication and peer identity binding; - broker authority protocol versions; - BrokerService IDs, protocol versions, request/response types, and feature requirements; - PolicyEngine policy versions, policy profile IDs, and audit requirements; @@ -344,13 +348,13 @@ Shared spec crates should define: - control/event/data channel formats; - shared-memory/ring layout versions and validation rules; - host syscall profiles for bootstrap, fast local mode, and strict mode; -- deployment profiles that bind a shim, UserLiteBox profile, broker transport, required services, and required broker features. +- deployment profiles that bind a shim, UserLiteBox profile, broker channel, required services, and required broker features. Startup should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. 2. The runner selects a UserLiteBox profile that matches the deployment's host ABI. -3. The user side establishes an authenticated broker transport. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The first POC's Unix-socket transport returns an explicit unauthenticated placeholder credential through the same `ServerTransport` API that later authenticated transports will implement. +3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The first POC's Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `ServerControlChannel` API that later authenticated channels will implement. 4. The broker binds the caller identity used for dispatch to the authenticated peer credential. User mode does not choose its own authority identity. 5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserLiteBox, BrokerHost, channel/ring, and host-syscall-profile versions. 6. The broker replies with supported services and capabilities. @@ -415,7 +419,7 @@ The stricter baseline uses one broker per sandbox session and one sandboxed host |---|---| | sandbox session | broker | | guest process identity | BrokerCore | -| authenticated host process association | broker transport / BrokerHost or host OS | +| authenticated host process association | broker channel / BrokerHost or host OS | | guest-visible process semantics | shim + UserLiteBox, backed by BrokerCore identity | | process creation | broker-mediated | | `exec`-like ABI behavior | shim/UserLiteBox within an existing broker association unless policy requires otherwise | @@ -607,7 +611,7 @@ Authority domain: | UserLiteBox depends on unavailable host ABI | select UserLiteBox profile by deployment, or provide the needed ABI through BrokerHost | | UserLiteBox depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | | shared-memory TOCTOU or double-fetch bugs | validate broker requests against private snapshots or revalidate every use of user-controlled fields | -| unauthenticated broker transport | authenticate peers before negotiation and bind `caller_identity` to the authenticated transport endpoint | +| unauthenticated broker channel | authenticate peers before negotiation and bind `caller_identity` to the authenticated channel endpoint | | PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | | custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the host support surface justifies it | | non-delegable syscalls bypass broker mapping/resource policy | block, constrain, or trap/arbitrate them before host execution | @@ -768,10 +772,10 @@ The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-c | Current component | Target shape | |---|---| | `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserLiteBox-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | -| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserLiteBox host-support implementations; native host calls are limited to local non-authoritative mechanics and broker transport/rings. | -| `litebox_platform_linux_kernel` | broker-kernel pieces split like LVBS: privileged backend operations become BrokerPlatform, while any user-mode support/trap transport becomes BrokerHost. | +| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserLiteBox host-support implementations; native host calls are limited to local non-authoritative mechanics and broker channels/rings. | +| `litebox_platform_linux_kernel` | broker-kernel pieces split like LVBS: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes BrokerHost. | | `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserLiteBox profile, create Shim/UserLiteBox, authenticate to the broker, and negotiate deployment profile compatibility. | -| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; split external entry/transport support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | +| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; split external entry/channel support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | ## Initial implementation direction @@ -782,13 +786,13 @@ The first proof of concept should use: ```text litebox_broker_protocol litebox_broker_core -litebox_broker_transport +litebox_broker_channel litebox_broker_wire litebox_broker_unix_socket litebox_broker_server litebox_broker_client separate userland broker process -Unix-domain-socket transport implementing neutral transport traits +Unix-domain-socket channel implementing neutral control-channel traits control channel only minimal PolicyEngine broker-owned event object @@ -800,11 +804,11 @@ Then proceed incrementally: 1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and transport-neutral. -4. Create `litebox_broker_transport` with neutral client/server request-response traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from transport/protocol requests to direct BrokerCore domain calls. +3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. +4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls. 5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. -7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future transports can implement the same neutral transport traits. +7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. 10. Make UserLiteBox handle tables broker-backed views for migrated object families. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 2b837feb6..ac1f8c06e 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -14,7 +14,7 @@ Authority domain: broker entry/server + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform Kernel-broker deployments: - BrokerHost supports user-mode UserLiteBox execution and broker transport + BrokerHost supports user-mode UserLiteBox execution and broker channel ``` The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/event/data channels, authenticated per-process broker associations, and fail-closed behavior. @@ -24,7 +24,9 @@ The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation - Build vertical slices, not a big-bang refactor. - Keep UserLiteBox untrusted and broker authority explicit. - Keep BrokerCore shim-neutral. -- Keep BrokerCore protocol-neutral and transport-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/server code adapts protocol requests and transport credentials before calling it. +- Keep BrokerCore protocol-neutral and channel-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/server code adapts protocol requests and channel credentials before calling it. +- Keep the broker protocol modular: the outer request/response envelope is for connection-level broker messages and coarse authority routing, while object/domain operations live in nested request/response families such as `CoreRequest::Event` and `EventResponse`. +- Keep the control channel strictly paired request/response; broker-initiated readiness, interrupt, fault, revocation, and session-failure messages should use a separate notification channel/message family. - Put domain-specific authority in BrokerServices. - Put final allow/deny/audit decisions in PolicyEngine. - Keep BrokerPlatform as authorized backend execution, not a policy owner. @@ -57,7 +59,7 @@ Initial contents: - protocol version type; - broker event reference handles with reference generations; -- minimal event request/response messages; +- small broker request/response envelope with minimal nested event request/response families; - readiness and wait outcome payloads; - ABI-neutral error categories; @@ -81,11 +83,11 @@ Initial scope: - at least one authenticated process association within the broker session; - control channel only; - major-version/minor-compatible protocol negotiation; -- neutral blocking `no_std` request/response transport traits with transport-specific error types and explicit clean-close receive semantics; -- transport-produced peer credentials returned through the server transport trait and mapped by the broker server into BrokerCore caller credentials; -- reusable `no_std + alloc` request/response wire codec for byte-stream transports; -- Unix-domain-socket framing as the first concrete userland transport crate; -- a Unix-socket executable that wires the generic transport-neutral server to the concrete Unix transport; +- neutral blocking `no_std` control-channel traits with channel-specific error types and explicit clean-close receive semantics; +- channel-produced peer credentials returned through the server control-channel trait and mapped by the broker server into BrokerCore caller credentials; +- reusable `no_std + alloc` request/response wire codec for byte-stream channel implementations; +- Unix-domain-socket framing as the first concrete userland channel implementation; +- a Unix-socket executable that wires the generic channel-neutral server to the concrete Unix control-channel implementation; - server-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; - BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and connection cleanup; - default-deny PolicyEngine; @@ -94,14 +96,16 @@ Initial scope: Exit criteria: - UserLiteBox can connect and negotiate. -- Broker binds caller identity to the authenticated transport. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. -- Userland transport code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. -- BrokerCore has no dependency on `litebox_broker_protocol`, `litebox_broker_transport`, wire codecs, or concrete IPC crates. -- Client code does not need to depend on the userland broker server crate to use the first Unix socket transport. -- The generic broker server library does not depend on concrete Unix socket transport code and remains `no_std`. +- Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. +- Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. +- BrokerCore has no dependency on `litebox_broker_protocol`, `litebox_broker_channel`, wire codecs, or concrete IPC crates. +- Client code does not need to depend on the userland broker server crate to use the first Unix socket channel. +- The generic broker server library does not depend on concrete Unix socket channel code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. - Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly; newer core error categories or wait outcomes that the server adapter cannot represent return `Internal`. - Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so clients can downgrade without reconnecting or guessing. +- BrokerCore/object operations are grouped below the broker envelope instead of added as unrelated top-level `BrokerRequest` and `BrokerResponse` variants. +- Control-channel contracts live in `litebox_broker_channel` and stay separate from semantic protocol messages. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. ## Phase 3: UserLiteBox facade @@ -169,7 +173,7 @@ Exit criteria: - Double close, stale fd, dup, inherited refs, and process-exit cleanup are tested. - UserLiteBox cannot create a live broker object by editing local fd state. -## Phase 6: Control/event/data transport +## Phase 6: Control/notification/data channels Add the durable-unicorn-style channel split. @@ -326,7 +330,7 @@ Only after userland semantics are stable, implement broker-kernel deployment. Split trusted deployment code into: -- BrokerHost: user-mode execution support and transport delivery; +- BrokerHost: user-mode execution support and channel delivery; - BrokerPlatform: privileged backend execution; - BrokerCore/BrokerServices/PolicyEngine: shared authority logic. @@ -379,7 +383,7 @@ Add conformance tests at each layer: - protocol parsing rejects malformed frames; - policy default-denies unknown operations; -- caller identity is transport-bound; +- caller identity is channel-bound; - stale reference IDs fail; - wrong reference generation fails; - process disconnect cleans up refs; diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_channel/Cargo.toml similarity index 83% rename from litebox_broker_transport/Cargo.toml rename to litebox_broker_channel/Cargo.toml index 303581c46..f98acb10b 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_channel/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "litebox_broker_transport" +name = "litebox_broker_channel" version = "0.1.0" edition = "2024" @@ -8,4 +8,3 @@ litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1. [lints] workspace = true - diff --git a/litebox_broker_channel/src/lib.rs b/litebox_broker_channel/src/lib.rs new file mode 100644 index 000000000..9c182d766 --- /dev/null +++ b/litebox_broker_channel/src/lib.rs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Shared broker channel contracts. +//! +//! This crate defines delivery contracts for broker authority messages. The +//! current surface is intentionally limited to the paired control channel: +//! one [`BrokerRequest`](litebox_broker_protocol::BrokerRequest) produces one +//! [`BrokerResponse`](litebox_broker_protocol::BrokerResponse). Concrete IPC +//! implementations own framing, buffering, authentication, and the mechanism; +//! non-blocking IPCs can provide a blocking adapter at this boundary. +//! +//! Broker-initiated readiness, interrupt, fault, revocation, or session-failure +//! traffic must use a separately named notification channel and notification +//! message family when that lane is introduced. Such notifications must not be +//! delivered as unsolicited control-channel responses. + +#![no_std] + +use litebox_broker_protocol::{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 server 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 [`ServerControlChannel::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, Copy, 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, Copy, 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) + } +} + +/// Client-side control channel for broker authority calls. +pub trait ClientControlChannel { + /// 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>; +} + +/// Server-side control channel for broker authority calls. +pub trait ServerControlChannel { + /// 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_client/Cargo.toml b/litebox_broker_client/Cargo.toml index 2ae0ddffc..f7acbc69b 100644 --- a/litebox_broker_client/Cargo.toml +++ b/litebox_broker_client/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_client/src/error.rs b/litebox_broker_client/src/error.rs index 95b0181b6..d112457e0 100644 --- a/litebox_broker_client/src/error.rs +++ b/litebox_broker_client/src/error.rs @@ -9,14 +9,14 @@ use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; #[derive(Debug)] #[non_exhaustive] pub enum ClientError { - /// The transport failed. - Transport(E), + /// The control channel failed. + Channel(E), /// An operation requiring an active broker session was called before negotiation. NotNegotiated, /// Negotiation was requested after the client was already active. AlreadyNegotiated, - /// The broker closed the transport before returning a response. - TransportClosed, + /// The broker closed the channel before returning a response. + ChannelClosed, /// The broker returned a response this client does not understand. UnknownResponse, /// The broker accepted negotiation with a version that cannot serve the request. @@ -42,10 +42,10 @@ pub enum ClientError { impl fmt::Display for ClientError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Transport(error) => write!(f, "broker transport failed: {error}"), + Self::Channel(error) => write!(f, "broker channel failed: {error}"), Self::NotNegotiated => write!(f, "broker client has not negotiated protocol version"), Self::AlreadyNegotiated => f.write_str("broker client already negotiated"), - Self::TransportClosed => write!(f, "broker closed the transport"), + Self::ChannelClosed => write!(f, "broker closed the channel"), Self::UnknownResponse => f.write_str("unknown broker response"), Self::IncompatibleNegotiation { requested, @@ -75,11 +75,11 @@ where { fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { match self { - Self::Transport(error) => Some(error), + Self::Channel(error) => Some(error), Self::Broker(error) => Some(error), Self::NotNegotiated | Self::AlreadyNegotiated - | Self::TransportClosed + | Self::ChannelClosed | Self::UnknownResponse | Self::IncompatibleNegotiation { .. } | Self::UnsupportedVersion { .. } diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_client/src/event.rs index 9b4f51d1c..2082aa34f 100644 --- a/litebox_broker_client/src/event.rs +++ b/litebox_broker_client/src/event.rs @@ -2,19 +2,22 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ObjectHandle, ReadinessState, WaitOutcome, + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, + ObjectHandle, ReadinessState, WaitOutcome, }; -use litebox_broker_transport::ClientTransport; +use litebox_broker_channel::ClientControlChannel; use crate::{BrokerClient, ClientError, Result}; -impl BrokerClient { +impl BrokerClient { /// Creates a broker-owned event object. pub fn create_event(&mut self) -> Result { self.ensure_negotiated()?; - match self.request(BrokerRequest::CreateEvent)? { - BrokerResponse::Handle(handle) => Ok(handle), + match self.request(event_request(EventRequest::Create))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => { + Ok(handle) + } response => Err(ClientError::UnexpectedResponse(response)), } } @@ -22,8 +25,10 @@ impl BrokerClient { /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { self.ensure_negotiated()?; - match self.request(BrokerRequest::WaitEvent { handle })? { - BrokerResponse::Wait(outcome) => Ok(outcome), + match self.request(event_request(EventRequest::Wait { handle }))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait { outcome })) => { + Ok(outcome) + } response => Err(ClientError::UnexpectedResponse(response)), } } @@ -31,9 +36,15 @@ impl BrokerClient { /// Signals a broker-owned event object. pub fn signal_event(&mut self, handle: ObjectHandle) -> Result { self.ensure_negotiated()?; - match self.request(BrokerRequest::SignalEvent { handle })? { - BrokerResponse::Readiness(readiness) => Ok(readiness), + match self.request(event_request(EventRequest::Signal { handle }))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Signaled { readiness })) => { + Ok(readiness) + } response => Err(ClientError::UnexpectedResponse(response)), } } } + +const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) +} diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index 401d4f4c8..228dfd46d 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -3,9 +3,9 @@ //! Typed client adapter for broker requests. //! -//! The client owns request/response sequencing but does not own a transport. -//! Userland, kernel, or ring-buffer deployments can provide transports by -//! implementing [`litebox_broker_transport::ClientTransport`]. +//! The client owns request/response sequencing but does not own a channel. +//! Userland, kernel, or ring-buffer deployments can provide channels by +//! implementing [`litebox_broker_channel::ClientControlChannel`]. #![no_std] @@ -16,8 +16,8 @@ mod error; mod event; mod negotiate; +use litebox_broker_channel::{ClientControlChannel, ReceivedBrokerResponse}; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; -use litebox_broker_transport::{ClientTransport, ReceivedResponse}; pub use error::{ClientError, Result}; @@ -26,7 +26,7 @@ pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); /// Typed client for broker operations. pub struct BrokerClient { - transport: T, + channel: T, state: ConnectionState, } @@ -39,16 +39,16 @@ enum ConnectionState { } impl BrokerClient { - /// Creates a broker client over an already-connected transport. - pub const fn new(transport: T) -> Self { + /// Creates a broker client over an already-connected control channel. + pub const fn new(channel: T) -> Self { Self { - transport, + channel, state: ConnectionState::AwaitingNegotiation, } } } -impl BrokerClient { +impl BrokerClient { /// Returns the effective protocol version this connection negotiated. /// /// Feature gating must use this effective version because the broker may @@ -72,19 +72,19 @@ impl BrokerClient { } pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { - self.transport + self.channel .send_request(&request) - .map_err(ClientError::Transport)?; + .map_err(ClientError::Channel)?; match self - .transport + .channel .recv_response() - .map_err(ClientError::Transport)? - .ok_or(ClientError::TransportClosed)? + .map_err(ClientError::Channel)? + .ok_or(ClientError::ChannelClosed)? { - ReceivedResponse::Response(BrokerResponse::Error(error)) => { + ReceivedBrokerResponse::Response(BrokerResponse::Error(error)) => { Err(ClientError::Broker(error)) } - ReceivedResponse::Response(response) => Ok(response), + ReceivedBrokerResponse::Response(response) => Ok(response), _ => Err(ClientError::UnknownResponse), } } @@ -98,14 +98,15 @@ mod tests { #[test] fn event_operations_require_negotiation_without_sending() { - let transport = FakeTransport::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); - let mut client = BrokerClient::new(transport); + let channel = + FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); + let mut client = BrokerClient::new(channel); assert!(matches!( client.create_event(), Err(ClientError::NotNegotiated) )); - assert_eq!(client.transport.sent_request, None); + assert_eq!(client.channel.sent_request, None); } #[test] @@ -114,14 +115,14 @@ mod tests { CLIENT_PROTOCOL_VERSION.major, CLIENT_PROTOCOL_VERSION.minor - 1, ); - let transport = FakeTransport::new(Some(BrokerResponse::Negotiated { + let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: CLIENT_PROTOCOL_VERSION, })); - let mut client = BrokerClient::new(transport); + let mut client = BrokerClient::new(channel); assert_eq!(client.negotiate_version(requested).unwrap(), requested); assert_eq!( - client.transport.sent_request, + client.channel.sent_request, Some(BrokerRequest::Negotiate { protocol_version: requested }) @@ -132,10 +133,10 @@ mod tests { #[test] fn negotiate_version_rejects_incompatible_broker_response() { let requested = ProtocolVersion::new(1, 1); - let transport = FakeTransport::new(Some(BrokerResponse::Negotiated { + let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: ProtocolVersion::new(1, 0), })); - let mut client = BrokerClient::new(transport); + let mut client = BrokerClient::new(channel); assert!(matches!( client.negotiate_version(requested), @@ -155,10 +156,10 @@ mod tests { CLIENT_PROTOCOL_VERSION.minor + 1, ); let fallback = CLIENT_PROTOCOL_VERSION; - let transport = FakeTransport::new(Some(BrokerResponse::VersionMismatch { + let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { broker_protocol_version: fallback, })); - let mut client = BrokerClient::new(transport); + let mut client = BrokerClient::new(channel); assert!(matches!( client.negotiate_version(too_new), @@ -169,7 +170,7 @@ mod tests { )); assert_eq!(client.negotiated_protocol_version(), None); - client.transport.response = Some(BrokerResponse::Negotiated { + client.channel.response = Some(BrokerResponse::Negotiated { broker_protocol_version: fallback, }); @@ -177,12 +178,12 @@ mod tests { assert_eq!(client.negotiated_protocol_version(), Some(fallback)); } - struct FakeTransport { + struct FakeControlChannel { sent_request: Option, response: Option, } - impl FakeTransport { + impl FakeControlChannel { const fn new(response: Option) -> Self { Self { sent_request: None, @@ -191,7 +192,7 @@ mod tests { } } - impl ClientTransport for FakeTransport { + impl ClientControlChannel for FakeControlChannel { type Error = Infallible; fn send_request( @@ -202,8 +203,10 @@ mod tests { Ok(()) } - fn recv_response(&mut self) -> core::result::Result, Self::Error> { - Ok(self.response.take().map(ReceivedResponse::Response)) + fn recv_response( + &mut self, + ) -> core::result::Result, Self::Error> { + Ok(self.response.take().map(ReceivedBrokerResponse::Response)) } } } diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_client/src/negotiate.rs index d6bdd2859..9983c1474 100644 --- a/litebox_broker_client/src/negotiate.rs +++ b/litebox_broker_client/src/negotiate.rs @@ -3,11 +3,11 @@ use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; -use litebox_broker_transport::ClientTransport; +use litebox_broker_channel::ClientControlChannel; use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION, ClientError, Result}; -impl BrokerClient { +impl BrokerClient { /// Negotiates the default client protocol version. /// /// Returns the effective protocol version this connection will speak. diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 9dbc5f75f..a0caeeb58 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Protocol- and transport-independent broker authority core. +//! Protocol- and channel-independent broker authority core. //! //! `litebox_broker_core` owns broker-side object identity, reference lifetime, //! rights checks, reference generation checks, and policy calls. It deliberately has no //! dependency on protocol request/response types, Unix sockets, shared-memory -//! rings, or any other transport. +//! rings, kernel traps, or any other channel implementation. #![no_std] @@ -38,7 +38,7 @@ pub use types::{ObjectRights, ObjectType}; /// BrokerCore result type. pub type Result = core::result::Result; -/// Transport-independent broker authority state. +/// Channel-independent broker authority state. pub struct BrokerCore

{ policy: P, next_process_id: u64, diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index f28cfcaff..c07750a78 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -3,7 +3,7 @@ //! Shared broker protocol types. //! -//! This crate is intentionally transport-neutral. It describes broker-visible +//! This crate is intentionally channel-neutral. It describes broker-visible //! opaque handles, errors, and versions, but does not know whether the bytes //! move over Unix sockets, shared rings, kernel traps, or another IPC mechanism. @@ -14,7 +14,10 @@ mod message; mod object; pub use error::ErrorCode; -pub use message::{BrokerRequest, BrokerResponse, ReadinessState, WaitOutcome}; +pub use message::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, + ReadinessState, WaitOutcome, +}; pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; /// Major/minor broker protocol version. diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 6f6cd8dc8..d81295175 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -30,6 +30,10 @@ pub enum WaitOutcome { } /// Broker request transported 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, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum BrokerRequest { @@ -38,21 +42,41 @@ pub enum BrokerRequest { /// Required protocol version. protocol_version: ProtocolVersion, }, + /// BrokerCore authority request. + Core(CoreRequest), +} + +/// Request adapted by the broker server into a BrokerCore domain call. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreRequest { + /// Event object request family. + Event(EventRequest), +} + +/// Broker-owned event object request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventRequest { /// Create a broker-owned event object. - CreateEvent, + Create, /// Check whether an event wait would complete now. - WaitEvent { + Wait { /// Event handle. handle: ObjectHandle, }, /// Signal an event. - SignalEvent { + Signal { /// Event handle. handle: ObjectHandle, }, } /// Broker response transported 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, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum BrokerResponse { @@ -74,12 +98,28 @@ pub enum BrokerResponse { /// 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, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum CoreResponse { + /// Event object response family. + Event(EventResponse), +} + +/// Broker-owned event object response. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventResponse { /// Operation returned a broker object handle. - Handle(ObjectHandle), + Created { handle: ObjectHandle }, /// Operation returned readiness state. - Readiness(ReadinessState), + Signaled { readiness: ReadinessState }, /// Operation returned wait state. - Wait(WaitOutcome), - /// Operation failed with an ABI-neutral broker error. - Error(ErrorCode), + Wait { outcome: WaitOutcome }, } diff --git a/litebox_broker_server/Cargo.toml b/litebox_broker_server/Cargo.toml index 311d07d05..b1d9ac848 100644 --- a/litebox_broker_server/Cargo.toml +++ b/litebox_broker_server/Cargo.toml @@ -6,7 +6,7 @@ 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" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_server/src/lib.rs index a6edeabfc..c7675cff3 100644 --- a/litebox_broker_server/src/lib.rs +++ b/litebox_broker_server/src/lib.rs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Transport-neutral broker server loop for the split-broker proof of concept. +//! Channel-neutral broker server loop for the split-broker proof of concept. //! //! This crate wires `litebox_broker_core` to any implementation of the neutral -//! server transport trait. Concrete transports live in separate crates such as +//! server control-channel trait. Concrete channels live in separate crates such as //! `litebox_broker_unix_socket`. #![no_std] diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 8b25f0553..823654710 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -3,6 +3,7 @@ use core::fmt; +use litebox_broker_channel::{PeerCredential, ReceivedBrokerRequest, ServerControlChannel}; use litebox_broker_core::{ BrokerConnection, BrokerCore, BrokerError, CallerCredential, ObjectHandle as CoreObjectHandle, ObjectReferenceGeneration as CoreObjectReferenceGeneration, @@ -10,61 +11,58 @@ use litebox_broker_core::{ WaitOutcome as CoreWaitOutcome, }; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ErrorCode, ObjectHandle as ProtocolObjectHandle, + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, EventRequest, + EventResponse, ObjectHandle as ProtocolObjectHandle, ObjectReferenceGeneration as ProtocolObjectReferenceGeneration, ObjectReferenceId as ProtocolObjectReferenceId, ProtocolVersion, ReadinessState as ProtocolReadinessState, WaitOutcome as ProtocolWaitOutcome, }; -use litebox_broker_transport::{PeerCredential, ReceivedRequest, ServerTransport}; /// Protocol version this broker server implementation supports. pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); -/// Serves one broker connection over the provided connected transport. +/// Serves one broker connection over the provided connected control channel. pub fn serve_connection( core: &mut BrokerCore

, - transport: &mut T, + channel: &mut T, ) -> Result> where P: PolicyEngine, - T: ServerTransport, + T: ServerControlChannel, { - let peer_credential = transport + let peer_credential = channel .peer_credential() - .map_err(BrokerServeError::Transport)?; + .map_err(BrokerServeError::Channel)?; let caller_credential = caller_credential_from_peer(peer_credential) .map_err(|()| BrokerServeError::ConnectionSetup)?; let connection = core .create_connection(caller_credential) .map_err(|_error| BrokerServeError::ConnectionSetup)?; - let result = serve_request_loop(core, transport, &connection); + let result = serve_request_loop(core, channel, &connection); core.close_connection(connection); result } fn serve_request_loop( core: &mut BrokerCore

, - transport: &mut T, + channel: &mut T, connection: &BrokerConnection, ) -> Result> where P: PolicyEngine, - T: ServerTransport, + T: ServerControlChannel, { let mut state = ConnectionState::AwaitingNegotiation; loop { - let Some(received) = transport - .recv_request() - .map_err(BrokerServeError::Transport)? - else { + let Some(received) = channel.recv_request().map_err(BrokerServeError::Channel)? else { break; }; let dispatch = handle_received_request(core, connection, &mut state, received); - transport + channel .send_response(&dispatch.response) - .map_err(BrokerServeError::Transport)?; + .map_err(BrokerServeError::Channel)?; if let DispatchOutcome::Close(reason) = dispatch.outcome { return Ok(ConnectionTermination::BrokerClosed(reason)); } @@ -85,10 +83,10 @@ fn handle_received_request( core: &mut BrokerCore

, connection: &BrokerConnection, state: &mut ConnectionState, - received: ReceivedRequest, + received: ReceivedBrokerRequest, ) -> BrokerDispatch { match received { - ReceivedRequest::Request(request) => handle_request(core, connection, state, request), + ReceivedBrokerRequest::Request(request) => handle_request(core, connection, state, request), _ => handle_unknown_request(*state), } } @@ -126,23 +124,39 @@ fn handle_active_request( BrokerResponse::Error(ErrorCode::ProtocolState), CloseReason::ProtocolViolation, ), - BrokerRequest::CreateEvent => BrokerDispatch::continue_after(handle_core_result( - core.create_event(connection), - |handle| BrokerResponse::Handle(protocol_handle(handle)), - )), - BrokerRequest::WaitEvent { handle } => BrokerDispatch::continue_after(handle_wait_result( - core.wait_event(connection, core_handle(handle)), - )), - BrokerRequest::SignalEvent { handle } => { - BrokerDispatch::continue_after(handle_core_result( - core.signal_event(connection, core_handle(handle)), - |readiness| BrokerResponse::Readiness(protocol_readiness_state(readiness)), - )) + BrokerRequest::Core(CoreRequest::Event(request)) => { + BrokerDispatch::continue_after(handle_event_request(core, connection, request)) } _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), } } +fn handle_event_request( + core: &mut BrokerCore

, + connection: &BrokerConnection, + request: EventRequest, +) -> BrokerResponse { + match request { + EventRequest::Create => handle_core_result(core.create_event(connection), |handle| { + event_response(EventResponse::Created { + handle: protocol_handle(handle), + }) + }), + EventRequest::Wait { handle } => { + handle_wait_result(core.wait_event(connection, core_handle(handle))) + } + EventRequest::Signal { handle } => handle_core_result( + core.signal_event(connection, core_handle(handle)), + |readiness| { + event_response(EventResponse::Signaled { + readiness: protocol_readiness_state(readiness), + }) + }, + ), + _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), + } +} + fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { if state == ConnectionState::AwaitingNegotiation { BrokerDispatch::close_after( @@ -185,13 +199,17 @@ fn handle_core_result( fn handle_wait_result(result: litebox_broker_core::Result) -> BrokerResponse { match result { Ok(outcome) => match protocol_wait_outcome(outcome) { - Some(outcome) => BrokerResponse::Wait(outcome), + Some(outcome) => event_response(EventResponse::Wait { outcome }), None => BrokerResponse::Error(ErrorCode::Internal), }, Err(error) => BrokerResponse::Error(protocol_error(error)), } } +const fn event_response(response: EventResponse) -> BrokerResponse { + BrokerResponse::Core(CoreResponse::Event(response)) +} + fn protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, @@ -274,8 +292,6 @@ impl BrokerDispatch { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CloseReason { - /// The peer requested an unsupported protocol version. - UnsupportedVersion, /// The peer violated the request sequencing state machine. ProtocolViolation, } @@ -283,7 +299,6 @@ pub enum CloseReason { impl fmt::Display for CloseReason { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::UnsupportedVersion => f.write_str("unsupported protocol version"), Self::ProtocolViolation => f.write_str("protocol violation"), } } @@ -293,7 +308,7 @@ impl fmt::Display for CloseReason { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ConnectionTermination { - /// The peer cleanly closed the transport. + /// The peer cleanly closed the channel. PeerClosed, /// The server sent a terminal protocol response and closed the connection. BrokerClosed(CloseReason), @@ -305,15 +320,15 @@ pub enum ConnectionTermination { pub enum BrokerServeError { /// The server could not allocate or map state for a new connection. ConnectionSetup, - /// The concrete transport failed. - Transport(E), + /// The concrete channel failed. + Channel(E), } impl fmt::Display for BrokerServeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ConnectionSetup => f.write_str("broker connection setup failed"), - Self::Transport(error) => write!(f, "broker transport failed: {error}"), + Self::Channel(error) => write!(f, "broker channel failed: {error}"), } } } @@ -325,7 +340,7 @@ where fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { match self { Self::ConnectionSetup => None, - Self::Transport(error) => Some(error), + Self::Channel(error) => Some(error), } } } @@ -343,7 +358,7 @@ mod tests { &mut core, &connection, &mut state, - BrokerRequest::CreateEvent, + event_request(EventRequest::Create), ); assert_eq!( @@ -467,8 +482,12 @@ mod tests { let (mut core, connection, mut state) = new_connection(); negotiate(&mut core, &connection, &mut state); - let dispatch = - handle_received_request(&mut core, &connection, &mut state, ReceivedRequest::Unknown); + let dispatch = handle_received_request( + &mut core, + &connection, + &mut state, + ReceivedBrokerRequest::Unknown, + ); assert_eq!( dispatch.response, @@ -481,8 +500,12 @@ mod tests { fn dispatch_closes_unknown_requests_before_negotiation() { let (mut core, connection, mut state) = new_connection(); - let dispatch = - handle_received_request(&mut core, &connection, &mut state, ReceivedRequest::Unknown); + let dispatch = handle_received_request( + &mut core, + &connection, + &mut state, + ReceivedBrokerRequest::Unknown, + ); assert_eq!( dispatch.response, @@ -524,10 +547,10 @@ mod tests { &mut core, &connection, &mut state, - BrokerRequest::CreateEvent, + event_request(EventRequest::Create), ); let handle = match dispatch.response { - BrokerResponse::Handle(handle) => handle, + BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, response => panic!("unexpected response: {response:?}"), }; @@ -535,13 +558,13 @@ mod tests { &mut core, &connection, &mut state, - BrokerRequest::WaitEvent { handle }, + event_request(EventRequest::Wait { handle }), ); assert_eq!( dispatch.response, - BrokerResponse::Wait(ProtocolWaitOutcome::WouldBlock( - ProtocolReadinessState::new(false, 0) - )) + event_response(EventResponse::Wait { + outcome: ProtocolWaitOutcome::WouldBlock(ProtocolReadinessState::new(false, 0)) + }) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } @@ -564,10 +587,10 @@ mod tests { &mut core, &owner, &mut owner_state, - BrokerRequest::CreateEvent, + event_request(EventRequest::Create), ); let handle = match dispatch.response { - BrokerResponse::Handle(handle) => handle, + BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, response => panic!("unexpected response: {response:?}"), }; @@ -575,7 +598,7 @@ mod tests { &mut core, &other, &mut other_state, - BrokerRequest::WaitEvent { handle }, + event_request(EventRequest::Wait { handle }), ); assert_eq!( dispatch.response, @@ -644,4 +667,8 @@ mod tests { } ); } + + const fn event_request(request: EventRequest) -> BrokerRequest { + BrokerRequest::Core(CoreRequest::Event(request)) + } } diff --git a/litebox_broker_transport/src/lib.rs b/litebox_broker_transport/src/lib.rs deleted file mode 100644 index aaf693622..000000000 --- a/litebox_broker_transport/src/lib.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Shared broker transport traits. -//! -//! This crate defines blocking request/response direction traits only. Concrete -//! transports own framing, buffering, and the IPC mechanism; non-blocking IPCs -//! can provide a blocking adapter at this boundary. - -#![no_std] - -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; - -/// Peer identity information supplied by the transport or host layer. -/// -/// The first userland proof of concept does not authenticate Unix-socket peers, -/// but transports still return an explicit credential value so the server 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. - /// - /// Transports that are expected to authenticate peers must return an error - /// from [`ServerTransport::peer_credential`] when authentication is - /// unavailable or fails; this variant is only for deployments that - /// deliberately choose unauthenticated operation. - Unauthenticated, -} - -/// Request received from a transport. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum ReceivedRequest { - /// 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 ReceivedRequest { - fn from(request: BrokerRequest) -> Self { - Self::Request(request) - } -} - -/// Response received from a transport. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum ReceivedResponse { - /// 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 ReceivedResponse { - fn from(response: BrokerResponse) -> Self { - Self::Response(response) - } -} - -/// Client-side request/response transport for broker calls. -pub trait ClientTransport { - /// Transport-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 transport cleanly before - /// starting another response frame. - fn recv_response(&mut self) -> Result, Self::Error>; -} - -/// Server-side request/response transport for broker calls. -pub trait ServerTransport { - /// Transport-specific error type. - type Error; - - /// Returns the peer credential authenticated for this transport endpoint. - fn peer_credential(&self) -> Result; - - /// Receives one broker request. - /// - /// Returns `Ok(None)` when the peer closed the transport 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_unix_socket/Cargo.toml b/litebox_broker_unix_socket/Cargo.toml index 20f0597f5..770f05918 100644 --- a/litebox_broker_unix_socket/Cargo.toml +++ b/litebox_broker_unix_socket/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_broker_server = { path = "../litebox_broker_server", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } litebox_broker_wire = { path = "../litebox_broker_wire", version = "0.1.0" } [[bin]] diff --git a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs index 1f25899ad..1306dce15 100644 --- a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs +++ b/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs @@ -8,15 +8,15 @@ use std::path::PathBuf; use litebox_broker_core::{BrokerCore, EventOnlyPolicy}; use litebox_broker_server::{BrokerServeError, serve_connection}; -use litebox_broker_unix_socket::UnixStreamServerTransport; +use litebox_broker_unix_socket::UnixStreamServerControlChannel; fn main() -> io::Result<()> { let args = Args::parse(env::args().skip(1))?; let listener = UnixListener::bind(&args.socket_path)?; let (stream, _) = listener.accept()?; - let mut transport = UnixStreamServerTransport::from_accepted(stream); + let mut channel = UnixStreamServerControlChannel::from_accepted(stream); let mut broker = BrokerCore::new(EventOnlyPolicy); - serve_connection(&mut broker, &mut transport) + serve_connection(&mut broker, &mut channel) .map(|_| ()) .map_err(broker_error) } @@ -24,7 +24,7 @@ fn main() -> io::Result<()> { fn broker_error(error: BrokerServeError) -> io::Error { match error { BrokerServeError::ConnectionSetup => io::Error::other("broker connection setup failed"), - BrokerServeError::Transport(error) => error, + BrokerServeError::Channel(error) => error, error => io::Error::other(error.to_string()), } } diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 823a4dde9..6971dd041 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -1,31 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Unix-domain-socket broker transport for hosted userland deployments. +//! Unix-domain-socket broker channel for hosted userland deployments. //! //! This crate deliberately uses `std` because Unix-domain sockets and `std::io` //! framing are hosted userland concerns. Portable broker interfaces live in the -//! no_std protocol, wire, transport, client, core, and server crates. +//! no_std protocol, wire, channel, client, core, and server crates. use std::io::{self, Read, Write}; use std::os::unix::net::UnixStream; use std::path::Path; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; -use litebox_broker_transport::{ - ClientTransport, PeerCredential, ReceivedRequest, ReceivedResponse, ServerTransport, +use litebox_broker_channel::{ + ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, + ServerControlChannel, }; +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; use litebox_broker_wire::{decode_request, decode_response, encode_request, encode_response}; const MAX_FRAME_LEN: usize = 64 * 1024; -/// Client-side Unix-domain-socket transport for the hosted userland POC. -pub struct UnixStreamClientTransport { +/// Client-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamClientControlChannel { stream: UnixStream, } -impl UnixStreamClientTransport { - /// Creates a client transport from an already-connected Unix stream. +impl UnixStreamClientControlChannel { + /// Creates a client control channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { Self { stream } } @@ -36,19 +37,19 @@ impl UnixStreamClientTransport { } } -/// Server-side Unix-domain-socket transport for the hosted userland POC. -pub struct UnixStreamServerTransport { +/// Server-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamServerControlChannel { stream: UnixStream, } -impl UnixStreamServerTransport { - /// Creates a server transport from an accepted Unix stream. +impl UnixStreamServerControlChannel { + /// Creates a server control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { Self { stream } } } -impl ClientTransport for UnixStreamClientTransport { +impl ClientControlChannel for UnixStreamClientControlChannel { type Error = io::Error; fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { @@ -58,7 +59,7 @@ impl ClientTransport for UnixStreamClientTransport { ) } - fn recv_response(&mut self) -> io::Result> { + fn recv_response(&mut self) -> io::Result> { let Some(frame) = read_frame(&mut self.stream)? else { return Ok(None); }; @@ -66,14 +67,14 @@ impl ClientTransport for UnixStreamClientTransport { } } -impl ServerTransport for UnixStreamServerTransport { +impl ServerControlChannel for UnixStreamServerControlChannel { type Error = io::Error; fn peer_credential(&self) -> io::Result { Ok(PeerCredential::Unauthenticated) } - fn recv_request(&mut self) -> io::Result> { + fn recv_request(&mut self) -> io::Result> { let Some(frame) = read_frame(&mut self.stream)? else { return Ok(None); }; diff --git a/litebox_broker_unix_socket/tests/userland_broker.rs b/litebox_broker_unix_socket/tests/userland_broker.rs index fa5d738a0..b7c738a3d 100644 --- a/litebox_broker_unix_socket/tests/userland_broker.rs +++ b/litebox_broker_unix_socket/tests/userland_broker.rs @@ -12,14 +12,14 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use litebox_broker_client::BrokerClient; use litebox_broker_protocol::{ReadinessState, WaitOutcome}; use litebox_broker_server::SUPPORTED_PROTOCOL_VERSION; -use litebox_broker_unix_socket::UnixStreamClientTransport; +use litebox_broker_unix_socket::UnixStreamClientControlChannel; #[test] fn separate_process_broker_serves_event_object_requests() { let socket_path = unique_socket_path(); let mut child = spawn_broker(&socket_path); - let transport = connect_with_retry(&socket_path).unwrap(); - let mut client = BrokerClient::new(transport); + let channel = connect_with_retry(&socket_path).unwrap(); + let mut client = BrokerClient::new(channel); assert_eq!(client.negotiate().unwrap(), SUPPORTED_PROTOCOL_VERSION); @@ -52,11 +52,11 @@ fn spawn_broker(socket_path: &Path) -> Child { .unwrap() } -fn connect_with_retry(socket_path: &Path) -> io::Result { +fn connect_with_retry(socket_path: &Path) -> io::Result { let deadline = Instant::now() + Duration::from_secs(5); loop { - match UnixStreamClientTransport::connect(socket_path) { - Ok(transport) => return Ok(transport), + match UnixStreamClientControlChannel::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 diff --git a/litebox_broker_wire/Cargo.toml b/litebox_broker_wire/Cargo.toml index fb1ef8b67..a174be7a3 100644 --- a/litebox_broker_wire/Cargo.toml +++ b/litebox_broker_wire/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_transport = { path = "../litebox_broker_transport", version = "0.1.0" } +litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index 536dc1b37..c46fb9836 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -10,23 +10,28 @@ extern crate alloc; use core::fmt; use alloc::vec::Vec; +use litebox_broker_channel::{ReceivedBrokerRequest, ReceivedBrokerResponse}; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ErrorCode, ObjectHandle, ObjectReferenceGeneration, - ObjectReferenceId, ProtocolVersion, ReadinessState, WaitOutcome, + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, EventRequest, + EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, + ReadinessState, WaitOutcome, }; -use litebox_broker_transport::{ReceivedRequest, ReceivedResponse}; const REQUEST_TAG_NEGOTIATE: u8 = 0; -const REQUEST_TAG_CREATE_EVENT: u8 = 1; -const REQUEST_TAG_WAIT_EVENT: u8 = 2; -const REQUEST_TAG_SIGNAL_EVENT: u8 = 3; +const REQUEST_TAG_CORE: u8 = 1; +const CORE_REQUEST_TAG_EVENT: u8 = 0; +const EVENT_REQUEST_TAG_CREATE: u8 = 0; +const EVENT_REQUEST_TAG_WAIT: u8 = 1; +const EVENT_REQUEST_TAG_SIGNAL: u8 = 2; const RESPONSE_TAG_NEGOTIATED: u8 = 0; -const RESPONSE_TAG_HANDLE: u8 = 1; -const RESPONSE_TAG_READINESS: u8 = 2; -const RESPONSE_TAG_WAIT: u8 = 3; -const RESPONSE_TAG_ERROR: u8 = 4; -const RESPONSE_TAG_VERSION_MISMATCH: u8 = 5; +const RESPONSE_TAG_CORE: u8 = 1; +const RESPONSE_TAG_ERROR: u8 = 2; +const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; +const CORE_RESPONSE_TAG_EVENT: u8 = 0; +const EVENT_RESPONSE_TAG_CREATED: u8 = 0; +const EVENT_RESPONSE_TAG_SIGNALED: u8 = 1; +const EVENT_RESPONSE_TAG_WAIT: u8 = 2; const WAIT_OUTCOME_TAG_READY: u8 = 1; const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; @@ -88,41 +93,86 @@ pub fn encode_request(request: BrokerRequest) -> Result, WireError> { encoder.u16(protocol_version.major); encoder.u16(protocol_version.minor); } - BrokerRequest::CreateEvent => { - encoder.u8(REQUEST_TAG_CREATE_EVENT); + BrokerRequest::Core(request) => encode_core_request(&mut encoder, request)?, + _ => return Err(WireError::EncodeUnknownRequestTag), + } + Ok(encoder.finish()) +} + +fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) -> Result<(), WireError> { + encoder.u8(REQUEST_TAG_CORE); + match request { + CoreRequest::Event(request) => { + encoder.u8(CORE_REQUEST_TAG_EVENT); + encode_event_request(encoder, request) } - BrokerRequest::WaitEvent { handle } => { - encoder.u8(REQUEST_TAG_WAIT_EVENT); + _ => Err(WireError::EncodeUnknownRequestTag), + } +} + +fn encode_event_request(encoder: &mut Encoder, request: EventRequest) -> Result<(), WireError> { + match request { + EventRequest::Create => { + encoder.u8(EVENT_REQUEST_TAG_CREATE); + Ok(()) + } + EventRequest::Wait { handle } => { + encoder.u8(EVENT_REQUEST_TAG_WAIT); encoder.handle(handle); + Ok(()) } - BrokerRequest::SignalEvent { handle } => { - encoder.u8(REQUEST_TAG_SIGNAL_EVENT); + EventRequest::Signal { handle } => { + encoder.u8(EVENT_REQUEST_TAG_SIGNAL); encoder.handle(handle); + Ok(()) } - _ => return Err(WireError::EncodeUnknownRequestTag), + _ => Err(WireError::EncodeUnknownRequestTag), } - Ok(encoder.finish()) } /// Decodes a broker request body. -pub fn decode_request(frame: &[u8]) -> Result { +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: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), }, - REQUEST_TAG_CREATE_EVENT => BrokerRequest::CreateEvent, - REQUEST_TAG_WAIT_EVENT => BrokerRequest::WaitEvent { + REQUEST_TAG_CORE => match 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)) +} + +fn decode_core_request(decoder: &mut Decoder<'_>) -> Result, WireError> { + let request = match decoder.u8()? { + CORE_REQUEST_TAG_EVENT => match decode_event_request(decoder)? { + Some(request) => CoreRequest::Event(request), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(request)) +} + +fn decode_event_request(decoder: &mut Decoder<'_>) -> Result, WireError> { + let request = match decoder.u8()? { + EVENT_REQUEST_TAG_CREATE => EventRequest::Create, + EVENT_REQUEST_TAG_WAIT => EventRequest::Wait { handle: decoder.handle()?, }, - REQUEST_TAG_SIGNAL_EVENT => BrokerRequest::SignalEvent { + EVENT_REQUEST_TAG_SIGNAL => EventRequest::Signal { handle: decoder.handle()?, }, - _ => return Ok(ReceivedRequest::Unknown), + _ => return Ok(None), }; - decoder.finish()?; - Ok(ReceivedRequest::Request(request)) + + Ok(Some(request)) } /// Encodes a broker response body. @@ -146,28 +196,7 @@ pub fn encode_response(response: BrokerResponse) -> Result, WireError> { encoder.u16(broker_protocol_version.major); encoder.u16(broker_protocol_version.minor); } - BrokerResponse::Handle(handle) => { - encoder.u8(RESPONSE_TAG_HANDLE); - encoder.handle(handle); - } - BrokerResponse::Readiness(readiness) => { - encoder.u8(RESPONSE_TAG_READINESS); - encoder.readiness(readiness); - } - BrokerResponse::Wait(outcome) => { - encoder.u8(RESPONSE_TAG_WAIT); - match outcome { - WaitOutcome::Ready(readiness) => { - encoder.u8(WAIT_OUTCOME_TAG_READY); - encoder.readiness(readiness); - } - WaitOutcome::WouldBlock(readiness) => { - encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); - encoder.readiness(readiness); - } - _ => return Err(WireError::EncodeUnknownWaitOutcome), - } - } + BrokerResponse::Core(response) => encode_core_response(&mut encoder, response)?, BrokerResponse::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.u16(error.as_raw()); @@ -177,8 +206,55 @@ pub fn encode_response(response: BrokerResponse) -> Result, WireError> { Ok(encoder.finish()) } +fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) -> Result<(), WireError> { + encoder.u8(RESPONSE_TAG_CORE); + match response { + CoreResponse::Event(response) => { + encoder.u8(CORE_RESPONSE_TAG_EVENT); + encode_event_response(encoder, response) + } + _ => Err(WireError::EncodeUnknownResponseTag), + } +} + +fn encode_event_response(encoder: &mut Encoder, response: EventResponse) -> Result<(), WireError> { + match response { + EventResponse::Created { handle } => { + encoder.u8(EVENT_RESPONSE_TAG_CREATED); + encoder.handle(handle); + Ok(()) + } + EventResponse::Signaled { readiness } => { + encoder.u8(EVENT_RESPONSE_TAG_SIGNALED); + encoder.readiness(readiness); + Ok(()) + } + EventResponse::Wait { outcome } => { + encoder.u8(EVENT_RESPONSE_TAG_WAIT); + encode_wait_outcome(encoder, outcome) + } + _ => Err(WireError::EncodeUnknownResponseTag), + } +} + +fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) -> Result<(), WireError> { + match outcome { + WaitOutcome::Ready(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_READY); + encoder.readiness(readiness); + Ok(()) + } + WaitOutcome::WouldBlock(readiness) => { + encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); + encoder.readiness(readiness); + Ok(()) + } + _ => Err(WireError::EncodeUnknownWaitOutcome), + } +} + /// Decodes a broker response body. -pub fn decode_response(frame: &[u8]) -> Result { +pub fn decode_response(frame: &[u8]) -> Result { let mut decoder = Decoder::new(frame); let tag = decoder.u8()?; let response = match tag { @@ -188,24 +264,55 @@ pub fn decode_response(frame: &[u8]) -> Result { RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { broker_protocol_version: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), }, - RESPONSE_TAG_HANDLE => BrokerResponse::Handle(decoder.handle()?), - RESPONSE_TAG_READINESS => BrokerResponse::Readiness(decoder.readiness()?), - RESPONSE_TAG_WAIT => { - let outcome = match decoder.u8()? { - WAIT_OUTCOME_TAG_READY => WaitOutcome::Ready(decoder.readiness()?), - WAIT_OUTCOME_TAG_WOULD_BLOCK => WaitOutcome::WouldBlock(decoder.readiness()?), - _ => return Err(WireError::UnknownWaitOutcome), - }; - BrokerResponse::Wait(outcome) - } + RESPONSE_TAG_CORE => match 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(ReceivedResponse::Unknown), + _ => return Ok(ReceivedBrokerResponse::Unknown), }; decoder.finish()?; - Ok(ReceivedResponse::Response(response)) + Ok(ReceivedBrokerResponse::Response(response)) +} + +fn decode_core_response(decoder: &mut Decoder<'_>) -> Result, WireError> { + let response = match decoder.u8()? { + CORE_RESPONSE_TAG_EVENT => match decode_event_response(decoder)? { + Some(response) => CoreResponse::Event(response), + None => return Ok(None), + }, + _ => return Ok(None), + }; + + Ok(Some(response)) +} + +fn decode_event_response(decoder: &mut Decoder<'_>) -> Result, WireError> { + let response = match decoder.u8()? { + EVENT_RESPONSE_TAG_CREATED => EventResponse::Created { + handle: decoder.handle()?, + }, + EVENT_RESPONSE_TAG_SIGNALED => EventResponse::Signaled { + readiness: decoder.readiness()?, + }, + EVENT_RESPONSE_TAG_WAIT => EventResponse::Wait { + outcome: decode_wait_outcome(decoder)?, + }, + _ => return Ok(None), + }; + + Ok(Some(response)) +} + +fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result { + match decoder.u8()? { + WAIT_OUTCOME_TAG_READY => Ok(WaitOutcome::Ready(decoder.readiness()?)), + WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(WaitOutcome::WouldBlock(decoder.readiness()?)), + _ => Err(WireError::UnknownWaitOutcome), + } } #[derive(Default)] @@ -324,15 +431,15 @@ mod tests { BrokerRequest::Negotiate { protocol_version: ProtocolVersion::new(1, 0), }, - BrokerRequest::CreateEvent, - BrokerRequest::WaitEvent { handle }, - BrokerRequest::SignalEvent { handle }, + event_request(EventRequest::Create), + event_request(EventRequest::Wait { handle }), + event_request(EventRequest::Signal { handle }), ]; for request in requests { assert_eq!( decode_request(&encode_request(request).unwrap()).unwrap(), - ReceivedRequest::Request(request) + ReceivedBrokerRequest::Request(request) ); } } @@ -347,10 +454,16 @@ mod tests { BrokerResponse::VersionMismatch { broker_protocol_version: ProtocolVersion::new(1, 0), }, - BrokerResponse::Handle(handle), - BrokerResponse::Readiness(ReadinessState::new(false, 7)), - BrokerResponse::Wait(WaitOutcome::Ready(ReadinessState::new(true, 8))), - BrokerResponse::Wait(WaitOutcome::WouldBlock(ReadinessState::new(false, 9))), + event_response(EventResponse::Created { handle }), + event_response(EventResponse::Signaled { + readiness: ReadinessState::new(false, 7), + }), + event_response(EventResponse::Wait { + outcome: WaitOutcome::Ready(ReadinessState::new(true, 8)), + }), + event_response(EventResponse::Wait { + outcome: WaitOutcome::WouldBlock(ReadinessState::new(false, 9)), + }), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::Internal), ]; @@ -358,7 +471,7 @@ mod tests { for response in responses { assert_eq!( decode_response(&encode_response(response).unwrap()).unwrap(), - ReceivedResponse::Response(response) + ReceivedBrokerResponse::Response(response) ); } } @@ -367,10 +480,10 @@ mod tests { fn decode_rejects_malformed_request_frames() { assert_eq!( decode_request(&[0xff, 1, 2, 3]), - Ok(ReceivedRequest::Unknown) + Ok(ReceivedBrokerRequest::Unknown) ); assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); - let mut frame = encode_request(BrokerRequest::CreateEvent).unwrap(); + let mut frame = encode_request(event_request(EventRequest::Create)).unwrap(); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } @@ -379,27 +492,27 @@ mod tests { fn decode_rejects_malformed_response_frames() { assert_eq!( decode_response(&[0xff, 1, 2, 3]), - Ok(ReceivedResponse::Unknown) + Ok(ReceivedBrokerResponse::Unknown) ); assert_eq!( - decode_response(&[3, 0xff]), + decode_response(&[1, 0, 2, 0xff]), Err(WireError::UnknownWaitOutcome) ); assert_eq!( - decode_response(&[4, 0xff, 0xff]), - Ok(ReceivedResponse::Response(BrokerResponse::Error( + decode_response(&[2, 0xff, 0xff]), + Ok(ReceivedBrokerResponse::Response(BrokerResponse::Error( ErrorCode::Unknown(0xffff) ))) ); - let mut invalid_bool = [2, 2, 0, 0, 0, 0, 0, 0, 0, 0]; + let mut invalid_bool = [1, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0]; assert_eq!( decode_response(&invalid_bool), Err(WireError::InvalidBoolean) ); - invalid_bool[1] = 1; - invalid_bool[9] = 1; + invalid_bool[3] = 1; + invalid_bool[11] = 1; let mut frame = invalid_bool.to_vec(); frame.push(0xff); assert_eq!(decode_response(&frame), Err(WireError::TrailingBytes)); @@ -408,12 +521,11 @@ mod tests { #[test] fn readiness_response_wire_shape_is_pinned() { assert_eq!( - encode_response(BrokerResponse::Readiness(ReadinessState::new( - true, - 0x0102_0304_0506_0708 - ))) + encode_response(event_response(EventResponse::Signaled { + readiness: ReadinessState::new(true, 0x0102_0304_0506_0708) + })) .unwrap(), - [2, 1, 8, 7, 6, 5, 4, 3, 2, 1] + [1, 0, 1, 1, 8, 7, 6, 5, 4, 3, 2, 1] ); } @@ -423,4 +535,12 @@ mod tests { 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)) + } } From 5965b0fd4dd3589ce62b12efd80d9df2d314b8f5 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 14:23:50 -0700 Subject: [PATCH 14/66] Prune broker unit tests Keep server, core, and Unix socket tests focused on distinct interface and security behavior while removing redundant private implementation coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/connection.rs | 14 -- litebox_broker_core/src/identity.rs | 20 +-- litebox_broker_core/src/object.rs | 30 ---- litebox_broker_server/src/server.rs | 191 +++++--------------------- litebox_broker_unix_socket/src/lib.rs | 39 +++--- 5 files changed, 49 insertions(+), 245 deletions(-) diff --git a/litebox_broker_core/src/connection.rs b/litebox_broker_core/src/connection.rs index 071f151db..eaf961d2f 100644 --- a/litebox_broker_core/src/connection.rs +++ b/litebox_broker_core/src/connection.rs @@ -48,20 +48,6 @@ mod tests { use super::*; use crate::{BrokerCore, EventOnlyPolicy}; - #[test] - fn create_connection_records_caller_credential() { - let mut core = BrokerCore::new(EventOnlyPolicy); - - let connection = core - .create_connection(CallerCredential::Unauthenticated) - .unwrap(); - - assert_eq!( - connection.caller_credential(), - CallerCredential::Unauthenticated - ); - } - #[test] fn close_connection_releases_owned_references_and_orphaned_objects() { let mut core = BrokerCore::new(EventOnlyPolicy); diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index f76b56fd6..e6cbd4a3b 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -99,7 +99,7 @@ impl

BrokerCore

{ #[cfg(test)] mod tests { use super::*; - use crate::{BrokerError, DefaultDenyPolicy}; + use crate::DefaultDenyPolicy; #[test] fn create_association_uses_one_session_and_distinct_processes() { @@ -122,22 +122,4 @@ mod tests { CallerCredential::Unauthenticated ); } - - #[test] - fn create_association_issues_max_process_id_then_exhausts() { - let mut core = BrokerCore::new(DefaultDenyPolicy); - core.next_process_id = u64::MAX; - - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - assert_eq!(association.process_id, ProcessId::new(u64::MAX)); - assert_eq!(association.session_id, SessionId::FIRST); - assert_eq!(core.next_process_id, 0); - assert_eq!( - core.create_association(CallerCredential::Unauthenticated), - Err(BrokerError::ResourceExhausted) - ); - assert_eq!(core.next_process_id, 0); - } } diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 383a3c213..ce488cb9f 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -292,34 +292,4 @@ mod tests { Err(BrokerError::ResourceExhausted) ); } - - #[test] - fn validate_handle_rejects_stale_reference_generation() { - let mut core = BrokerCore::new(DefaultDenyPolicy); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core - .insert_object_with_reference( - association, - ObjectKind::Event(EventObject::new()), - ObjectType::Event, - ObjectRights::WAIT, - ) - .unwrap(); - let stale_handle = ObjectHandle::new( - handle.reference_id, - ObjectReferenceGeneration::new(handle.reference_generation.get() + 1), - ); - - assert_eq!( - core.validate_handle( - association, - stale_handle, - ObjectType::Event, - ObjectRights::WAIT - ), - Err(BrokerError::StaleHandle) - ); - } } diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 823654710..6ec758731 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -351,7 +351,7 @@ mod tests { use litebox_broker_core::EventOnlyPolicy; #[test] - fn dispatch_requires_negotiation_first() { + fn dispatch_enforces_negotiation_state() { let (mut core, connection, mut state) = new_connection(); let dispatch = handle_request( @@ -360,20 +360,9 @@ mod tests { &mut state, event_request(EventRequest::Create), ); - - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::ProtocolViolation) - ); + assert_protocol_violation(dispatch); assert_eq!(state, ConnectionState::AwaitingNegotiation); - } - #[test] - fn dispatch_closes_after_post_negotiation_negotiate() { let (mut core, connection, mut state) = new_connection(); negotiate(&mut core, &connection, &mut state); @@ -385,19 +374,11 @@ mod tests { protocol_version: SUPPORTED_PROTOCOL_VERSION, }, ); - - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::ProtocolViolation) - ); + assert_protocol_violation(dispatch); } #[test] - fn dispatch_reports_supported_version_after_unsupported_major() { + fn dispatch_rejects_unsupported_protocol_version_without_activation() { let (mut core, connection, mut state) = new_connection(); let dispatch = handle_request( @@ -420,65 +401,17 @@ mod tests { } #[test] - fn dispatch_accepts_supported_older_minor_version() { + fn dispatch_handles_unknown_wire_requests_by_state() { let (mut core, connection, mut state) = new_connection(); - let requested = ProtocolVersion::new( - SUPPORTED_PROTOCOL_VERSION.major, - SUPPORTED_PROTOCOL_VERSION.minor - 1, - ); - - let dispatch = handle_request( - &mut core, - &connection, - &mut state, - BrokerRequest::Negotiate { - protocol_version: requested, - }, - ); - assert_eq!( - dispatch.response, - BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!( - state, - ConnectionState::Active { - negotiated_protocol_version: requested - } - ); - } - - #[test] - fn dispatch_rejects_newer_minor_version() { - let (mut core, connection, mut state) = new_connection(); - - let dispatch = handle_request( + let dispatch = handle_received_request( &mut core, &connection, &mut state, - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new( - SUPPORTED_PROTOCOL_VERSION.major, - SUPPORTED_PROTOCOL_VERSION.minor + 1, - ), - }, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::VersionMismatch { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } + ReceivedBrokerRequest::Unknown, ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!(state, ConnectionState::AwaitingNegotiation); - } + assert_protocol_violation(dispatch); - #[test] - fn dispatch_reports_unknown_requests_without_closing() { let (mut core, connection, mut state) = new_connection(); negotiate(&mut core, &connection, &mut state); @@ -496,52 +429,10 @@ mod tests { assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } - #[test] - fn dispatch_closes_unknown_requests_before_negotiation() { - let (mut core, connection, mut state) = new_connection(); - - let dispatch = handle_received_request( - &mut core, - &connection, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::ProtocolViolation) - ); - } - #[test] fn dispatch_negotiates_then_routes_event_requests() { let (mut core, connection, mut state) = new_connection(); - - let dispatch = handle_request( - &mut core, - &connection, - &mut state, - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ); - assert_eq!( - dispatch.response, - BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION - } - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!( - state, - ConnectionState::Active { - negotiated_protocol_version: SUPPORTED_PROTOCOL_VERSION - } - ); + negotiate(&mut core, &connection, &mut state); let dispatch = handle_request( &mut core, @@ -549,6 +440,7 @@ mod tests { &mut state, event_request(EventRequest::Create), ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); let handle = match dispatch.response { BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, response => panic!("unexpected response: {response:?}"), @@ -567,65 +459,44 @@ mod tests { }) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - } - - #[test] - fn dispatch_rejects_handle_from_another_connection() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let owner = core - .create_connection(CallerCredential::Unauthenticated) - .unwrap(); - let other = core - .create_connection(CallerCredential::Unauthenticated) - .unwrap(); - let mut owner_state = ConnectionState::AwaitingNegotiation; - let mut other_state = ConnectionState::AwaitingNegotiation; - negotiate(&mut core, &owner, &mut owner_state); - negotiate(&mut core, &other, &mut other_state); let dispatch = handle_request( &mut core, - &owner, - &mut owner_state, - event_request(EventRequest::Create), + &connection, + &mut state, + event_request(EventRequest::Signal { handle }), ); - let handle = match dispatch.response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, - response => panic!("unexpected response: {response:?}"), - }; + assert_eq!( + dispatch.response, + event_response(EventResponse::Signaled { + readiness: ProtocolReadinessState::new(true, 1) + }) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); let dispatch = handle_request( &mut core, - &other, - &mut other_state, + &connection, + &mut state, event_request(EventRequest::Wait { handle }), ); assert_eq!( dispatch.response, - BrokerResponse::Error(ErrorCode::UnknownObject) + event_response(EventResponse::Wait { + outcome: ProtocolWaitOutcome::Ready(ProtocolReadinessState::new(true, 1)) + }) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } - #[test] - fn protocol_adapters_preserve_handle_fields() { - let protocol = ProtocolObjectHandle::new( - ProtocolObjectReferenceId::new(12), - ProtocolObjectReferenceGeneration::new(13), + fn assert_protocol_violation(dispatch: BrokerDispatch) { + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::ProtocolState) ); - - assert_eq!(protocol_handle(core_handle(protocol)), protocol); - } - - #[test] - fn protocol_adapters_preserve_wait_outcome() { - let outcome = CoreWaitOutcome::Ready(CoreReadinessState::new(true, 42)); - assert_eq!( - protocol_wait_outcome(outcome), - Some(ProtocolWaitOutcome::Ready(ProtocolReadinessState::new( - true, 42 - ))) + dispatch.outcome, + DispatchOutcome::Close(CloseReason::ProtocolViolation) ); } diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 6971dd041..6a6c62967 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -159,43 +159,38 @@ mod tests { } #[test] - fn truncated_length_prefix_is_invalid() { + 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(&mut reader).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); - let error = read_frame(&mut reader).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::InvalidData); - } - - #[test] - fn zero_frame_length_is_invalid() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); writer.write_all(&0u32.to_le_bytes()).unwrap(); + assert_eq!( + read_frame(&mut reader).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); - let error = read_frame(&mut reader).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::InvalidData); - } - - #[test] - fn oversize_frame_length_is_invalid() { 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(&mut reader).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); - let error = read_frame(&mut reader).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::InvalidData); - } - - #[test] - fn truncated_frame_body_is_invalid() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); writer.write_all(&4u32.to_le_bytes()).unwrap(); writer.write_all(&[1, 2]).unwrap(); drop(writer); - - let error = read_frame(&mut reader).unwrap_err(); - assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert_eq!( + read_frame(&mut reader).unwrap_err().kind(), + io::ErrorKind::InvalidData + ); } } From 717ac4b7dbfe539517c500132cb2ba2648d5495e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 15:21:44 -0700 Subject: [PATCH 15/66] Clean up split broker modularity Separate the Unix socket channel adapter from the hosted userland broker deployment, replace the redundant broker connection wrapper with associations, and align broker interface naming. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 9 +- Cargo.lock | 10 +- Cargo.toml | 2 + docs/broker-design.md | 20 +-- docs/impl-plan.md | 3 +- litebox_broker_channel/src/lib.rs | 3 +- litebox_broker_client/src/error.rs | 2 +- litebox_broker_client/src/event.rs | 2 +- litebox_broker_core/src/connection.rs | 67 --------- litebox_broker_core/src/event.rs | 37 +++-- litebox_broker_core/src/identity.rs | 56 +++++--- litebox_broker_core/src/lib.rs | 4 +- litebox_broker_core/src/object.rs | 47 +++++-- litebox_broker_core/src/policy.rs | 2 + litebox_broker_core/src/types.rs | 2 +- litebox_broker_protocol/src/error.rs | 9 ++ litebox_broker_protocol/src/message.rs | 8 +- litebox_broker_server/src/server.rs | 128 +++++++++--------- litebox_broker_unix_socket/Cargo.toml | 9 -- litebox_broker_userland/Cargo.toml | 20 +++ .../src/main.rs | 2 +- .../tests/userland_broker.rs | 0 litebox_broker_wire/src/lib.rs | 12 +- 23 files changed, 230 insertions(+), 224 deletions(-) delete mode 100644 litebox_broker_core/src/connection.rs create mode 100644 litebox_broker_userland/Cargo.toml rename litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs => litebox_broker_userland/src/main.rs (95%) rename {litebox_broker_unix_socket => litebox_broker_userland}/tests/userland_broker.rs (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5458ce0fe..231845215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,8 +231,12 @@ jobs: # since it is a purely-userland implementation. # # - `litebox_broker_unix_socket` is allowed to have `std` access, - # since it is the concrete Unix-domain-socket transport and hosted - # broker executable for the userland split-broker proof of concept. + # since it is the concrete Unix-domain-socket channel for the + # userland split-broker proof of concept. + # + # - `litebox_broker_userland` is allowed to have `std` access, + # since it is the hosted broker executable for the userland + # split-broker proof of concept. # # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. @@ -290,6 +294,7 @@ jobs: find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ -not -path './litebox_broker_unix_socket/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 7026261c3..c4bda5db3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1511,11 +1511,19 @@ name = "litebox_broker_unix_socket" version = "0.1.0" dependencies = [ "litebox_broker_channel", + "litebox_broker_protocol", + "litebox_broker_wire", +] + +[[package]] +name = "litebox_broker_userland" +version = "0.1.0" +dependencies = [ "litebox_broker_client", "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_wire", + "litebox_broker_unix_socket", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index ebbd3fa10..f73b735b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "litebox_broker_server", "litebox_broker_channel", "litebox_broker_unix_socket", + "litebox_broker_userland", "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", @@ -42,6 +43,7 @@ default-members = [ "litebox_broker_server", "litebox_broker_channel", "litebox_broker_unix_socket", + "litebox_broker_userland", "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", diff --git a/docs/broker-design.md b/docs/broker-design.md index 0c2f1b048..e295ef880 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -134,11 +134,12 @@ The broker split should use crate names that make the authority boundary visible | Crate | Initial role | |---|---| | `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | -| `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, connection cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_channel` | Neutral `no_std` channel-contract crate for blocking broker authority control-channel contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the channel-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking transports can provide blocking adapters at this boundary. | +| `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | +| `litebox_broker_channel` | Neutral `no_std` channel-contract crate for blocking broker authority control-channel contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the channel-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking IPCs can provide blocking adapters at this boundary. | | `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream channel implementations reuse it rather than duplicating protocol encoding. | -| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket channel implementation for hosted userland testing, plus the hosted Unix-socket broker executable used by the POC. | | `litebox_broker_server` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | +| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | +| `litebox_broker_userland` | Hosted `std` broker executable used by the userland POC. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket channel implementation together. | | `litebox_broker_client` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | | `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | @@ -148,7 +149,7 @@ Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. -Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection without exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. +Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection or exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while client and server connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so clients can retry with a compatible version on expensive or credentialed channels instead of reconnecting and guessing. @@ -319,7 +320,7 @@ The initial Linux ring set can use five unidirectional rings: | data | broker to runner | bulk response/event payload bytes | | data | runner to broker | bulk request payload bytes | -Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a UserLiteBox scheduler, but keeping the MPSC layout preserves one transport format. +Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a UserLiteBox scheduler, but keeping the MPSC layout preserves one ring format. Shared-memory rings are not trusted. The broker validates header magic/version, ring offsets/capacities, producer/consumer roles, cursor movement, frame bounds, and frame contents before acting. Impossible cursor movement, malformed frames, or writes inconsistent with ring ownership are protocol failures. @@ -705,7 +706,7 @@ Future mapping: | Current responsibility | Target component | |---|---| | page-table and address-space management | BrokerPlatform | -| VTL/trusted-domain trap and transport mechanism | BrokerHost | +| VTL/trusted-domain trap and channel mechanism | BrokerHost | | broker request decode and dispatch | broker server/entry layer | | domain validation and authorization | BrokerCore + PolicyEngine | | normal-world memory mapping and validation | BrokerPlatform | @@ -723,7 +724,7 @@ Future mapping: | Current responsibility | Target component | |---|---| | early boot and trusted-domain initialization | broker bootstrap | -| VTL trap/transport dispatch | BrokerHost | +| VTL trap/channel dispatch | BrokerHost | | broker request decode and dispatch | broker server/entry layer | | domain validation and authorization | BrokerCore + PolicyEngine | | session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | @@ -790,6 +791,7 @@ litebox_broker_channel litebox_broker_wire litebox_broker_unix_socket litebox_broker_server +litebox_broker_userland litebox_broker_client separate userland broker process Unix-domain-socket channel implementing neutral control-channel traits @@ -804,8 +806,8 @@ Then proceed incrementally: 1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, connection cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation plus hosted executable, and `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls. +3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. +4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. 5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index ac1f8c06e..8caf52081 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -89,7 +89,7 @@ Initial scope: - Unix-domain-socket framing as the first concrete userland channel implementation; - a Unix-socket executable that wires the generic channel-neutral server to the concrete Unix control-channel implementation; - server-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; -- BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and connection cleanup; +- BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and association cleanup; - default-deny PolicyEngine; - fail-closed channel/session behavior. @@ -98,6 +98,7 @@ Exit criteria: - UserLiteBox can connect and negotiate. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. +- The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. - BrokerCore has no dependency on `litebox_broker_protocol`, `litebox_broker_channel`, wire codecs, or concrete IPC crates. - Client code does not need to depend on the userland broker server crate to use the first Unix socket channel. - The generic broker server library does not depend on concrete Unix socket channel code and remains `no_std`. diff --git a/litebox_broker_channel/src/lib.rs b/litebox_broker_channel/src/lib.rs index 9c182d766..9d267cdab 100644 --- a/litebox_broker_channel/src/lib.rs +++ b/litebox_broker_channel/src/lib.rs @@ -5,8 +5,7 @@ //! //! This crate defines delivery contracts for broker authority messages. The //! current surface is intentionally limited to the paired control channel: -//! one [`BrokerRequest`](litebox_broker_protocol::BrokerRequest) produces one -//! [`BrokerResponse`](litebox_broker_protocol::BrokerResponse). Concrete IPC +//! one [`BrokerRequest`] produces one [`BrokerResponse`]. Concrete IPC //! implementations own framing, buffering, authentication, and the mechanism; //! non-blocking IPCs can provide a blocking adapter at this boundary. //! diff --git a/litebox_broker_client/src/error.rs b/litebox_broker_client/src/error.rs index d112457e0..16c8408e2 100644 --- a/litebox_broker_client/src/error.rs +++ b/litebox_broker_client/src/error.rs @@ -33,7 +33,7 @@ pub enum ClientError { /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, }, - /// BrokerCore rejected the request. + /// The broker rejected the request. Broker(ErrorCode), /// The broker returned a response type that does not match the request. UnexpectedResponse(BrokerResponse), diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_client/src/event.rs index 2082aa34f..853a7b05b 100644 --- a/litebox_broker_client/src/event.rs +++ b/litebox_broker_client/src/event.rs @@ -26,7 +26,7 @@ impl BrokerClient { pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { self.ensure_negotiated()?; match self.request(event_request(EventRequest::Wait { handle }))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait { outcome })) => { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Waited { outcome })) => { Ok(outcome) } response => Err(ClientError::UnexpectedResponse(response)), diff --git a/litebox_broker_core/src/connection.rs b/litebox_broker_core/src/connection.rs deleted file mode 100644 index eaf961d2f..000000000 --- a/litebox_broker_core/src/connection.rs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use crate::identity::Association; -use crate::{BrokerCore, CallerCredential, Result}; - -/// Broker-side authority state for one caller connection. -/// -/// This type intentionally carries only BrokerCore association identity. Protocol -/// negotiation, request sequencing, and transport state live in the server layer. -pub struct BrokerConnection { - association: Association, -} - -impl BrokerConnection { - pub(crate) const fn new(association: Association) -> Self { - Self { association } - } - - pub(crate) const fn association(&self) -> Association { - self.association - } - - /// Returns the broker-entry-authenticated caller credential for this connection. - pub const fn caller_credential(&self) -> CallerCredential { - self.association.caller_credential() - } -} - -impl

BrokerCore

{ - /// Allocates broker authority state for one caller connection. - pub fn create_connection( - &mut self, - caller_credential: CallerCredential, - ) -> Result { - self.create_association(caller_credential) - .map(BrokerConnection::new) - } - - /// Closes a broker connection and releases state owned by its association. - pub fn close_connection(&mut self, connection: BrokerConnection) { - self.close_association(connection.association); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{BrokerCore, EventOnlyPolicy}; - - #[test] - fn close_connection_releases_owned_references_and_orphaned_objects() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let connection = core - .create_connection(CallerCredential::Unauthenticated) - .unwrap(); - - let _handle = core.create_event(&connection).unwrap(); - assert_eq!(core.references.len(), 1); - assert_eq!(core.objects.len(), 1); - - core.close_connection(connection); - - assert!(core.references.is_empty()); - assert!(core.objects.is_empty()); - } -} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 16aac07c4..6fcf11533 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -3,7 +3,7 @@ use crate::object::{ObjectId, ObjectKind}; use crate::{ - BrokerConnection, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, + BrokerAssociation, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, PolicyEngine, Result, }; @@ -35,8 +35,7 @@ pub enum WaitOutcome { impl BrokerCore

{ /// Creates a broker-owned event object. - pub fn create_event(&mut self, connection: &BrokerConnection) -> Result { - let association = connection.association(); + pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { self.authorize_create_object(association, ObjectType::Event)?; self.insert_object_with_reference( @@ -50,16 +49,15 @@ impl BrokerCore

{ /// 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 transport-specific + /// 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, - connection: &BrokerConnection, + association: &BrokerAssociation, handle: ObjectHandle, ) -> Result { - let association = connection.association(); let object_id = - self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; let state = self.event_state(object_id)?; Ok(if state.ready { WaitOutcome::Ready(state) @@ -71,12 +69,11 @@ impl BrokerCore

{ /// Signals a broker-owned event object. pub fn signal_event( &mut self, - connection: &BrokerConnection, + association: &BrokerAssociation, handle: ObjectHandle, ) -> Result { - let association = connection.association(); let object_id = - self.authorize_object_use(association, handle, ObjectType::Event, ObjectRights::WRITE)?; + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; match &mut self.object_mut(object_id)?.kind { ObjectKind::Event(event) => event.signal(), } @@ -125,12 +122,12 @@ mod tests { #[test] fn wait_rejects_reference_without_wait_right() { let mut core = BrokerCore::new(EventOnlyPolicy); - let connection = core - .create_connection(CallerCredential::Unauthenticated) + let association = core + .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = core .insert_object_with_reference( - connection.association(), + &association, ObjectKind::Event(EventObject::new()), ObjectType::Event, ObjectRights::WRITE, @@ -138,7 +135,7 @@ mod tests { .unwrap(); assert_eq!( - core.wait_event(&connection, handle), + core.wait_event(&association, handle), Err(BrokerError::InvalidRights) ); } @@ -146,15 +143,15 @@ mod tests { #[test] fn wait_rejects_stale_reference_generation() { let mut core = BrokerCore::new(EventOnlyPolicy); - let connection = core - .create_connection(CallerCredential::Unauthenticated) + let association = core + .create_association(CallerCredential::Unauthenticated) .unwrap(); - let mut handle = core.create_event(&connection).unwrap(); + let mut handle = core.create_event(&association).unwrap(); handle.reference_generation = ObjectReferenceGeneration::new(handle.reference_generation.get() + 1); assert_eq!( - core.wait_event(&connection, handle), + core.wait_event(&association, handle), Err(BrokerError::StaleHandle) ); } @@ -163,10 +160,10 @@ mod tests { fn wait_hides_handle_owned_by_another_association() { let mut core = BrokerCore::new(EventOnlyPolicy); let owner = core - .create_connection(CallerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); let other = core - .create_connection(CallerCredential::Unauthenticated) + .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = core.create_event(&owner).unwrap(); diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index e6cbd4a3b..70f27593b 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -7,7 +7,7 @@ use crate::{BrokerCore, Result, allocate_id}; /// /// 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 connection-creation seam. +/// 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 { @@ -44,22 +44,36 @@ id_type! { ProcessId } -/// Broker-assigned identity bound to one authenticated caller association. +/// Broker-assigned identity for one caller association. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct AssociationIdentity { + session_id: SessionId, + process_id: ProcessId, +} + +impl AssociationIdentity { + const fn new(session_id: SessionId, process_id: ProcessId) -> Self { + Self { + session_id, + process_id, + } + } +} + +/// 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(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct Association { +#[derive(Debug, PartialEq, Eq)] +pub struct BrokerAssociation { /// Broker-assigned sandbox session identity. - session_id: SessionId, - /// Broker-assigned guest process identity. - process_id: ProcessId, + identity: AssociationIdentity, /// Broker-entry-authenticated caller credential for this association. caller_credential: CallerCredential, } -impl Association { +impl BrokerAssociation { /// Creates an authenticated association identity. pub(crate) const fn new( session_id: SessionId, @@ -67,27 +81,31 @@ impl Association { caller_credential: CallerCredential, ) -> Self { Self { - session_id, - process_id, + identity: AssociationIdentity::new(session_id, process_id), caller_credential, } } - pub(crate) const fn caller_credential(self) -> CallerCredential { + pub(crate) const fn identity(&self) -> AssociationIdentity { + self.identity + } + + /// Returns the broker-entry-authenticated caller credential for this association. + pub const fn caller_credential(&self) -> CallerCredential { self.caller_credential } } impl

BrokerCore

{ - /// Allocates a broker association for one process connection. - pub(crate) fn create_association( + /// Allocates broker authority state for one authenticated caller association. + pub fn create_association( &mut self, caller_credential: CallerCredential, - ) -> Result { + ) -> Result { let process_id = allocate_id(&mut self.next_process_id)?; // The POC models one sandbox session per BrokerCore; multi-session // allocation belongs with the future deployment/session manager. - let association = Association::new( + let association = BrokerAssociation::new( SessionId::FIRST, ProcessId::new(process_id), caller_credential, @@ -112,10 +130,10 @@ mod tests { .create_association(CallerCredential::Unauthenticated) .unwrap(); - assert_eq!(first.session_id, SessionId::FIRST); - assert_eq!(second.session_id, SessionId::FIRST); - assert_eq!(first.process_id, ProcessId::new(1)); - assert_eq!(second.process_id, ProcessId::new(2)); + assert_eq!(first.identity.session_id, SessionId::FIRST); + assert_eq!(second.identity.session_id, SessionId::FIRST); + assert_eq!(first.identity.process_id, ProcessId::new(1)); + assert_eq!(second.identity.process_id, ProcessId::new(2)); assert_eq!(first.caller_credential(), CallerCredential::Unauthenticated); assert_eq!( second.caller_credential(), diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index a0caeeb58..c85010534 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -14,7 +14,6 @@ extern crate alloc; #[cfg(test)] extern crate std; -mod connection; mod error; mod event; mod identity; @@ -24,10 +23,9 @@ mod types; use alloc::collections::BTreeMap; -pub use connection::BrokerConnection; pub use error::BrokerError; pub use event::{ReadinessState, WaitOutcome}; -pub use identity::CallerCredential; +pub use identity::{BrokerAssociation, CallerCredential}; use object::{ObjectEntry, ObjectId, ObjectReference}; pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; pub use policy::{ diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index ce488cb9f..238c4193c 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -4,7 +4,7 @@ use alloc::vec::Vec; use crate::event::EventObject; -use crate::identity::Association; +use crate::identity::{AssociationIdentity, BrokerAssociation}; use crate::{ BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, PolicyOperation, Result, allocate_id, @@ -85,7 +85,7 @@ const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGen pub(crate) struct ObjectReference { pub(crate) object_id: ObjectId, pub(crate) reference_generation: ObjectReferenceGeneration, - pub(crate) owner: Association, + pub(crate) owner: AssociationIdentity, pub(crate) object_type: ObjectType, pub(crate) rights: ObjectRights, } @@ -117,7 +117,7 @@ impl BrokerCore

{ /// slot so stale handles cannot validate against a recycled reference. pub(crate) fn insert_object_with_reference( &mut self, - association: Association, + association: &BrokerAssociation, kind: ObjectKind, object_type: ObjectType, rights: ObjectRights, @@ -132,7 +132,7 @@ impl BrokerCore

{ ObjectReference { object_id, reference_generation, - owner: association, + owner: association.identity(), object_type, rights, }, @@ -143,7 +143,7 @@ impl BrokerCore

{ pub(crate) fn authorize_create_object( &mut self, - association: Association, + association: &BrokerAssociation, object_type: ObjectType, ) -> Result<()> { self.policy.authorize(PolicyOperation::create_object( @@ -152,9 +152,9 @@ impl BrokerCore

{ )) } - pub(crate) fn authorize_object_use( + pub(crate) fn authorize_use_object( &mut self, - association: Association, + association: &BrokerAssociation, handle: ObjectHandle, object_type: ObjectType, rights: ObjectRights, @@ -182,7 +182,7 @@ impl BrokerCore

{ fn validate_handle( &self, - association: Association, + association: &BrokerAssociation, handle: ObjectHandle, expected_type: ObjectType, required_rights: ObjectRights, @@ -191,7 +191,7 @@ impl BrokerCore

{ .references .get(&handle.reference_id) .ok_or(BrokerError::UnknownObject)?; - if reference.owner != association { + if reference.owner != association.identity() { return Err(BrokerError::UnknownObject); } if reference.reference_generation != handle.reference_generation { @@ -225,12 +225,14 @@ impl BrokerCore

{ } impl

BrokerCore

{ - pub(crate) fn close_association(&mut self, association: Association) { + /// Closes a broker association and releases references owned by it. + pub fn close_association(&mut self, association: BrokerAssociation) { + let identity = association.identity(); let reference_ids = self .references .iter() .filter_map(|(reference_id, reference)| { - (reference.owner == association).then_some(*reference_id) + (reference.owner == identity).then_some(*reference_id) }) .collect::>(); @@ -259,7 +261,7 @@ impl

BrokerCore

{ #[cfg(test)] mod tests { use super::*; - use crate::{BrokerError, CallerCredential, DefaultDenyPolicy}; + use crate::{BrokerError, CallerCredential, DefaultDenyPolicy, EventOnlyPolicy}; #[test] fn object_and_reference_allocators_issue_max_id_then_exhaust() { @@ -272,7 +274,7 @@ mod tests { let handle = core .insert_object_with_reference( - association, + &association, ObjectKind::Event(EventObject::new()), ObjectType::Event, ObjectRights::WAIT, @@ -284,7 +286,7 @@ mod tests { assert_eq!(core.next_reference_id, 0); assert_eq!( core.insert_object_with_reference( - association, + &association, ObjectKind::Event(EventObject::new()), ObjectType::Event, ObjectRights::WAIT, @@ -292,4 +294,21 @@ mod tests { Err(BrokerError::ResourceExhausted) ); } + + #[test] + fn close_association_releases_owned_references_and_orphaned_objects() { + let mut core = BrokerCore::new(EventOnlyPolicy); + 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 index 17c4836d2..824bb73a2 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -11,7 +11,9 @@ pub enum PolicyOperation { 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, }, } diff --git a/litebox_broker_core/src/types.rs b/litebox_broker_core/src/types.rs index 990123d36..09d92c395 100644 --- a/litebox_broker_core/src/types.rs +++ b/litebox_broker_core/src/types.rs @@ -19,7 +19,7 @@ pub struct ObjectRights(u32); impl ObjectRights { /// Right to wait for readiness. pub const WAIT: Self = Self(1 << 0); - /// Right to write payload data or signal an object. + /// Right to mutate object state, such as signaling an event. pub const WRITE: Self = Self(1 << 1); /// Returns true when all `required` rights are present. diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 9be9ee281..72cd1d6e5 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -30,10 +30,19 @@ pub enum ErrorCode { /// Broker-side resource exhaustion. ResourceExhausted, /// Error code emitted by a newer broker and not understood by this client. + /// + /// 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 { diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index d81295175..38c6abd27 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -29,7 +29,7 @@ pub enum WaitOutcome { WouldBlock(ReadinessState), } -/// Broker request transported over the control channel. +/// 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 @@ -72,7 +72,7 @@ pub enum EventRequest { }, } -/// Broker response transported over the control channel. +/// 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 @@ -120,6 +120,6 @@ pub enum EventResponse { Created { handle: ObjectHandle }, /// Operation returned readiness state. Signaled { readiness: ReadinessState }, - /// Operation returned wait state. - Wait { outcome: WaitOutcome }, + /// Wait operation returned wait state. + Waited { outcome: WaitOutcome }, } diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 6ec758731..d3d9ae184 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -5,7 +5,7 @@ use core::fmt; use litebox_broker_channel::{PeerCredential, ReceivedBrokerRequest, ServerControlChannel}; use litebox_broker_core::{ - BrokerConnection, BrokerCore, BrokerError, CallerCredential, ObjectHandle as CoreObjectHandle, + BrokerAssociation, BrokerCore, BrokerError, CallerCredential, ObjectHandle as CoreObjectHandle, ObjectReferenceGeneration as CoreObjectReferenceGeneration, ObjectReferenceId as CoreObjectReferenceId, PolicyEngine, ReadinessState as CoreReadinessState, WaitOutcome as CoreWaitOutcome, @@ -34,20 +34,20 @@ where .peer_credential() .map_err(BrokerServeError::Channel)?; let caller_credential = caller_credential_from_peer(peer_credential) - .map_err(|()| BrokerServeError::ConnectionSetup)?; - let connection = core - .create_connection(caller_credential) - .map_err(|_error| BrokerServeError::ConnectionSetup)?; + .map_err(|()| BrokerServeError::AssociationSetup)?; + let association = core + .create_association(caller_credential) + .map_err(|_error| BrokerServeError::AssociationSetup)?; - let result = serve_request_loop(core, channel, &connection); - core.close_connection(connection); + let result = serve_request_loop(core, channel, &association); + core.close_association(association); result } fn serve_request_loop( core: &mut BrokerCore

, channel: &mut T, - connection: &BrokerConnection, + association: &BrokerAssociation, ) -> Result> where P: PolicyEngine, @@ -59,7 +59,7 @@ where break; }; - let dispatch = handle_received_request(core, connection, &mut state, received); + let dispatch = handle_received_request(core, association, &mut state, received); channel .send_response(&dispatch.response) .map_err(BrokerServeError::Channel)?; @@ -81,19 +81,21 @@ fn caller_credential_from_peer(peer_credential: PeerCredential) -> Result( core: &mut BrokerCore

, - connection: &BrokerConnection, + association: &BrokerAssociation, state: &mut ConnectionState, received: ReceivedBrokerRequest, ) -> BrokerDispatch { match received { - ReceivedBrokerRequest::Request(request) => handle_request(core, connection, state, request), + ReceivedBrokerRequest::Request(request) => { + handle_request(core, association, state, request) + } _ => handle_unknown_request(*state), } } fn handle_request( core: &mut BrokerCore

, - connection: &BrokerConnection, + association: &BrokerAssociation, state: &mut ConnectionState, request: BrokerRequest, ) -> BrokerDispatch { @@ -109,13 +111,13 @@ fn handle_request( }, ConnectionState::Active { negotiated_protocol_version, - } => handle_active_request(core, connection, negotiated_protocol_version, request), + } => handle_active_request(core, association, negotiated_protocol_version, request), } } fn handle_active_request( core: &mut BrokerCore

, - connection: &BrokerConnection, + association: &BrokerAssociation, _negotiated_protocol_version: ProtocolVersion, request: BrokerRequest, ) -> BrokerDispatch { @@ -125,7 +127,7 @@ fn handle_active_request( CloseReason::ProtocolViolation, ), BrokerRequest::Core(CoreRequest::Event(request)) => { - BrokerDispatch::continue_after(handle_event_request(core, connection, request)) + BrokerDispatch::continue_after(handle_event_request(core, association, request)) } _ => BrokerDispatch::continue_after(BrokerResponse::Error(ErrorCode::UnsupportedOperation)), } @@ -133,23 +135,23 @@ fn handle_active_request( fn handle_event_request( core: &mut BrokerCore

, - connection: &BrokerConnection, + association: &BrokerAssociation, request: EventRequest, ) -> BrokerResponse { match request { - EventRequest::Create => handle_core_result(core.create_event(connection), |handle| { + EventRequest::Create => handle_core_result(core.create_event(association), |handle| { event_response(EventResponse::Created { - handle: protocol_handle(handle), + handle: to_protocol_handle(handle), }) }), EventRequest::Wait { handle } => { - handle_wait_result(core.wait_event(connection, core_handle(handle))) + handle_wait_result(core.wait_event(association, to_core_handle(handle))) } EventRequest::Signal { handle } => handle_core_result( - core.signal_event(connection, core_handle(handle)), + core.signal_event(association, to_core_handle(handle)), |readiness| { event_response(EventResponse::Signaled { - readiness: protocol_readiness_state(readiness), + readiness: to_protocol_readiness_state(readiness), }) }, ), @@ -192,17 +194,17 @@ fn handle_core_result( ) -> BrokerResponse { match result { Ok(value) => into_response(value), - Err(error) => BrokerResponse::Error(protocol_error(error)), + Err(error) => BrokerResponse::Error(to_protocol_error(error)), } } fn handle_wait_result(result: litebox_broker_core::Result) -> BrokerResponse { match result { - Ok(outcome) => match protocol_wait_outcome(outcome) { - Some(outcome) => event_response(EventResponse::Wait { outcome }), + Ok(outcome) => match to_protocol_wait_outcome(outcome) { + Some(outcome) => event_response(EventResponse::Waited { outcome }), None => BrokerResponse::Error(ErrorCode::Internal), }, - Err(error) => BrokerResponse::Error(protocol_error(error)), + Err(error) => BrokerResponse::Error(to_protocol_error(error)), } } @@ -210,7 +212,7 @@ const fn event_response(response: EventResponse) -> BrokerResponse { BrokerResponse::Core(CoreResponse::Event(response)) } -fn protocol_error(error: BrokerError) -> ErrorCode { +fn to_protocol_error(error: BrokerError) -> ErrorCode { match error { BrokerError::PolicyDenied => ErrorCode::PolicyDenied, BrokerError::UnknownObject => ErrorCode::UnknownObject, @@ -222,31 +224,31 @@ fn protocol_error(error: BrokerError) -> ErrorCode { } } -fn core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { +fn to_core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { CoreObjectHandle::new( CoreObjectReferenceId::new(handle.reference_id.get()), CoreObjectReferenceGeneration::new(handle.reference_generation.get()), ) } -fn protocol_handle(handle: CoreObjectHandle) -> ProtocolObjectHandle { +fn to_protocol_handle(handle: CoreObjectHandle) -> ProtocolObjectHandle { ProtocolObjectHandle::new( ProtocolObjectReferenceId::new(handle.reference_id.get()), ProtocolObjectReferenceGeneration::new(handle.reference_generation.get()), ) } -fn protocol_readiness_state(readiness: CoreReadinessState) -> ProtocolReadinessState { +fn to_protocol_readiness_state(readiness: CoreReadinessState) -> ProtocolReadinessState { ProtocolReadinessState::new(readiness.ready, readiness.generation) } -fn protocol_wait_outcome(outcome: CoreWaitOutcome) -> Option { +fn to_protocol_wait_outcome(outcome: CoreWaitOutcome) -> Option { match outcome { CoreWaitOutcome::Ready(readiness) => Some(ProtocolWaitOutcome::Ready( - protocol_readiness_state(readiness), + to_protocol_readiness_state(readiness), )), CoreWaitOutcome::WouldBlock(readiness) => Some(ProtocolWaitOutcome::WouldBlock( - protocol_readiness_state(readiness), + to_protocol_readiness_state(readiness), )), _ => None, } @@ -318,8 +320,8 @@ pub enum ConnectionTermination { #[derive(Debug)] #[non_exhaustive] pub enum BrokerServeError { - /// The server could not allocate or map state for a new connection. - ConnectionSetup, + /// The server could not authenticate the peer or allocate broker association state. + AssociationSetup, /// The concrete channel failed. Channel(E), } @@ -327,7 +329,7 @@ pub enum BrokerServeError { impl fmt::Display for BrokerServeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::ConnectionSetup => f.write_str("broker connection setup failed"), + Self::AssociationSetup => f.write_str("broker association setup failed"), Self::Channel(error) => write!(f, "broker channel failed: {error}"), } } @@ -339,7 +341,7 @@ where { fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { match self { - Self::ConnectionSetup => None, + Self::AssociationSetup => None, Self::Channel(error) => Some(error), } } @@ -352,23 +354,23 @@ mod tests { #[test] fn dispatch_enforces_negotiation_state() { - let (mut core, connection, mut state) = new_connection(); + let (mut core, association, mut state) = new_association(); let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, event_request(EventRequest::Create), ); assert_protocol_violation(dispatch); assert_eq!(state, ConnectionState::AwaitingNegotiation); - let (mut core, connection, mut state) = new_connection(); - negotiate(&mut core, &connection, &mut state); + let (mut core, association, mut state) = new_association(); + negotiate(&mut core, &association, &mut state); let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, BrokerRequest::Negotiate { protocol_version: SUPPORTED_PROTOCOL_VERSION, @@ -379,11 +381,11 @@ mod tests { #[test] fn dispatch_rejects_unsupported_protocol_version_without_activation() { - let (mut core, connection, mut state) = new_connection(); + let (mut core, association, mut state) = new_association(); let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, BrokerRequest::Negotiate { protocol_version: ProtocolVersion::new(SUPPORTED_PROTOCOL_VERSION.major + 1, 0), @@ -402,22 +404,22 @@ mod tests { #[test] fn dispatch_handles_unknown_wire_requests_by_state() { - let (mut core, connection, mut state) = new_connection(); + let (mut core, association, mut state) = new_association(); let dispatch = handle_received_request( &mut core, - &connection, + &association, &mut state, ReceivedBrokerRequest::Unknown, ); assert_protocol_violation(dispatch); - let (mut core, connection, mut state) = new_connection(); - negotiate(&mut core, &connection, &mut state); + let (mut core, association, mut state) = new_association(); + negotiate(&mut core, &association, &mut state); let dispatch = handle_received_request( &mut core, - &connection, + &association, &mut state, ReceivedBrokerRequest::Unknown, ); @@ -431,12 +433,12 @@ mod tests { #[test] fn dispatch_negotiates_then_routes_event_requests() { - let (mut core, connection, mut state) = new_connection(); - negotiate(&mut core, &connection, &mut state); + let (mut core, association, mut state) = new_association(); + negotiate(&mut core, &association, &mut state); let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, event_request(EventRequest::Create), ); @@ -448,13 +450,13 @@ mod tests { let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, event_request(EventRequest::Wait { handle }), ); assert_eq!( dispatch.response, - event_response(EventResponse::Wait { + event_response(EventResponse::Waited { outcome: ProtocolWaitOutcome::WouldBlock(ProtocolReadinessState::new(false, 0)) }) ); @@ -462,7 +464,7 @@ mod tests { let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, event_request(EventRequest::Signal { handle }), ); @@ -476,13 +478,13 @@ mod tests { let dispatch = handle_request( &mut core, - &connection, + &association, &mut state, event_request(EventRequest::Wait { handle }), ); assert_eq!( dispatch.response, - event_response(EventResponse::Wait { + event_response(EventResponse::Waited { outcome: ProtocolWaitOutcome::Ready(ProtocolReadinessState::new(true, 1)) }) ); @@ -500,26 +502,26 @@ mod tests { ); } - fn new_connection() -> ( + fn new_association() -> ( BrokerCore, - BrokerConnection, + BrokerAssociation, ConnectionState, ) { let mut core = BrokerCore::new(EventOnlyPolicy); - let connection = core - .create_connection(CallerCredential::Unauthenticated) + let association = core + .create_association(CallerCredential::Unauthenticated) .unwrap(); - (core, connection, ConnectionState::AwaitingNegotiation) + (core, association, ConnectionState::AwaitingNegotiation) } fn negotiate( core: &mut BrokerCore

, - connection: &BrokerConnection, + association: &BrokerAssociation, state: &mut ConnectionState, ) { let dispatch = handle_request( core, - connection, + association, state, BrokerRequest::Negotiate { protocol_version: SUPPORTED_PROTOCOL_VERSION, diff --git a/litebox_broker_unix_socket/Cargo.toml b/litebox_broker_unix_socket/Cargo.toml index 770f05918..bfe546565 100644 --- a/litebox_broker_unix_socket/Cargo.toml +++ b/litebox_broker_unix_socket/Cargo.toml @@ -4,18 +4,9 @@ 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" } -litebox_broker_server = { path = "../litebox_broker_server", version = "0.1.0" } litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } litebox_broker_wire = { path = "../litebox_broker_wire", version = "0.1.0" } -[[bin]] -name = "litebox-broker-userland" -path = "src/bin/litebox-broker-userland.rs" - -[dev-dependencies] -litebox_broker_client = { path = "../litebox_broker_client", version = "0.1.0" } - [lints] workspace = true diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml new file mode 100644 index 000000000..b16fe1d41 --- /dev/null +++ b/litebox_broker_userland/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "litebox_broker_userland" +version = "0.1.0" +edition = "2024" + +[dependencies] +litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } +litebox_broker_server = { path = "../litebox_broker_server", version = "0.1.0" } +litebox_broker_unix_socket = { path = "../litebox_broker_unix_socket", version = "0.1.0" } + +[[bin]] +name = "litebox-broker-userland" +path = "src/main.rs" + +[dev-dependencies] +litebox_broker_client = { path = "../litebox_broker_client", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } + +[lints] +workspace = true diff --git a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs b/litebox_broker_userland/src/main.rs similarity index 95% rename from litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs rename to litebox_broker_userland/src/main.rs index 1306dce15..c963f4db8 100644 --- a/litebox_broker_unix_socket/src/bin/litebox-broker-userland.rs +++ b/litebox_broker_userland/src/main.rs @@ -23,7 +23,7 @@ fn main() -> io::Result<()> { fn broker_error(error: BrokerServeError) -> io::Error { match error { - BrokerServeError::ConnectionSetup => io::Error::other("broker connection setup failed"), + BrokerServeError::AssociationSetup => io::Error::other("broker association setup failed"), BrokerServeError::Channel(error) => error, error => io::Error::other(error.to_string()), } diff --git a/litebox_broker_unix_socket/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs similarity index 100% rename from litebox_broker_unix_socket/tests/userland_broker.rs rename to litebox_broker_userland/tests/userland_broker.rs diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index c46fb9836..01646d80f 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -31,7 +31,7 @@ const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const CORE_RESPONSE_TAG_EVENT: u8 = 0; const EVENT_RESPONSE_TAG_CREATED: u8 = 0; const EVENT_RESPONSE_TAG_SIGNALED: u8 = 1; -const EVENT_RESPONSE_TAG_WAIT: u8 = 2; +const EVENT_RESPONSE_TAG_WAITED: u8 = 2; const WAIT_OUTCOME_TAG_READY: u8 = 1; const WAIT_OUTCOME_TAG_WOULD_BLOCK: u8 = 2; @@ -229,8 +229,8 @@ fn encode_event_response(encoder: &mut Encoder, response: EventResponse) -> Resu encoder.readiness(readiness); Ok(()) } - EventResponse::Wait { outcome } => { - encoder.u8(EVENT_RESPONSE_TAG_WAIT); + EventResponse::Waited { outcome } => { + encoder.u8(EVENT_RESPONSE_TAG_WAITED); encode_wait_outcome(encoder, outcome) } _ => Err(WireError::EncodeUnknownResponseTag), @@ -298,7 +298,7 @@ fn decode_event_response(decoder: &mut Decoder<'_>) -> Result EventResponse::Signaled { readiness: decoder.readiness()?, }, - EVENT_RESPONSE_TAG_WAIT => EventResponse::Wait { + EVENT_RESPONSE_TAG_WAITED => EventResponse::Waited { outcome: decode_wait_outcome(decoder)?, }, _ => return Ok(None), @@ -458,10 +458,10 @@ mod tests { event_response(EventResponse::Signaled { readiness: ReadinessState::new(false, 7), }), - event_response(EventResponse::Wait { + event_response(EventResponse::Waited { outcome: WaitOutcome::Ready(ReadinessState::new(true, 8)), }), - event_response(EventResponse::Wait { + event_response(EventResponse::Waited { outcome: WaitOutcome::WouldBlock(ReadinessState::new(false, 9)), }), BrokerResponse::Error(ErrorCode::PolicyDenied), From 8d1d5f05c92a5e010bb502664a31970e9b07b01c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 15:51:59 -0700 Subject: [PATCH 16/66] Document split broker dependency boundaries Record the current Unix peer credential placeholder and clarify that BrokerCore/protocol type duplication is intentional at the server mapping boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 2 ++ litebox_broker_unix_socket/src/lib.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/docs/broker-design.md b/docs/broker-design.md index e295ef880..811104221 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -147,6 +147,8 @@ The broker split should use crate names that make the authority boundary visible Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_channel`; the broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +The apparent duplication between protocol types and BrokerCore domain types is intentional. Protocol types are the wire-visible compatibility contract, while BrokerCore types model authority-domain invariants; `litebox_broker_server` is the sanctioned mapping boundary between them. Do not merge those crates merely to remove duplicate handle/readiness shapes unless a future design preserves the same compatibility and authority separation. + Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection or exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 6a6c62967..4aad1f42d 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -71,6 +71,8 @@ impl ServerControlChannel for UnixStreamServerControlChannel { type Error = io::Error; fn peer_credential(&self) -> io::Result { + // TODO(split-broker): replace the PoC placeholder with Unix peer credential extraction + // before this channel is used as an authenticated deployment boundary. Ok(PeerCredential::Unauthenticated) } From feafb8f04754f014e9d2f2dfa333bb40bb76326b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 16:13:51 -0700 Subject: [PATCH 17/66] Address split broker review follow-ups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 2 + litebox_broker_channel/src/lib.rs | 5 + litebox_broker_core/src/error.rs | 3 + litebox_broker_core/src/event.rs | 49 +++++++- litebox_broker_core/src/identity.rs | 6 +- litebox_broker_core/src/lib.rs | 3 +- litebox_broker_core/src/object.rs | 142 ++++++++++++++++++----- litebox_broker_core/src/policy.rs | 51 +++++++-- litebox_broker_core/src/types.rs | 15 ++- litebox_broker_server/src/lib.rs | 3 + litebox_broker_server/src/server.rs | 170 ++++++++++++++++++++++++++++ 11 files changed, 399 insertions(+), 50 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index 811104221..5065062f8 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -147,6 +147,8 @@ The broker split should use crate names that make the authority boundary visible Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_channel`; the broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. + The apparent duplication between protocol types and BrokerCore domain types is intentional. Protocol types are the wire-visible compatibility contract, while BrokerCore types model authority-domain invariants; `litebox_broker_server` is the sanctioned mapping boundary between them. Do not merge those crates merely to remove duplicate handle/readiness shapes unless a future design preserves the same compatibility and authority separation. Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. diff --git a/litebox_broker_channel/src/lib.rs b/litebox_broker_channel/src/lib.rs index 9d267cdab..4fd6bfdf1 100644 --- a/litebox_broker_channel/src/lib.rs +++ b/litebox_broker_channel/src/lib.rs @@ -8,6 +8,11 @@ //! one [`BrokerRequest`] produces one [`BrokerResponse`]. Concrete IPC //! implementations own framing, buffering, authentication, and the mechanism; //! non-blocking IPCs can provide a blocking adapter at this boundary. +//! The current control channel is serial: clients should wait for the response +//! to one request before sending the next. A future ring-buffer or multiplexed +//! transport can preserve that shape with an adapter, or add correlation IDs in +//! a protocol extension if concurrent in-flight control requests become +//! necessary. //! //! Broker-initiated readiness, interrupt, fault, revocation, or session-failure //! traffic must use a separately named notification channel and notification diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 31c7e67b9..457c8512a 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -19,6 +19,8 @@ pub enum BrokerError { InvalidRights, /// Broker-side resource exhaustion. ResourceExhausted, + /// Policy returned a decision that does not match the authorized operation. + InvalidPolicyDecision, } impl fmt::Display for BrokerError { @@ -30,6 +32,7 @@ impl fmt::Display for BrokerError { 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::InvalidPolicyDecision => f.write_str("invalid broker policy decision"), } } } diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 6fcf11533..6d2820df7 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -36,13 +36,13 @@ pub enum WaitOutcome { impl BrokerCore

{ /// Creates a broker-owned event object. pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { - self.authorize_create_object(association, ObjectType::Event)?; + let rights = self.authorize_create_object(association, ObjectType::Event)?; self.insert_object_with_reference( association, ObjectKind::Event(EventObject::new()), ObjectType::Event, - ObjectRights::WAIT | ObjectRights::WRITE, + rights, ) } @@ -117,7 +117,10 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerCore, CallerCredential, EventOnlyPolicy, ObjectReferenceGeneration}; + use crate::{ + BrokerCore, CallerCredential, EventOnlyPolicy, ObjectOperation, ObjectReferenceGeneration, + PolicyDecision, PolicyEngine, PolicyOperation, + }; #[test] fn wait_rejects_reference_without_wait_right() { @@ -172,4 +175,44 @@ mod tests { Err(BrokerError::UnknownObject) ); } + + #[test] + fn create_event_uses_policy_granted_reference_rights() { + let mut core = BrokerCore::new(WaitOnlyCreatePolicy); + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + let handle = core.create_event(&association).unwrap(); + + assert!(matches!( + core.wait_event(&association, handle), + Ok(WaitOutcome::WouldBlock(_)) + )); + assert_eq!( + core.signal_event(&association, handle), + Err(BrokerError::InvalidRights) + ); + } + + struct WaitOnlyCreatePolicy; + + impl PolicyEngine for WaitOnlyCreatePolicy { + fn authorize(&mut self, operation: PolicyOperation) -> Result { + match operation { + PolicyOperation::Object { + object_type: ObjectType::Event, + operation: ObjectOperation::Create, + .. + } => Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT, + }), + PolicyOperation::Object { + object_type: ObjectType::Event, + operation: ObjectOperation::Use { .. }, + .. + } => Ok(PolicyDecision::Authorized), + } + } + } } diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 70f27593b..e18954934 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -64,10 +64,12 @@ impl AssociationIdentity { /// /// 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. +/// on that association. The current architecture expects one BrokerCore per +/// broker, so this token is scoped by broker-assigned session and process +/// identity rather than by a separate core identifier. #[derive(Debug, PartialEq, Eq)] pub struct BrokerAssociation { - /// Broker-assigned sandbox session identity. + /// Broker-assigned sandbox session and guest process identity. identity: AssociationIdentity, /// Broker-entry-authenticated caller credential for this association. caller_credential: CallerCredential, diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index c85010534..6b6ced50a 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -29,7 +29,8 @@ pub use identity::{BrokerAssociation, CallerCredential}; use object::{ObjectEntry, ObjectId, ObjectReference}; pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; pub use policy::{ - DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyEngine, PolicyOperation, + DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyDecision, PolicyEngine, + PolicyOperation, }; pub use types::{ObjectRights, ObjectType}; diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 238c4193c..eefe1edf6 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -6,8 +6,8 @@ use alloc::vec::Vec; use crate::event::EventObject; use crate::identity::{AssociationIdentity, BrokerAssociation}; use crate::{ - BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, PolicyOperation, Result, - allocate_id, + BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyDecision, PolicyEngine, + PolicyOperation, Result, allocate_id, }; macro_rules! id_type { @@ -145,11 +145,14 @@ impl BrokerCore

{ &mut self, association: &BrokerAssociation, object_type: ObjectType, - ) -> Result<()> { - self.policy.authorize(PolicyOperation::create_object( + ) -> 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( @@ -160,12 +163,14 @@ impl BrokerCore

{ rights: ObjectRights, ) -> Result { let object_id = self.validate_handle(association, handle, object_type, rights)?; - self.policy.authorize(PolicyOperation::use_object( + match self.policy.authorize(PolicyOperation::use_object( association.caller_credential(), object_type, rights, - ))?; - Ok(object_id) + ))? { + PolicyDecision::Authorized => Ok(object_id), + _ => Err(BrokerError::InvalidPolicyDecision), + } } pub(crate) fn object(&self, object_id: ObjectId) -> Result<&ObjectEntry> { @@ -187,16 +192,7 @@ impl BrokerCore

{ expected_type: ObjectType, required_rights: ObjectRights, ) -> Result { - let reference = self - .references - .get(&handle.reference_id) - .ok_or(BrokerError::UnknownObject)?; - if reference.owner != association.identity() { - return Err(BrokerError::UnknownObject); - } - if reference.reference_generation != handle.reference_generation { - return Err(BrokerError::StaleHandle); - } + let reference = self.reference_for_handle(association, handle)?; if reference.object_type != expected_type { return Err(BrokerError::WrongObjectType); } @@ -225,6 +221,24 @@ impl BrokerCore

{ } 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 identity = association.identity(); @@ -236,23 +250,42 @@ impl

BrokerCore

{ }) .collect::>(); + let mut object_ids = Vec::new(); for reference_id in reference_ids { - self.references.remove(&reference_id); + if let Some(reference) = self.references.remove(&reference_id) { + object_ids.push(reference.object_id); + } } - let object_ids = self - .objects - .keys() - .copied() - .filter(|object_id| { - !self - .references - .values() - .any(|reference| reference.object_id == *object_id) - }) - .collect::>(); - for object_id in object_ids { + self.drop_object_if_unreferenced(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.identity() { + 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); } } @@ -311,4 +344,51 @@ mod tests { assert!(core.references.is_empty()); assert!(core.objects.is_empty()); } + + #[test] + fn close_object_reference_releases_reference_and_orphaned_object() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let handle = core.create_event(&association).unwrap(); + + assert_eq!(core.close_object_reference(&association, handle), Ok(())); + assert!(core.references.is_empty()); + assert!(core.objects.is_empty()); + assert_eq!( + core.close_object_reference(&association, handle), + Err(BrokerError::UnknownObject) + ); + } + + #[test] + fn close_object_reference_rejects_stale_and_foreign_handles() { + let mut core = BrokerCore::new(EventOnlyPolicy); + 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(crate::WaitOutcome::WouldBlock(_)) + )); + } } diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 824bb73a2..534f9e9af 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -28,6 +28,19 @@ pub enum ObjectOperation { 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( @@ -58,7 +71,7 @@ impl PolicyOperation { /// Broker policy decision interface. pub trait PolicyEngine { /// Authorizes or denies a broker operation. - fn authorize(&mut self, operation: PolicyOperation) -> Result<(), BrokerError>; + fn authorize(&mut self, operation: PolicyOperation) -> Result; } /// Policy engine that denies every operation. @@ -66,33 +79,37 @@ pub trait PolicyEngine { pub struct DefaultDenyPolicy; impl PolicyEngine for DefaultDenyPolicy { - fn authorize(&mut self, _operation: PolicyOperation) -> Result<(), BrokerError> { + fn authorize(&mut self, _operation: PolicyOperation) -> Result { Err(BrokerError::PolicyDenied) } } /// Policy engine that allows only the first POC event-object surface. /// -/// The current event methods request exactly one operation right at a time: -/// `WAIT` for wait and `WRITE` for signal. Combined-right use requests are -/// intentionally denied until an event method that needs combined rights is -/// designed. +/// The current event create operation grants `WAIT | WRITE` on the initial +/// reference. Use requests may ask for any non-empty subset of those rights. #[derive(Clone, Copy, Debug, Default)] pub struct EventOnlyPolicy; +const EVENT_REFERENCE_RIGHTS: ObjectRights = ObjectRights::WAIT.union(ObjectRights::WRITE); + impl PolicyEngine for EventOnlyPolicy { - fn authorize(&mut self, operation: PolicyOperation) -> Result<(), BrokerError> { + fn authorize(&mut self, operation: PolicyOperation) -> Result { match operation { PolicyOperation::Object { caller_credential: CallerCredential::Unauthenticated, object_type: ObjectType::Event, operation: ObjectOperation::Create, - } => Ok(()), + } => Ok(PolicyDecision::GrantObjectReference { + rights: EVENT_REFERENCE_RIGHTS, + }), PolicyOperation::Object { caller_credential: CallerCredential::Unauthenticated, object_type: ObjectType::Event, operation: ObjectOperation::Use { rights }, - } if rights == ObjectRights::WAIT || rights == ObjectRights::WRITE => Ok(()), + } if !rights.is_empty() && EVENT_REFERENCE_RIGHTS.contains(rights) => { + Ok(PolicyDecision::Authorized) + } PolicyOperation::Object { object_type: ObjectType::Event, .. @@ -114,7 +131,9 @@ mod tests { CallerCredential::Unauthenticated, ObjectType::Event )), - Ok(()) + Ok(PolicyDecision::GrantObjectReference { + rights: ObjectRights::WAIT | ObjectRights::WRITE + }) ); assert_eq!( policy.authorize(PolicyOperation::use_object( @@ -122,7 +141,7 @@ mod tests { ObjectType::Event, ObjectRights::WAIT )), - Ok(()) + Ok(PolicyDecision::Authorized) ); assert_eq!( policy.authorize(PolicyOperation::use_object( @@ -130,7 +149,7 @@ mod tests { ObjectType::Event, ObjectRights::WRITE )), - Ok(()) + Ok(PolicyDecision::Authorized) ); assert_eq!( policy.authorize(PolicyOperation::use_object( @@ -138,6 +157,14 @@ mod tests { 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) ); } diff --git a/litebox_broker_core/src/types.rs b/litebox_broker_core/src/types.rs index 09d92c395..916a9b39f 100644 --- a/litebox_broker_core/src/types.rs +++ b/litebox_broker_core/src/types.rs @@ -17,21 +17,34 @@ pub enum ObjectType { 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 signaling an event. 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(self.0 | rhs.0) + self.union(rhs) } } diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_server/src/lib.rs index c7675cff3..c42be3342 100644 --- a/litebox_broker_server/src/lib.rs +++ b/litebox_broker_server/src/lib.rs @@ -9,6 +9,9 @@ #![no_std] +#[cfg(test)] +extern crate std; + mod server; pub use server::{ diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index d3d9ae184..3b787496c 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -491,6 +491,109 @@ mod tests { assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } + #[test] + fn serve_connection_negotiates_routes_event_and_returns_peer_closed() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut channel = FakeServerChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ))), + Ok(Some(ReceivedBrokerRequest::Request(event_request( + EventRequest::Create, + )))), + Ok(None), + ])); + + assert_eq!( + serve_connection(&mut core, &mut channel).unwrap(), + ConnectionTermination::PeerClosed + ); + assert_eq!( + channel.responses[0], + BrokerResponse::Negotiated { + broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + } + ); + let handle = match channel.responses[1] { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, + response => panic!("unexpected response: {response:?}"), + }; + + let probe = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + assert_eq!( + core.close_object_reference(&probe, to_core_handle(handle)), + Err(BrokerError::UnknownObject) + ); + } + + #[test] + fn serve_connection_closes_after_protocol_violation() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut channel = FakeServerChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request(event_request( + EventRequest::Create, + )))), + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ))), + ])); + + assert_eq!( + serve_connection(&mut core, &mut channel).unwrap(), + ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) + ); + assert_eq!( + channel.responses, + [BrokerResponse::Error(ErrorCode::ProtocolState)] + ); + assert_eq!(channel.requests.len(), 1); + } + + #[test] + fn serve_connection_returns_channel_error_after_cleanup_path() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut channel = FakeServerChannel::new(std::vec::Vec::from([ + Ok(Some(ReceivedBrokerRequest::Request( + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ))), + Ok(Some(ReceivedBrokerRequest::Request(event_request( + EventRequest::Create, + )))), + Err(FakeChannelError::Recv), + ])); + + match serve_connection(&mut core, &mut channel) { + Err(BrokerServeError::Channel(FakeChannelError::Recv)) => {} + result => panic!("unexpected serve result: {result:?}"), + } + assert_eq!(channel.responses.len(), 2); + } + + #[test] + fn serve_connection_returns_channel_error_when_response_send_fails() { + let mut core = BrokerCore::new(EventOnlyPolicy); + let mut channel = FakeServerChannel::new(std::vec::Vec::from([Ok(Some( + ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }), + ))])); + channel.send_error = Some(FakeChannelError::Send); + + match serve_connection(&mut core, &mut channel) { + Err(BrokerServeError::Channel(FakeChannelError::Send)) => {} + result => panic!("unexpected serve result: {result:?}"), + } + assert!(channel.responses.is_empty()); + } + fn assert_protocol_violation(dispatch: BrokerDispatch) { assert_eq!( dispatch.response, @@ -544,4 +647,71 @@ mod tests { const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } + + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + enum FakeChannelError { + Recv, + Send, + } + + impl fmt::Display for FakeChannelError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Recv => f.write_str("fake receive error"), + Self::Send => f.write_str("fake send error"), + } + } + } + + impl core::error::Error for FakeChannelError {} + + struct FakeServerChannel { + requests: + std::vec::Vec, FakeChannelError>>, + responses: std::vec::Vec, + send_error: Option, + } + + impl FakeServerChannel { + fn new( + requests: std::vec::Vec< + core::result::Result, FakeChannelError>, + >, + ) -> Self { + Self { + requests, + responses: std::vec::Vec::new(), + send_error: None, + } + } + } + + impl ServerControlChannel for FakeServerChannel { + 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); + Ok(()) + } + } } From 00b6b303e69ec008c28d79aa9c1f43c957355f9b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 29 May 2026 19:38:04 -0700 Subject: [PATCH 18/66] Integrate split broker with linux userland runner Connect the linux userland runner to an externally managed split-broker Unix socket and verify the broker connection during startup. Add setup-deadline handling to the Unix socket broker client so connect, negotiation, and verification cannot hang indefinitely. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 5 + litebox_broker_client/src/lib.rs | 5 + litebox_broker_unix_socket/src/lib.rs | 145 ++++++++++++++++-- litebox_runner_linux_userland/Cargo.toml | 5 + litebox_runner_linux_userland/src/lib.rs | 19 ++- .../src/split_broker.rs | 113 ++++++++++++++ litebox_runner_linux_userland/tests/run.rs | 57 +++++++ 7 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 litebox_runner_linux_userland/src/split_broker.rs diff --git a/Cargo.lock b/Cargo.lock index c4bda5db3..17b331f7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1707,6 +1707,11 @@ dependencies = [ "glob", "libc", "litebox", + "litebox_broker_client", + "litebox_broker_core", + "litebox_broker_protocol", + "litebox_broker_server", + "litebox_broker_unix_socket", "litebox_common_linux", "litebox_platform_linux_userland", "litebox_platform_multiplex", diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index 228dfd46d..a1fecc48f 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -46,6 +46,11 @@ impl BrokerClient { 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 BrokerClient { diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 4aad1f42d..aaf4e988e 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -10,6 +10,7 @@ use std::io::{self, Read, Write}; use std::os::unix::net::UnixStream; use std::path::Path; +use std::time::{Duration, Instant}; use litebox_broker_channel::{ ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, @@ -23,18 +24,42 @@ const MAX_FRAME_LEN: usize = 64 * 1024; /// Client-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamClientControlChannel { stream: UnixStream, + io_deadline: Option, } impl UnixStreamClientControlChannel { /// Creates a client control channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { - Self { stream } + Self { + stream, + io_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_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; + match deadline { + Some(deadline) => self.set_stream_io_timeout(Some(io_timeout_for_deadline(deadline)?)), + None => self.set_stream_io_timeout(None), + } + } + + fn set_stream_io_timeout(&self, timeout: Option) -> io::Result<()> { + self.stream.set_read_timeout(timeout)?; + self.stream.set_write_timeout(timeout) + } } /// Server-side Unix-domain-socket control channel for the hosted userland POC. @@ -53,14 +78,15 @@ impl ClientControlChannel for UnixStreamClientControlChannel { type Error = io::Error; fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { - write_frame( + write_frame_with_deadline( &mut self.stream, &encode_request(*request).map_err(wire_error)?, + self.io_deadline, ) } fn recv_response(&mut self) -> io::Result> { - let Some(frame) = read_frame(&mut self.stream)? else { + let Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { return Ok(None); }; decode_response(&frame).map(Some).map_err(wire_error) @@ -92,9 +118,17 @@ impl ServerControlChannel for UnixStreamServerControlChannel { } fn read_frame(stream: &mut UnixStream) -> io::Result>> { + read_frame_with_deadline(stream, None) +} + +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")), @@ -110,23 +144,73 @@ fn read_frame(stream: &mut UnixStream) -> io::Result>> { } let mut frame = vec![0; len]; - stream.read_exact(&mut frame).map_err(|error| { - if error.kind() == io::ErrorKind::UnexpectedEof { - invalid_data("truncated broker frame") - } else { - error + 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(stream: &mut UnixStream, frame: &[u8]) -> io::Result<()> { + write_frame_with_deadline(stream, frame, None) +} + +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"))?; - stream.write_all(&len.to_le_bytes())?; - stream.write_all(frame) + 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 invalid_data(message: &'static str) -> io::Error { @@ -195,4 +279,43 @@ mod tests { io::ErrorKind::InvalidData ); } + + #[test] + fn client_response_read_honors_io_timeout() { + let (client, _server) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamClientControlChannel::from_connected(client); + channel + .set_io_timeout(Some(Duration::from_millis(10))) + .unwrap(); + + let error = channel.recv_response().unwrap_err(); + assert!( + matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ), + "unexpected timeout error kind: {error:?}" + ); + } + + #[test] + fn client_response_read_honors_io_deadline() { + let (mut server, client) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamClientControlChannel::from_connected(client); + channel + .set_io_deadline(Some(Instant::now() + Duration::from_millis(20))) + .unwrap(); + + let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); + server.write_all(&8u32.to_le_bytes()).unwrap(); + + let error = reader.join().expect("deadline reader panicked"); + assert!( + matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ), + "unexpected deadline error kind: {error:?}" + ); + } } diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 6a5b88374..164e2c23e 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_client = { version = "0.1.0", path = "../litebox_broker_client" } +litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } +litebox_broker_unix_socket = { version = "0.1.0", path = "../litebox_broker_unix_socket" } 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_server = { version = "0.1.0", path = "../litebox_broker_server" } [features] lock_tracing = ["litebox/lock_tracing"] diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 9a18b4a7c..e28e93638 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 split_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 split broker Unix socket and verify the control path. + #[arg( + long = "split-broker-socket", + value_name = "PATH", + value_hint = clap::ValueHint::FilePath, + requires = "unstable", + help_heading = "Unstable Options" + )] + pub split_broker_socket: Option, } struct MmappedFile { @@ -201,6 +212,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); + let split_broker = split_broker::connect(cli_args.split_broker_socket.as_deref())?; + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); let litebox = shim_builder.litebox(); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. @@ -414,7 +427,11 @@ pub fn run(cli_args: CliArgs) -> Result<()> { shutdown.store(true, core::sync::atomic::Ordering::Relaxed); net_worker.join().unwrap(); } - std::process::exit(program.process.wait()) + let exit_status = program.process.wait(); + if let Some(split_broker) = split_broker { + split_broker.shutdown(); + } + std::process::exit(exit_status) } /// Pin the current thread to a specific CPU core diff --git a/litebox_runner_linux_userland/src/split_broker.rs b/litebox_runner_linux_userland/src/split_broker.rs new file mode 100644 index 000000000..cee9708e6 --- /dev/null +++ b/litebox_runner_linux_userland/src/split_broker.rs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::{ + path::Path, + thread, + time::{Duration, Instant}, +}; + +use anyhow::{Context as _, Result, ensure}; +use litebox_broker_client::BrokerClient; +use litebox_broker_protocol::{ReadinessState, WaitOutcome}; +use litebox_broker_unix_socket::UnixStreamClientControlChannel; + +const SETUP_TIMEOUT: Duration = Duration::from_secs(5); +const RETRY_DELAY: Duration = Duration::from_millis(20); + +type Client = BrokerClient; + +pub(crate) struct SplitBrokerConnection { + client: Option, +} + +pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { + match socket_path { + Some(path) => connect_to_endpoint(path).map(Some), + None => Ok(None), + } +} + +impl SplitBrokerConnection { + pub(crate) fn shutdown(mut self) { + self.client.take(); + } +} + +impl Drop for SplitBrokerConnection { + fn drop(&mut self) { + self.client.take(); + } +} + +fn connect_to_endpoint(socket_path: &Path) -> Result { + let setup_deadline = Instant::now() + SETUP_TIMEOUT; + let mut client = connect_with_retry(socket_path, setup_deadline).with_context(|| { + format!( + "failed to connect to split broker at {}", + socket_path.display() + ) + })?; + verify_broker_connection(&mut client)?; + client + .control_channel_mut() + .set_io_deadline(None) + .context("failed to clear split broker setup deadline")?; + Ok(SplitBrokerConnection { + client: Some(client), + }) +} + +fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { + loop { + match UnixStreamClientControlChannel::connect(socket_path) { + Ok(mut channel) => { + channel + .set_io_deadline(Some(setup_deadline)) + .context("failed to configure split broker setup deadline")?; + let mut client = BrokerClient::new(channel); + client + .negotiate() + .context("split broker negotiation failed")?; + return Ok(client); + } + Err(error) => { + if Instant::now() >= setup_deadline { + return Err(error).context("timed out connecting to split broker"); + } + } + } + let remaining = setup_deadline.saturating_duration_since(Instant::now()); + thread::sleep(RETRY_DELAY.min(remaining)); + } +} + +fn verify_broker_connection(client: &mut Client) -> Result<()> { + let handle = client + .create_event() + .context("split broker event create verification failed")?; + let outcome = client + .wait_event(handle) + .context("split broker event wait verification failed")?; + ensure!( + outcome == WaitOutcome::WouldBlock(ReadinessState::new(false, 0)), + "new split broker event had unexpected wait outcome: {outcome:?}" + ); + + let readiness = client + .signal_event(handle) + .context("split broker event signal verification failed")?; + ensure!( + readiness == ReadinessState::new(true, 1), + "split broker event signal returned unexpected readiness: {readiness:?}" + ); + + let outcome = client + .wait_event(handle) + .context("split broker event ready-wait verification failed")?; + ensure!( + outcome == WaitOutcome::Ready(readiness), + "signaled split broker event had unexpected wait outcome: {outcome:?}" + ); + Ok(()) +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 3df36a850..777e347e3 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -119,6 +119,12 @@ impl Runner { self } + #[cfg(all(target_arch = "x86_64", target_os = "linux"))] + fn split_broker_socket(&mut self, socket_path: &Path) -> &mut Self { + self.command.arg("--split-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); @@ -226,6 +232,57 @@ 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"))] +#[test] +fn test_runner_connects_to_split_broker() { + let socket_path = unique_test_socket_path("runner-split-broker"); + let _ = std::fs::remove_file(&socket_path); + + let (ready_tx, ready_rx) = std::sync::mpsc::channel(); + let server_socket_path = socket_path.clone(); + let broker_thread = std::thread::spawn(move || { + let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) + .expect("failed to bind split broker test socket"); + ready_tx.send(()).expect("failed to report broker ready"); + + let (stream, _) = listener.accept().expect("failed to accept broker client"); + let mut channel = + litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); + let mut core = litebox_broker_core::BrokerCore::new(litebox_broker_core::EventOnlyPolicy); + let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) + .expect("split broker server failed"); + assert_eq!( + termination, + litebox_broker_server::ConnectionTermination::PeerClosed + ); + let _ = std::fs::remove_file(server_socket_path); + }); + + ready_rx + .recv_timeout(std::time::Duration::from_secs(5)) + .expect("split broker test server did not start"); + let true_path = run_which("true"); + Runner::new(&true_path, "split_broker_true_rewriter") + .split_broker_socket(&socket_path) + .run(); + broker_thread + .join() + .expect("split broker test server panicked"); + let _ = std::fs::remove_file(socket_path); +} + #[cfg(target_arch = "x86_64")] #[test] fn test_node_with_rewriter() { From d12f7f8920daccb139abde6ed59bed0989b01afe Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 1 Jun 2026 13:19:51 -0700 Subject: [PATCH 19/66] Rename broker runner integration Replace split-broker terminology in the linux userland runner with broker naming and update the unstable socket option to --broker-socket. Refresh the broker design and implementation docs to distinguish the current externally owned Unix-socket PoC from future deployment-profile negotiation and broker-owned runner launch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +- docs/broker-design.md | 46 ++++++++++--------- docs/impl-plan.md | 12 ++--- litebox_broker_server/src/lib.rs | 2 +- litebox_broker_unix_socket/src/lib.rs | 2 +- .../src/{split_broker.rs => broker.rs} | 44 ++++++++---------- litebox_runner_linux_userland/src/lib.rs | 14 +++--- litebox_runner_linux_userland/tests/run.rs | 22 ++++----- 8 files changed, 71 insertions(+), 75 deletions(-) rename litebox_runner_linux_userland/src/{split_broker.rs => broker.rs} (66%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 231845215..bcf7de954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -232,11 +232,11 @@ jobs: # # - `litebox_broker_unix_socket` is allowed to have `std` access, # since it is the concrete Unix-domain-socket channel for the - # userland split-broker proof of concept. + # userland broker proof of concept. # # - `litebox_broker_userland` is allowed to have `std` access, # since it is the hosted broker executable for the userland - # split-broker proof of concept. + # broker proof of concept. # # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. diff --git a/docs/broker-design.md b/docs/broker-design.md index 5065062f8..597467778 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -1,10 +1,10 @@ -# LiteBox Broker Split Design +# LiteBox Broker Architecture Design ## Goal Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel-backed deployments. -This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, a Windows-like ABI, or another guest ABI. The broker split should not assume any one shim's syscall set, process model, or resource vocabulary. +This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, a Windows-like ABI, or another guest ABI. The broker architecture should not assume any one shim's syscall set, process model, or resource vocabulary. The design separates LiteBox into: @@ -65,7 +65,7 @@ Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap ha ### UserLiteBox -Runs in user mode. It is the combined user-mode LiteBox component that replaces the earlier top-level split between user-core logic and user-platform mechanics. +Runs in user mode. It is the combined user-mode LiteBox component that replaces the earlier top-level separation between user-core logic and user-platform mechanics. UserLiteBox contains: @@ -129,7 +129,7 @@ Trusted backend for privileged operations: address-space control, host I/O, file ## Crate layout and naming -The broker split should use crate names that make the authority boundary visible. The first proof of concept should start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. +The broker architecture should use crate names that make the authority boundary visible. The first proof of concept should start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. | Crate | Initial role | |---|---| @@ -159,7 +159,7 @@ Negotiation separates the broker's max-supported protocol version from the effec The known broker protocol keeps the outer envelope intentionally small. Connection-level messages such as negotiation and common errors stay at the broker layer; BrokerCore/object operations are grouped below that layer by authority domain and object family, for example `BrokerRequest::Core(CoreRequest::Event(EventRequest::Wait { .. }))`. New object families should add a nested request/response family instead of growing a flat top-level `BrokerRequest`/`BrokerResponse` operation list. The wire codec may encode those nested families as layered tags, but tag widths and unknown-tag handling remain private to the codec. -Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical split: +Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical separation: | Future crate/layer | Role | |---|---| @@ -242,7 +242,7 @@ Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. Th ## Rust `std` and runtime strategy -The new split should not force one Rust runtime model everywhere. +The new architecture should not force one Rust runtime model everywhere. | Component | Recommended baseline | |---|---| @@ -306,7 +306,7 @@ Memory-management and process-management calls are the hard cases. If they canno ### Linux userland bootstrap profile -The durable-unicorn Linux experiment provides a concrete hosted-userland profile: +The durable-unicorn Linux experiment provides a future hosted-userland profile where the broker owns runner launch and ring setup: - the broker creates one anonymous `memfd` per spawned runner; - the `memfd` is inherited by the runner and identified by an environment/argument convention; @@ -314,6 +314,8 @@ The durable-unicorn Linux experiment provides a concrete hosted-userland profile - the broker binds the mapped ring set to the host-authenticated spawned runner identity; - the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. +This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, verifies a minimal create/wait/signal event path, and then starts the guest. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. + The initial Linux ring set can use five unidirectional rings: | Ring | Direction | Purpose | @@ -355,7 +357,7 @@ Shared spec crates should define: - host syscall profiles for bootstrap, fast local mode, and strict mode; - deployment profiles that bind a shim, UserLiteBox profile, broker channel, required services, and required broker features. -Startup should fail closed: +The eventual deployment contract should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. 2. The runner selects a UserLiteBox profile that matches the deployment's host ABI. @@ -365,6 +367,8 @@ Startup should fail closed: 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. +The current hosted PoC implements only the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a minimal event-operation verification bounded by the runner's broker setup deadline. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, and authenticated identity binding remain future work. + UserLiteBox should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. ## Security invariant @@ -435,7 +439,7 @@ POSIX-like `fork` is broker-mediated at the identity/channel/resource level, whi ## UserLiteBox vs BrokerCore in current `litebox` -The current `litebox` crate should not be split by whole module. Most modules mix ergonomic user-facing logic with authority-bearing state. The useful split is by responsibility. +The current `litebox` crate should not be migrated by whole module. Most modules mix ergonomic user-facing logic with authority-bearing state. The useful boundary is by responsibility. | Current area | Keep in UserLiteBox | Move to BrokerCore / broker side | |---|---|---| @@ -589,7 +593,7 @@ The broker path is required for authority changes, cross-workload operations, sh Moving the whole core into broker would make the trusted boundary too large and too chatty. It would also force guest pointer handling, guest ABI compatibility policy, and shim-specific logic into the trusted domain. -This split keeps the trusted computing base smaller: +This separation keeps the trusted computing base smaller: ```text User mode: @@ -605,7 +609,7 @@ Authority domain: |---|---| | UserLiteBox cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | | user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | -| ABI becomes too chatty | batching, shared memory data planes, control/event/data channel split, local private fast paths | +| ABI becomes too chatty | batching, shared memory data planes, control/event/data channel separation, local private fast paths | | duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in UserLiteBox | | handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | | broker bottleneck from no host-handle delegation | use broker-owned rings, batching, object-specific data channels, and policy caching | @@ -637,7 +641,7 @@ LiteBox's proposed design combines ideas from several systems rather than copyin | **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | UserLiteBox should not be a generic syscall escape hatch | | **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | | **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | -| **SKernel** | split guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | +| **SKernel** | separates guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | | **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore should keep object identity internal and expose reference capabilities with generation checks while storing authoritative rights | | **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | | **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | @@ -661,7 +665,7 @@ always in user mode. The current code does not match the final boundaries exactly. In particular, some current shims and platforms are linked together in the trusted domain for existing deployments. The mapping below describes the intended migration target. -Where a current crate spans both user-mode and authority responsibilities, the mapping below describes the destination split, not a one-to-one rename. +Where a current crate spans both user-mode and authority responsibilities, the mapping below describes the destination responsibility boundary, not a one-to-one rename. ### `litebox_shim_optee` @@ -763,24 +767,24 @@ Future mapping: | Current responsibility | Target component | |---|---| -| selecting a single `Platform` | split into UserLiteBox profile selection, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | +| selecting a single `Platform` | separate into UserLiteBox profile selection, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | | global platform accessor for shim-side code | UserLiteBox internal host-support accessor | | trusted backend selection | BrokerPlatform accessor inside the broker | | policy module/profile selection | PolicyEngine configuration inside the broker | -This split is needed because the current platform traits mix local execution mechanics with trusted authority. +This separation is needed because the current platform traits mix local execution mechanics with trusted authority. ### Other shims, platforms, and runners -The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-crate migration plan. Other existing crates follow the same destination split: +The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-crate migration plan. Other existing crates follow the same destination responsibility boundary: | Current component | Target shape | |---|---| | `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserLiteBox-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | | `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserLiteBox host-support implementations; native host calls are limited to local non-authoritative mechanics and broker channels/rings. | -| `litebox_platform_linux_kernel` | broker-kernel pieces split like LVBS: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes BrokerHost. | +| `litebox_platform_linux_kernel` | broker-kernel pieces follow the LVBS boundary: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes BrokerHost. | | `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserLiteBox profile, create Shim/UserLiteBox, authenticate to the broker, and negotiate deployment profile compatibility. | -| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; split external entry/channel support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | +| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; move external entry/channel support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | ## Initial implementation direction @@ -808,12 +812,12 @@ Early end-to-end shim tests should use a hybrid migration profile: only the migr Then proceed incrementally: -1. Define the component split: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. +1. Define the component boundary: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. 4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. -5. Add startup negotiation between runner/client and broker, including required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost features. -6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. Add duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup after the initial broker path is proven. +5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation and minimal event-path verification over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. +6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 8caf52081..f9b8fc1d5 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -1,8 +1,8 @@ -# Broker Split Implementation Plan +# Broker Architecture Implementation Plan ## Goal -Implement the broker split as incremental vertical slices while keeping the existing LiteBox behavior working. +Implement the broker architecture as incremental vertical slices while keeping the existing LiteBox behavior working. The target architecture is: @@ -95,7 +95,7 @@ Initial scope: Exit criteria: -- UserLiteBox can connect and negotiate. +- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket` and uses `litebox_broker_client` directly; routing through UserLiteBox remains part of Phase 3. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. @@ -146,7 +146,7 @@ UserLiteBox owns: Exit criteria: - Create, wait, and signal work through BrokerCore and the separate broker process. -- Duplicate, close, explicit readiness queries, stale-handle tests, and process-disconnect cleanup are added after the first end-to-end path is proven. +- BrokerCore and the server already release association-owned references on channel disconnect, and BrokerCore has an explicit in-domain `close_object_reference` operation. Protocol-level close, duplicate, explicit readiness queries, and broader stale-handle coverage remain future work after the first end-to-end path is proven. ## Phase 5: Broker-backed fd semantics @@ -176,7 +176,7 @@ Exit criteria: ## Phase 6: Control/notification/data channels -Add the durable-unicorn-style channel split. +Add the durable-unicorn-style control/notification/data channel separation. Channels: @@ -329,7 +329,7 @@ Exit criteria: Only after userland semantics are stable, implement broker-kernel deployment. -Split trusted deployment code into: +Separate trusted deployment code into: - BrokerHost: user-mode execution support and channel delivery; - BrokerPlatform: privileged backend execution; diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_server/src/lib.rs index c42be3342..0d7906a7b 100644 --- a/litebox_broker_server/src/lib.rs +++ b/litebox_broker_server/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Channel-neutral broker server loop for the split-broker proof of concept. +//! Channel-neutral broker server loop for the broker proof of concept. //! //! This crate wires `litebox_broker_core` to any implementation of the neutral //! server control-channel trait. Concrete channels live in separate crates such as diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index aaf4e988e..7fd68e6a9 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -97,7 +97,7 @@ impl ServerControlChannel for UnixStreamServerControlChannel { type Error = io::Error; fn peer_credential(&self) -> io::Result { - // TODO(split-broker): replace the PoC placeholder with Unix peer credential extraction + // TODO(broker): replace the PoC placeholder with Unix peer credential extraction // before this channel is used as an authenticated deployment boundary. Ok(PeerCredential::Unauthenticated) } diff --git a/litebox_runner_linux_userland/src/split_broker.rs b/litebox_runner_linux_userland/src/broker.rs similarity index 66% rename from litebox_runner_linux_userland/src/split_broker.rs rename to litebox_runner_linux_userland/src/broker.rs index cee9708e6..cc9c3f952 100644 --- a/litebox_runner_linux_userland/src/split_broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -17,43 +17,39 @@ const RETRY_DELAY: Duration = Duration::from_millis(20); type Client = BrokerClient; -pub(crate) struct SplitBrokerConnection { +pub(crate) struct BrokerConnection { client: Option, } -pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { +pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { match socket_path { Some(path) => connect_to_endpoint(path).map(Some), None => Ok(None), } } -impl SplitBrokerConnection { +impl BrokerConnection { pub(crate) fn shutdown(mut self) { self.client.take(); } } -impl Drop for SplitBrokerConnection { +impl Drop for BrokerConnection { fn drop(&mut self) { self.client.take(); } } -fn connect_to_endpoint(socket_path: &Path) -> Result { +fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let mut client = connect_with_retry(socket_path, setup_deadline).with_context(|| { - format!( - "failed to connect to split broker at {}", - socket_path.display() - ) - })?; + let mut client = connect_with_retry(socket_path, setup_deadline) + .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; verify_broker_connection(&mut client)?; client .control_channel_mut() .set_io_deadline(None) - .context("failed to clear split broker setup deadline")?; - Ok(SplitBrokerConnection { + .context("failed to clear broker setup deadline")?; + Ok(BrokerConnection { client: Some(client), }) } @@ -64,16 +60,14 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { channel .set_io_deadline(Some(setup_deadline)) - .context("failed to configure split broker setup deadline")?; + .context("failed to configure broker setup deadline")?; let mut client = BrokerClient::new(channel); - client - .negotiate() - .context("split broker negotiation failed")?; + client.negotiate().context("broker negotiation failed")?; return Ok(client); } Err(error) => { if Instant::now() >= setup_deadline { - return Err(error).context("timed out connecting to split broker"); + return Err(error).context("timed out connecting to broker"); } } } @@ -85,29 +79,29 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result Result<()> { let handle = client .create_event() - .context("split broker event create verification failed")?; + .context("broker event create verification failed")?; let outcome = client .wait_event(handle) - .context("split broker event wait verification failed")?; + .context("broker event wait verification failed")?; ensure!( outcome == WaitOutcome::WouldBlock(ReadinessState::new(false, 0)), - "new split broker event had unexpected wait outcome: {outcome:?}" + "new broker event had unexpected wait outcome: {outcome:?}" ); let readiness = client .signal_event(handle) - .context("split broker event signal verification failed")?; + .context("broker event signal verification failed")?; ensure!( readiness == ReadinessState::new(true, 1), - "split broker event signal returned unexpected readiness: {readiness:?}" + "broker event signal returned unexpected readiness: {readiness:?}" ); let outcome = client .wait_event(handle) - .context("split broker event ready-wait verification failed")?; + .context("broker event ready-wait verification failed")?; ensure!( outcome == WaitOutcome::Ready(readiness), - "signaled split broker event had unexpected wait outcome: {outcome:?}" + "signaled broker event had unexpected wait outcome: {outcome:?}" ); Ok(()) } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index e28e93638..fdaef1e86 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -9,7 +9,7 @@ use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; use std::path::{Path, PathBuf}; -mod split_broker; +mod broker; extern crate alloc; @@ -79,15 +79,15 @@ pub struct CliArgs { help_heading = "Unstable Options" )] pub program_from_tar: bool, - /// Connect to an already-running split broker Unix socket and verify the control path. + /// Connect to an already-running broker Unix socket and verify the control path. #[arg( - long = "split-broker-socket", + long = "broker-socket", value_name = "PATH", value_hint = clap::ValueHint::FilePath, requires = "unstable", help_heading = "Unstable Options" )] - pub split_broker_socket: Option, + pub broker_socket: Option, } struct MmappedFile { @@ -212,7 +212,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { } litebox_platform_multiplex::set_platform(platform); - let split_broker = split_broker::connect(cli_args.split_broker_socket.as_deref())?; + let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); let litebox = shim_builder.litebox(); @@ -428,8 +428,8 @@ pub fn run(cli_args: CliArgs) -> Result<()> { net_worker.join().unwrap(); } let exit_status = program.process.wait(); - if let Some(split_broker) = split_broker { - split_broker.shutdown(); + if let Some(broker_connection) = broker_connection { + broker_connection.shutdown(); } std::process::exit(exit_status) } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 777e347e3..955686693 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -120,8 +120,8 @@ impl Runner { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] - fn split_broker_socket(&mut self, socket_path: &Path) -> &mut Self { - self.command.arg("--split-broker-socket").arg(socket_path); + fn broker_socket(&mut self, socket_path: &Path) -> &mut Self { + self.command.arg("--broker-socket").arg(socket_path); self } @@ -246,15 +246,15 @@ fn unique_test_socket_path(name: &str) -> PathBuf { #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] -fn test_runner_connects_to_split_broker() { - let socket_path = unique_test_socket_path("runner-split-broker"); +fn test_runner_connects_to_broker() { + let socket_path = unique_test_socket_path("runner-broker"); let _ = std::fs::remove_file(&socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let server_socket_path = socket_path.clone(); let broker_thread = std::thread::spawn(move || { let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) - .expect("failed to bind split broker test socket"); + .expect("failed to bind broker test socket"); ready_tx.send(()).expect("failed to report broker ready"); let (stream, _) = listener.accept().expect("failed to accept broker client"); @@ -262,7 +262,7 @@ fn test_runner_connects_to_split_broker() { litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); let mut core = litebox_broker_core::BrokerCore::new(litebox_broker_core::EventOnlyPolicy); let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) - .expect("split broker server failed"); + .expect("broker server failed"); assert_eq!( termination, litebox_broker_server::ConnectionTermination::PeerClosed @@ -272,14 +272,12 @@ fn test_runner_connects_to_split_broker() { ready_rx .recv_timeout(std::time::Duration::from_secs(5)) - .expect("split broker test server did not start"); + .expect("broker test server did not start"); let true_path = run_which("true"); - Runner::new(&true_path, "split_broker_true_rewriter") - .split_broker_socket(&socket_path) + Runner::new(&true_path, "broker_true_rewriter") + .broker_socket(&socket_path) .run(); - broker_thread - .join() - .expect("split broker test server panicked"); + broker_thread.join().expect("broker test server panicked"); let _ = std::fs::remove_file(socket_path); } From 463396e7dbf9a904d37a1283e466f34584e068b1 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 1 Jun 2026 13:42:50 -0700 Subject: [PATCH 20/66] Keep broker startup negotiation-only Remove the event smoke test from the linux userland runner startup path so --broker-socket only connects and negotiates before launching the guest. Add a separate Unix-socket broker integration test for the event create/wait/signal path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 6 +-- litebox_runner_linux_userland/src/broker.rs | 34 +------------ litebox_runner_linux_userland/tests/run.rs | 54 +++++++++++++++++++-- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index 597467778..7f1050df5 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -314,7 +314,7 @@ The durable-unicorn Linux experiment provides a future hosted-userland profile w - the broker binds the mapped ring set to the host-authenticated spawned runner identity; - the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. -This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, verifies a minimal create/wait/signal event path, and then starts the guest. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. +This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, and then starts the guest without issuing broker object operations as a startup smoke test. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. The initial Linux ring set can use five unidirectional rings: @@ -367,7 +367,7 @@ The eventual deployment contract should fail closed: 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. -The current hosted PoC implements only the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a minimal event-operation verification bounded by the runner's broker setup deadline. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, and authenticated identity binding remain future work. +The current hosted PoC implements only the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and guest-visible broker object routing remain future work. UserLiteBox should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. @@ -816,7 +816,7 @@ Then proceed incrementally: 2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. 4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. -5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation and minimal event-path verification over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. +5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index cc9c3f952..e8b9e93a0 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -7,9 +7,8 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::{Context as _, Result, ensure}; +use anyhow::{Context as _, Result}; use litebox_broker_client::BrokerClient; -use litebox_broker_protocol::{ReadinessState, WaitOutcome}; use litebox_broker_unix_socket::UnixStreamClientControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -44,7 +43,6 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let mut client = connect_with_retry(socket_path, setup_deadline) .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; - verify_broker_connection(&mut client)?; client .control_channel_mut() .set_io_deadline(None) @@ -75,33 +73,3 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result Result<()> { - let handle = client - .create_event() - .context("broker event create verification failed")?; - let outcome = client - .wait_event(handle) - .context("broker event wait verification failed")?; - ensure!( - outcome == WaitOutcome::WouldBlock(ReadinessState::new(false, 0)), - "new broker event had unexpected wait outcome: {outcome:?}" - ); - - let readiness = client - .signal_event(handle) - .context("broker event signal verification failed")?; - ensure!( - readiness == ReadinessState::new(true, 1), - "broker event signal returned unexpected readiness: {readiness:?}" - ); - - let outcome = client - .wait_event(handle) - .context("broker event ready-wait verification failed")?; - ensure!( - outcome == WaitOutcome::Ready(readiness), - "signaled broker event had unexpected wait outcome: {outcome:?}" - ); - Ok(()) -} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 955686693..5d02a18f1 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -245,13 +245,14 @@ fn unique_test_socket_path(name: &str) -> PathBuf { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -#[test] -fn test_runner_connects_to_broker() { - let socket_path = unique_test_socket_path("runner-broker"); +fn spawn_test_broker

(socket_path: &Path, policy: P) -> std::thread::JoinHandle<()> +where + P: litebox_broker_core::PolicyEngine + Send + 'static, +{ let _ = std::fs::remove_file(&socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); - let server_socket_path = socket_path.clone(); + let server_socket_path = socket_path.to_path_buf(); let broker_thread = std::thread::spawn(move || { let listener = std::os::unix::net::UnixListener::bind(&server_socket_path) .expect("failed to bind broker test socket"); @@ -260,7 +261,7 @@ fn test_runner_connects_to_broker() { let (stream, _) = listener.accept().expect("failed to accept broker client"); let mut channel = litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); - let mut core = litebox_broker_core::BrokerCore::new(litebox_broker_core::EventOnlyPolicy); + let mut core = litebox_broker_core::BrokerCore::new(policy); let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) .expect("broker server failed"); assert_eq!( @@ -273,6 +274,15 @@ fn test_runner_connects_to_broker() { ready_rx .recv_timeout(std::time::Duration::from_secs(5)) .expect("broker test server did not start"); + broker_thread +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +fn test_runner_connects_to_broker() { + let socket_path = unique_test_socket_path("runner-broker"); + let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::DefaultDenyPolicy); + let true_path = run_which("true"); Runner::new(&true_path, "broker_true_rewriter") .broker_socket(&socket_path) @@ -281,6 +291,40 @@ fn test_runner_connects_to_broker() { let _ = std::fs::remove_file(socket_path); } +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +fn test_broker_event_path_over_unix_socket() { + use litebox_broker_protocol::{ReadinessState, WaitOutcome}; + + let socket_path = unique_test_socket_path("broker-event"); + let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::EventOnlyPolicy); + + let mut channel = + litebox_broker_unix_socket::UnixStreamClientControlChannel::connect(&socket_path) + .expect("failed to connect to broker test socket"); + channel + .set_io_timeout(Some(std::time::Duration::from_secs(5))) + .expect("failed to configure broker test timeout"); + let mut client = litebox_broker_client::BrokerClient::new(channel); + client.negotiate().expect("broker negotiation failed"); + + let handle = client.create_event().expect("event create failed"); + assert_eq!( + client.wait_event(handle).expect("event wait failed"), + WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + ); + let readiness = client.signal_event(handle).expect("event signal failed"); + assert_eq!(readiness, ReadinessState::new(true, 1)); + assert_eq!( + client.wait_event(handle).expect("event ready-wait failed"), + WaitOutcome::Ready(readiness) + ); + + drop(client); + broker_thread.join().expect("broker test server panicked"); + let _ = std::fs::remove_file(socket_path); +} + #[cfg(target_arch = "x86_64")] #[test] fn test_node_with_rewriter() { From dc3c445320526c97441a04fbb3e0f97ddd8ef23f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 12:50:27 -0700 Subject: [PATCH 21/66] Clean up broker protocol and eventfd path Centralize broker request/response DTOs and transport-neutral channel contracts in litebox_broker_protocol, remove the standalone litebox_broker_channel crate, and wire the broker-backed nonblocking eventfd path through the runner broker control boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 13 +- Cargo.toml | 2 - docs/broker-design.md | 12 +- docs/impl-plan.md | 4 +- litebox_broker_channel/Cargo.toml | 10 - litebox_broker_client/Cargo.toml | 1 - litebox_broker_client/src/event.rs | 64 +++- litebox_broker_client/src/lib.rs | 26 +- litebox_broker_client/src/negotiate.rs | 6 +- litebox_broker_core/Cargo.toml | 3 + litebox_broker_core/src/error.rs | 6 + litebox_broker_core/src/event.rs | 107 +++--- litebox_broker_core/src/lib.rs | 10 +- litebox_broker_core/src/object.rs | 62 +--- litebox_broker_protocol/Cargo.toml | 1 - .../src/channel.rs | 22 +- litebox_broker_protocol/src/error.rs | 5 + litebox_broker_protocol/src/event.rs | 158 +++++++++ litebox_broker_protocol/src/lib.rs | 21 +- litebox_broker_protocol/src/message.rs | 62 ++-- litebox_broker_protocol/src/object.rs | 4 +- litebox_broker_server/Cargo.toml | 1 - litebox_broker_server/src/server.rs | 150 +++------ litebox_broker_unix_socket/Cargo.toml | 1 - litebox_broker_unix_socket/src/lib.rs | 6 +- .../tests/userland_broker.rs | 2 +- litebox_broker_wire/Cargo.toml | 1 - litebox_broker_wire/src/lib.rs | 187 +++++++---- litebox_runner_linux_userland/src/broker.rs | 240 ++++++++++++- litebox_runner_linux_userland/src/lib.rs | 5 +- litebox_runner_linux_userland/tests/eventfd.c | 145 ++++++++ litebox_runner_linux_userland/tests/run.rs | 19 +- litebox_shim_linux/Cargo.toml | 1 + litebox_shim_linux/src/lib.rs | 30 +- litebox_shim_linux/src/syscalls/eventfd.rs | 271 +++++++++++++-- litebox_shim_linux/src/syscalls/file.rs | 315 +++++++++++++++++- 36 files changed, 1533 insertions(+), 440 deletions(-) delete mode 100644 litebox_broker_channel/Cargo.toml rename litebox_broker_channel/src/lib.rs => litebox_broker_protocol/src/channel.rs (73%) create mode 100644 litebox_broker_protocol/src/event.rs create mode 100644 litebox_runner_linux_userland/tests/eventfd.c diff --git a/Cargo.lock b/Cargo.lock index 17b331f7a..bffa2cc19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,24 +1475,19 @@ dependencies = [ ] [[package]] -name = "litebox_broker_channel" +name = "litebox_broker_client" version = "0.1.0" dependencies = [ "litebox_broker_protocol", ] [[package]] -name = "litebox_broker_client" +name = "litebox_broker_core" version = "0.1.0" dependencies = [ - "litebox_broker_channel", "litebox_broker_protocol", ] -[[package]] -name = "litebox_broker_core" -version = "0.1.0" - [[package]] name = "litebox_broker_protocol" version = "0.1.0" @@ -1501,7 +1496,6 @@ version = "0.1.0" name = "litebox_broker_server" version = "0.1.0" dependencies = [ - "litebox_broker_channel", "litebox_broker_core", "litebox_broker_protocol", ] @@ -1510,7 +1504,6 @@ dependencies = [ name = "litebox_broker_unix_socket" version = "0.1.0" dependencies = [ - "litebox_broker_channel", "litebox_broker_protocol", "litebox_broker_wire", ] @@ -1530,7 +1523,6 @@ dependencies = [ name = "litebox_broker_wire" version = "0.1.0" dependencies = [ - "litebox_broker_channel", "litebox_broker_protocol", ] @@ -1803,6 +1795,7 @@ dependencies = [ "bitvec", "libc", "litebox", + "litebox_broker_protocol", "litebox_common_linux", "litebox_platform_multiplex", "litebox_syscall_rewriter", diff --git a/Cargo.toml b/Cargo.toml index f73b735b8..a78b9da8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_channel", "litebox_broker_unix_socket", "litebox_broker_userland", "litebox_broker_wire", @@ -41,7 +40,6 @@ default-members = [ "litebox_broker_core", "litebox_broker_protocol", "litebox_broker_server", - "litebox_broker_channel", "litebox_broker_unix_socket", "litebox_broker_userland", "litebox_broker_wire", diff --git a/docs/broker-design.md b/docs/broker-design.md index 7f1050df5..8df62001c 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -133,9 +133,8 @@ The broker architecture should use crate names that make the authority boundary | Crate | Initial role | |---|---| -| `litebox_broker_protocol` | Shared `no_std` protocol crate for wire-visible types only: protocol version type, opaque event reference handles, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors for the first event-object path. Richer envelopes, feature negotiation, and wire-visible frame structures are added when later milestones need them. | +| `litebox_broker_protocol` | Shared `no_std` protocol crate for broker-visible DTOs and transport-neutral control-channel contracts: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and client/server control-channel traits. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_channel` | Neutral `no_std` channel-contract crate for blocking broker authority control-channel contracts shared by clients and broker entry loops, including explicit clean-close receive semantics and the channel-produced peer credential type. Concrete IPC implementations live outside this crate; non-blocking IPCs can provide blocking adapters at this boundary. | | `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream channel implementations reuse it rather than duplicating protocol encoding. | | `litebox_broker_server` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | @@ -145,11 +144,11 @@ The broker architecture should use crate names that make the authority boundary `litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. -Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore must also not depend on `litebox_broker_protocol` or `litebox_broker_channel`; the broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. -The apparent duplication between protocol types and BrokerCore domain types is intentional. Protocol types are the wire-visible compatibility contract, while BrokerCore types model authority-domain invariants; `litebox_broker_server` is the sanctioned mapping boundary between them. Do not merge those crates merely to remove duplicate handle/readiness shapes unless a future design preserves the same compatibility and authority separation. +Shared broker DTOs live in `litebox_broker_protocol` to avoid repeating request/response and handle/readiness shapes across protocol, core, client, wire, and server code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_server` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. @@ -795,7 +794,6 @@ The first proof of concept should use: ```text litebox_broker_protocol litebox_broker_core -litebox_broker_channel litebox_broker_wire litebox_broker_unix_socket litebox_broker_server @@ -813,9 +811,9 @@ Early end-to-end shim tests should use a hybrid migration profile: only the migr Then proceed incrementally: 1. Define the component boundary: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. -2. Create `litebox_broker_protocol` with only the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, and ABI-neutral errors needed for the first event-object path. +2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Create `litebox_broker_channel` with neutral client/server control-channel traits, `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. +4. Create `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. 5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index f9b8fc1d5..805673b7b 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -99,14 +99,14 @@ Exit criteria: - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. -- BrokerCore has no dependency on `litebox_broker_protocol`, `litebox_broker_channel`, wire codecs, or concrete IPC crates. +- BrokerCore depends only on shared broker value DTOs from `litebox_broker_protocol`; it does not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC crates. - Client code does not need to depend on the userland broker server crate to use the first Unix socket channel. - The generic broker server library does not depend on concrete Unix socket channel code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. - Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly; newer core error categories or wait outcomes that the server adapter cannot represent return `Internal`. - Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so clients can downgrade without reconnecting or guessing. - BrokerCore/object operations are grouped below the broker envelope instead of added as unrelated top-level `BrokerRequest` and `BrokerResponse` variants. -- Control-channel contracts live in `litebox_broker_channel` and stay separate from semantic protocol messages. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. +- Control-channel contracts live in `litebox_broker_protocol::channel`, separate from semantic message DTO modules but in the same shared protocol crate. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. ## Phase 3: UserLiteBox facade diff --git a/litebox_broker_channel/Cargo.toml b/litebox_broker_channel/Cargo.toml deleted file mode 100644 index f98acb10b..000000000 --- a/litebox_broker_channel/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "litebox_broker_channel" -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_client/Cargo.toml b/litebox_broker_client/Cargo.toml index f7acbc69b..7fe967476 100644 --- a/litebox_broker_client/Cargo.toml +++ b/litebox_broker_client/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_client/src/event.rs index 853a7b05b..5a4b56279 100644 --- a/litebox_broker_client/src/event.rs +++ b/litebox_broker_client/src/event.rs @@ -2,21 +2,30 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, - ObjectHandle, ReadinessState, WaitOutcome, + AddEventRequest, BrokerRequest, BrokerResponse, ClientControlChannel, ConsumeEventRequest, + ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, + EventRequest, EventResponse, ObjectHandle, ReadinessState, WaitEventRequest, WaitOutcome, }; -use litebox_broker_channel::ClientControlChannel; - use crate::{BrokerClient, ClientError, Result}; impl BrokerClient { /// 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_negotiated()?; - match self.request(event_request(EventRequest::Create))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => { - Ok(handle) + match self.request(event_request(EventRequest::Create( + CreateEventRequest::new(initial_count), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + Ok(response.handle) } response => Err(ClientError::UnexpectedResponse(response)), } @@ -25,20 +34,45 @@ impl BrokerClient { /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { self.ensure_negotiated()?; - match self.request(event_request(EventRequest::Wait { handle }))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Waited { outcome })) => { - Ok(outcome) + match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( + handle, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { + Ok(response.outcome) + } + response => Err(ClientError::UnexpectedResponse(response)), + } + } + + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( + &mut self, + handle: ObjectHandle, + value: u64, + ) -> Result { + self.ensure_negotiated()?; + match self.request(event_request(EventRequest::Add(AddEventRequest::new( + handle, value, + ))))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { + Ok(response.readiness) } response => Err(ClientError::UnexpectedResponse(response)), } } - /// Signals a broker-owned event object. - pub fn signal_event(&mut self, handle: ObjectHandle) -> Result { + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { self.ensure_negotiated()?; - match self.request(event_request(EventRequest::Signal { handle }))? { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Signaled { readiness })) => { - Ok(readiness) + match self.request(event_request(EventRequest::Consume( + ConsumeEventRequest::new(handle, mode), + )))? { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { + Ok(response) } response => Err(ClientError::UnexpectedResponse(response)), } diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index a1fecc48f..b34fc3754 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -5,7 +5,7 @@ //! //! The client owns request/response sequencing but does not own a channel. //! Userland, kernel, or ring-buffer deployments can provide channels by -//! implementing [`litebox_broker_channel::ClientControlChannel`]. +//! implementing [`litebox_broker_protocol::ClientControlChannel`]. #![no_std] @@ -16,8 +16,9 @@ mod error; mod event; mod negotiate; -use litebox_broker_channel::{ClientControlChannel, ReceivedBrokerResponse}; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, ReceivedBrokerResponse, +}; pub use error::{ClientError, Result}; @@ -77,6 +78,13 @@ impl BrokerClient { } pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { + match self.raw_request(request)? { + BrokerResponse::Error(error) => Err(ClientError::Broker(error)), + response => Ok(response), + } + } + + fn raw_request(&mut self, request: BrokerRequest) -> Result { self.channel .send_request(&request) .map_err(ClientError::Channel)?; @@ -86,13 +94,19 @@ impl BrokerClient { .map_err(ClientError::Channel)? .ok_or(ClientError::ChannelClosed)? { - ReceivedBrokerResponse::Response(BrokerResponse::Error(error)) => { - Err(ClientError::Broker(error)) - } ReceivedBrokerResponse::Response(response) => Ok(response), _ => Err(ClientError::UnknownResponse), } } + + /// Sends one request on an active connection and returns the raw protocol response. + pub fn active_raw_request( + &mut self, + request: BrokerRequest, + ) -> Result { + self.ensure_negotiated()?; + self.raw_request(request) + } } #[cfg(test)] diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_client/src/negotiate.rs index 9983c1474..ef42c7481 100644 --- a/litebox_broker_client/src/negotiate.rs +++ b/litebox_broker_client/src/negotiate.rs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ProtocolVersion}; - -use litebox_broker_channel::ClientControlChannel; +use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, +}; use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION, ClientError, Result}; diff --git a/litebox_broker_core/Cargo.toml b/litebox_broker_core/Cargo.toml index 42b5c700c..9b9894147 100644 --- a/litebox_broker_core/Cargo.toml +++ b/litebox_broker_core/Cargo.toml @@ -3,5 +3,8 @@ 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 index 457c8512a..1550e31bf 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -19,6 +19,10 @@ pub enum BrokerError { InvalidRights, /// Broker-side resource exhaustion. ResourceExhausted, + /// 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, } @@ -32,6 +36,8 @@ impl fmt::Display for BrokerError { 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::UnsupportedOperation => f.write_str("unsupported broker operation"), Self::InvalidPolicyDecision => f.write_str("invalid broker policy decision"), } } diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 6d2820df7..e92c76414 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -6,41 +6,32 @@ use crate::{ BrokerAssociation, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, PolicyEngine, Result, }; +use litebox_broker_protocol::{ + ConsumeEventResponse, EventConsumeMode, ReadinessState, WaitOutcome, +}; -/// Event readiness snapshot. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ReadinessState { - /// Whether the event is currently ready. - pub ready: bool, - /// Monotonic generation incremented on readiness changes. - pub generation: u64, -} - -impl ReadinessState { - /// Creates a readiness snapshot. - pub const fn new(ready: bool, generation: u64) -> Self { - Self { ready, generation } - } -} - -/// Result of checking an event wait. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum WaitOutcome { - /// The event was ready and a nonblocking wait would complete. - Ready(ReadinessState), - /// The event was not ready and a blocking wait would sleep. - WouldBlock(ReadinessState), -} +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()), + ObjectKind::Event(EventObject::new(initial_count)), ObjectType::Event, rights, ) @@ -66,16 +57,31 @@ impl BrokerCore

{ }) } - /// Signals a broker-owned event object. - pub fn signal_event( + /// Adds readiness credits to a broker-owned event object. + pub fn add_event( &mut self, association: &BrokerAssociation, handle: ObjectHandle, + value: u64, ) -> Result { let object_id = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; match &mut self.object_mut(object_id)?.kind { - ObjectKind::Event(event) => event.signal(), + ObjectKind::Event(event) => event.add(value), + } + } + + /// Consumes readiness credits from a broker-owned event object. + pub fn consume_event( + &mut self, + association: &BrokerAssociation, + handle: ObjectHandle, + mode: EventConsumeMode, + ) -> Result { + let object_id = + self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; + match &mut self.object_mut(object_id)?.kind { + ObjectKind::Event(event) => event.consume(mode), } } @@ -88,29 +94,54 @@ impl BrokerCore

{ #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct EventObject { - ready: bool, + count: u64, readiness_generation: u64, } impl EventObject { - pub(crate) const fn new() -> Self { + pub(crate) const fn new(count: u64) -> Self { Self { - ready: false, + count, readiness_generation: 0, } } pub(crate) const fn readiness_state(self) -> ReadinessState { - ReadinessState::new(self.ready, self.readiness_generation) + ReadinessState::new(self.count > 0, self.readiness_generation) } - fn signal(&mut self) -> Result { - self.ready = true; + 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)?; + self.count = new_count; + self.bump_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), + }; + self.count -= value; + self.bump_generation()?; + Ok(ConsumeEventResponse::new(value, self.readiness_state())) + } + + fn bump_generation(&mut self) -> Result<()> { self.readiness_generation = self .readiness_generation .checked_add(1) .ok_or(BrokerError::ResourceExhausted)?; - Ok(self.readiness_state()) + Ok(()) } } @@ -131,7 +162,7 @@ mod tests { let handle = core .insert_object_with_reference( &association, - ObjectKind::Event(EventObject::new()), + ObjectKind::Event(EventObject::new(0)), ObjectType::Event, ObjectRights::WRITE, ) @@ -190,7 +221,7 @@ mod tests { Ok(WaitOutcome::WouldBlock(_)) )); assert_eq!( - core.signal_event(&association, handle), + core.add_event(&association, handle, 1), Err(BrokerError::InvalidRights) ); } diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 6b6ced50a..adacce3b3 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -5,8 +5,8 @@ //! //! `litebox_broker_core` owns broker-side object identity, reference lifetime, //! rights checks, reference generation checks, and policy calls. It deliberately has no -//! dependency on protocol request/response types, Unix sockets, shared-memory -//! rings, kernel traps, or any other channel implementation. +//! dependency on protocol envelopes, Unix sockets, shared-memory rings, kernel +//! traps, or any other channel implementation. #![no_std] @@ -24,10 +24,12 @@ mod types; use alloc::collections::BTreeMap; pub use error::BrokerError; -pub use event::{ReadinessState, WaitOutcome}; pub use identity::{BrokerAssociation, CallerCredential}; +pub use litebox_broker_protocol::{ + ConsumeEventResponse, EventConsumeMode, ObjectHandle, ObjectReferenceGeneration, + ObjectReferenceId, ReadinessState, WaitOutcome, +}; use object::{ObjectEntry, ObjectId, ObjectReference}; -pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; pub use policy::{ DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index eefe1edf6..0f53b9d12 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -9,27 +9,7 @@ use crate::{ BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyDecision, PolicyEngine, PolicyOperation, Result, allocate_id, }; - -macro_rules! id_type { - ($(#[$meta:meta])* $name:ident) => { - $(#[$meta])* - #[repr(transparent)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub struct $name(u64); - - impl $name { - /// Creates an identifier from its raw value. - pub const fn new(raw: u64) -> Self { - Self(raw) - } - - /// Returns the raw identifier value. - pub const fn get(self) -> u64 { - self.0 - } - } - }; -} +use litebox_broker_protocol::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; /// Broker-owned object identifier. #[repr(transparent)] @@ -43,42 +23,6 @@ impl ObjectId { } } -id_type! { - /// Broker-owned object reference identifier. - ObjectReferenceId -} - -id_type! { - /// Generation attached to a broker object reference. - ObjectReferenceGeneration -} - -/// Broker-owned reference handle returned by BrokerCore. -/// -/// UserLiteBox 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, - } - } -} - const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGeneration::new(1); #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -308,7 +252,7 @@ mod tests { let handle = core .insert_object_with_reference( &association, - ObjectKind::Event(EventObject::new()), + ObjectKind::Event(EventObject::new(0)), ObjectType::Event, ObjectRights::WAIT, ) @@ -320,7 +264,7 @@ mod tests { assert_eq!( core.insert_object_with_reference( &association, - ObjectKind::Event(EventObject::new()), + ObjectKind::Event(EventObject::new(0)), ObjectType::Event, ObjectRights::WAIT, ), diff --git a/litebox_broker_protocol/Cargo.toml b/litebox_broker_protocol/Cargo.toml index cdb474acf..2fccbca4e 100644 --- a/litebox_broker_protocol/Cargo.toml +++ b/litebox_broker_protocol/Cargo.toml @@ -7,4 +7,3 @@ edition = "2024" [lints] workspace = true - diff --git a/litebox_broker_channel/src/lib.rs b/litebox_broker_protocol/src/channel.rs similarity index 73% rename from litebox_broker_channel/src/lib.rs rename to litebox_broker_protocol/src/channel.rs index 4fd6bfdf1..5b92f16f9 100644 --- a/litebox_broker_channel/src/lib.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -1,27 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Shared broker channel contracts. -//! -//! This crate defines delivery contracts for broker authority messages. The -//! current surface is intentionally limited to the paired control channel: -//! one [`BrokerRequest`] produces one [`BrokerResponse`]. Concrete IPC -//! implementations own framing, buffering, authentication, and the mechanism; -//! non-blocking IPCs can provide a blocking adapter at this boundary. -//! The current control channel is serial: clients should wait for the response -//! to one request before sending the next. A future ring-buffer or multiplexed -//! transport can preserve that shape with an adapter, or add correlation IDs in -//! a protocol extension if concurrent in-flight control requests become -//! necessary. -//! -//! Broker-initiated readiness, interrupt, fault, revocation, or session-failure -//! traffic must use a separately named notification channel and notification -//! message family when that lane is introduced. Such notifications must not be -//! delivered as unsolicited control-channel responses. - -#![no_std] - -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; +use crate::{BrokerRequest, BrokerResponse}; /// Peer identity information supplied by the channel or host layer. /// diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 72cd1d6e5..1562a6275 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -29,6 +29,8 @@ pub enum ErrorCode { 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 client. /// /// This variant is reserved for raw codes not assigned by this protocol @@ -57,6 +59,7 @@ impl ErrorCode { 7 => Self::WrongObjectType, 8 => Self::InvalidRights, 9 => Self::ResourceExhausted, + 13 => Self::WouldBlock, raw => Self::Unknown(raw), } } @@ -75,6 +78,7 @@ impl ErrorCode { Self::WrongObjectType => 7, Self::InvalidRights => 8, Self::ResourceExhausted => 9, + Self::WouldBlock => 13, Self::Unknown(raw) => raw, } } @@ -94,6 +98,7 @@ impl fmt::Display for ErrorCode { 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}"), } } diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs new file mode 100644 index 000000000..655980c89 --- /dev/null +++ b/litebox_broker_protocol/src/event.rs @@ -0,0 +1,158 @@ +// 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 the object is currently ready. + pub 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(ready: bool, generation: u64) -> Self { + Self { ready, generation } + } +} + +/// Result of checking whether a broker event wait would complete now. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum WaitOutcome { + /// The object is ready now. + Ready(ReadinessState), + /// The object is not 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 } + } +} + +/// Response to an event consume request. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ConsumeEventResponse { + /// The number of credits consumed. + pub value: u64, + /// Readiness state after consuming credits. + pub readiness: ReadinessState, +} + +impl ConsumeEventResponse { + /// Creates an event consume response. + pub const fn new(value: u64, readiness: ReadinessState) -> Self { + Self { value, readiness } + } +} diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index c07750a78..3260a284a 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -1,22 +1,33 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Shared broker protocol types. +//! Shared broker protocol types and channel contracts. //! -//! This crate is intentionally channel-neutral. It describes broker-visible -//! opaque handles, errors, and versions, but does not know whether the bytes -//! move over Unix sockets, shared rings, kernel traps, or another IPC mechanism. +//! 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] +mod channel; mod error; +mod event; mod message; mod object; +pub use channel::{ + ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, + ServerControlChannel, +}; pub use error::ErrorCode; +pub use event::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, EventConsumeMode, ReadinessState, WaitEventRequest, + WaitEventResponse, WaitOutcome, +}; pub use message::{ BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, - ReadinessState, WaitOutcome, }; pub use object::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 38c6abd27..f1b97f4c8 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -1,33 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use crate::{ErrorCode, ObjectHandle, ProtocolVersion}; - -/// Broker-authoritative readiness state for one object. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct ReadinessState { - /// Whether the object is currently ready. - pub 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(ready: bool, generation: u64) -> Self { - Self { ready, generation } - } -} - -/// Result of checking a wait condition in BrokerCore. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum WaitOutcome { - /// The object is ready now. - Ready(ReadinessState), - /// The object is not ready; deployment-specific wait plumbing may block. - WouldBlock(ReadinessState), -} +use crate::ProtocolVersion; +use crate::{ + AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, + CreateEventRequest, CreateEventResponse, ErrorCode, WaitEventRequest, WaitEventResponse, +}; /// Broker request sent over the control channel. /// @@ -59,17 +37,13 @@ pub enum CoreRequest { #[non_exhaustive] pub enum EventRequest { /// Create a broker-owned event object. - Create, + Create(CreateEventRequest), /// Check whether an event wait would complete now. - Wait { - /// Event handle. - handle: ObjectHandle, - }, - /// Signal an event. - Signal { - /// Event handle. - handle: ObjectHandle, - }, + 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. @@ -116,10 +90,12 @@ pub enum CoreResponse { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum EventResponse { - /// Operation returned a broker object handle. - Created { handle: ObjectHandle }, - /// Operation returned readiness state. - Signaled { readiness: ReadinessState }, - /// Wait operation returned wait state. - Waited { outcome: WaitOutcome }, + /// 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 index 78ab33c6f..141be47be 100644 --- a/litebox_broker_protocol/src/object.rs +++ b/litebox_broker_protocol/src/object.rs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -/// Broker object reference handle returned to UserLiteBox. +/// Broker object reference handle returned to the local core. /// -/// UserLiteBox may cache this value, but the broker remains authoritative for +/// 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)] diff --git a/litebox_broker_server/Cargo.toml b/litebox_broker_server/Cargo.toml index b1d9ac848..74444bdb7 100644 --- a/litebox_broker_server/Cargo.toml +++ b/litebox_broker_server/Cargo.toml @@ -6,7 +6,6 @@ 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" } -litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 3b787496c..2b87cec47 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -3,19 +3,13 @@ use core::fmt; -use litebox_broker_channel::{PeerCredential, ReceivedBrokerRequest, ServerControlChannel}; use litebox_broker_core::{ - BrokerAssociation, BrokerCore, BrokerError, CallerCredential, ObjectHandle as CoreObjectHandle, - ObjectReferenceGeneration as CoreObjectReferenceGeneration, - ObjectReferenceId as CoreObjectReferenceId, PolicyEngine, ReadinessState as CoreReadinessState, - WaitOutcome as CoreWaitOutcome, + BrokerAssociation, BrokerCore, BrokerError, CallerCredential, PolicyEngine, }; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, EventRequest, - EventResponse, ObjectHandle as ProtocolObjectHandle, - ObjectReferenceGeneration as ProtocolObjectReferenceGeneration, - ObjectReferenceId as ProtocolObjectReferenceId, ProtocolVersion, - ReadinessState as ProtocolReadinessState, WaitOutcome as ProtocolWaitOutcome, + AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, + CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, ProtocolVersion, + ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, }; /// Protocol version this broker server implementation supports. @@ -139,21 +133,22 @@ fn handle_event_request( request: EventRequest, ) -> BrokerResponse { match request { - EventRequest::Create => handle_core_result(core.create_event(association), |handle| { - event_response(EventResponse::Created { - handle: to_protocol_handle(handle), + 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::Wait { handle } => { - handle_wait_result(core.wait_event(association, to_core_handle(handle))) } - EventRequest::Signal { handle } => handle_core_result( - core.signal_event(association, to_core_handle(handle)), - |readiness| { - event_response(EventResponse::Signaled { - readiness: to_protocol_readiness_state(readiness), - }) - }, + 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), + |response| event_response(EventResponse::Consume(response)), ), _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } @@ -198,16 +193,6 @@ fn handle_core_result( } } -fn handle_wait_result(result: litebox_broker_core::Result) -> BrokerResponse { - match result { - Ok(outcome) => match to_protocol_wait_outcome(outcome) { - Some(outcome) => event_response(EventResponse::Waited { outcome }), - None => BrokerResponse::Error(ErrorCode::Internal), - }, - Err(error) => BrokerResponse::Error(to_protocol_error(error)), - } -} - const fn event_response(response: EventResponse) -> BrokerResponse { BrokerResponse::Core(CoreResponse::Event(response)) } @@ -220,40 +205,12 @@ fn to_protocol_error(error: BrokerError) -> ErrorCode { BrokerError::WrongObjectType => ErrorCode::WrongObjectType, BrokerError::InvalidRights => ErrorCode::InvalidRights, BrokerError::ResourceExhausted => ErrorCode::ResourceExhausted, + BrokerError::WouldBlock => ErrorCode::WouldBlock, + BrokerError::UnsupportedOperation => ErrorCode::UnsupportedOperation, _ => ErrorCode::Internal, } } -fn to_core_handle(handle: ProtocolObjectHandle) -> CoreObjectHandle { - CoreObjectHandle::new( - CoreObjectReferenceId::new(handle.reference_id.get()), - CoreObjectReferenceGeneration::new(handle.reference_generation.get()), - ) -} - -fn to_protocol_handle(handle: CoreObjectHandle) -> ProtocolObjectHandle { - ProtocolObjectHandle::new( - ProtocolObjectReferenceId::new(handle.reference_id.get()), - ProtocolObjectReferenceGeneration::new(handle.reference_generation.get()), - ) -} - -fn to_protocol_readiness_state(readiness: CoreReadinessState) -> ProtocolReadinessState { - ProtocolReadinessState::new(readiness.ready, readiness.generation) -} - -fn to_protocol_wait_outcome(outcome: CoreWaitOutcome) -> Option { - match outcome { - CoreWaitOutcome::Ready(readiness) => Some(ProtocolWaitOutcome::Ready( - to_protocol_readiness_state(readiness), - )), - CoreWaitOutcome::WouldBlock(readiness) => Some(ProtocolWaitOutcome::WouldBlock( - to_protocol_readiness_state(readiness), - )), - _ => None, - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum ConnectionState { AwaitingNegotiation, @@ -351,17 +308,15 @@ where mod tests { use super::*; use litebox_broker_core::EventOnlyPolicy; + use litebox_broker_protocol::{ + AddEventRequest, CreateEventRequest, ReadinessState, WaitEventRequest, WaitOutcome, + }; #[test] fn dispatch_enforces_negotiation_state() { let (mut core, association, mut state) = new_association(); - let dispatch = handle_request( - &mut core, - &association, - &mut state, - event_request(EventRequest::Create), - ); + let dispatch = handle_request(&mut core, &association, &mut state, event_create_request(0)); assert_protocol_violation(dispatch); assert_eq!(state, ConnectionState::AwaitingNegotiation); @@ -436,15 +391,12 @@ mod tests { let (mut core, association, mut state) = new_association(); negotiate(&mut core, &association, &mut state); - let dispatch = handle_request( - &mut core, - &association, - &mut state, - event_request(EventRequest::Create), - ); + let dispatch = handle_request(&mut core, &association, &mut state, event_create_request(0)); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); let handle = match dispatch.response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + response.handle + } response => panic!("unexpected response: {response:?}"), }; @@ -452,13 +404,13 @@ mod tests { &mut core, &association, &mut state, - event_request(EventRequest::Wait { handle }), + event_request(EventRequest::Wait(WaitEventRequest::new(handle))), ); assert_eq!( dispatch.response, - event_response(EventResponse::Waited { - outcome: ProtocolWaitOutcome::WouldBlock(ProtocolReadinessState::new(false, 0)) - }) + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); @@ -466,13 +418,13 @@ mod tests { &mut core, &association, &mut state, - event_request(EventRequest::Signal { handle }), + event_request(EventRequest::Add(AddEventRequest::new(handle, 1))), ); assert_eq!( dispatch.response, - event_response(EventResponse::Signaled { - readiness: ProtocolReadinessState::new(true, 1) - }) + event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, 1) + ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); @@ -480,13 +432,13 @@ mod tests { &mut core, &association, &mut state, - event_request(EventRequest::Wait { handle }), + event_request(EventRequest::Wait(WaitEventRequest::new(handle))), ); assert_eq!( dispatch.response, - event_response(EventResponse::Waited { - outcome: ProtocolWaitOutcome::Ready(ProtocolReadinessState::new(true, 1)) - }) + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, 1)) + ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); } @@ -500,8 +452,8 @@ mod tests { protocol_version: SUPPORTED_PROTOCOL_VERSION, }, ))), - Ok(Some(ReceivedBrokerRequest::Request(event_request( - EventRequest::Create, + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, )))), Ok(None), ])); @@ -517,7 +469,9 @@ mod tests { } ); let handle = match channel.responses[1] { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Created { handle })) => handle, + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { + response.handle + } response => panic!("unexpected response: {response:?}"), }; @@ -525,7 +479,7 @@ mod tests { .create_association(CallerCredential::Unauthenticated) .unwrap(); assert_eq!( - core.close_object_reference(&probe, to_core_handle(handle)), + core.close_object_reference(&probe, handle), Err(BrokerError::UnknownObject) ); } @@ -534,8 +488,8 @@ mod tests { fn serve_connection_closes_after_protocol_violation() { let mut core = BrokerCore::new(EventOnlyPolicy); let mut channel = FakeServerChannel::new(std::vec::Vec::from([ - Ok(Some(ReceivedBrokerRequest::Request(event_request( - EventRequest::Create, + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, )))), Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { @@ -564,8 +518,8 @@ mod tests { protocol_version: SUPPORTED_PROTOCOL_VERSION, }, ))), - Ok(Some(ReceivedBrokerRequest::Request(event_request( - EventRequest::Create, + Ok(Some(ReceivedBrokerRequest::Request(event_create_request( + 0, )))), Err(FakeChannelError::Recv), ])); @@ -648,6 +602,10 @@ mod tests { 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 { Recv, diff --git a/litebox_broker_unix_socket/Cargo.toml b/litebox_broker_unix_socket/Cargo.toml index bfe546565..1ef949412 100644 --- a/litebox_broker_unix_socket/Cargo.toml +++ b/litebox_broker_unix_socket/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } litebox_broker_wire = { path = "../litebox_broker_wire", version = "0.1.0" } [lints] diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 7fd68e6a9..8742eeb20 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -5,18 +5,18 @@ //! //! This crate deliberately uses `std` because Unix-domain sockets and `std::io` //! framing are hosted userland concerns. Portable broker interfaces live in the -//! no_std protocol, wire, channel, client, core, and server crates. +//! no_std protocol, wire, client, core, and server 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_channel::{ +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; +use litebox_broker_protocol::{ ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, ServerControlChannel, }; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; use litebox_broker_wire::{decode_request, decode_response, encode_request, encode_response}; const MAX_FRAME_LEN: usize = 64 * 1024; diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index b7c738a3d..3190d0f5d 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -30,7 +30,7 @@ fn separate_process_broker_serves_event_object_requests() { ); assert_eq!( - client.signal_event(handle).unwrap(), + client.add_event(handle, 1).unwrap(), ReadinessState::new(true, 1) ); diff --git a/litebox_broker_wire/Cargo.toml b/litebox_broker_wire/Cargo.toml index a174be7a3..38443e090 100644 --- a/litebox_broker_wire/Cargo.toml +++ b/litebox_broker_wire/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_channel = { path = "../litebox_broker_channel", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index 01646d80f..9317d268f 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -10,11 +10,13 @@ extern crate alloc; use core::fmt; use alloc::vec::Vec; -use litebox_broker_channel::{ReceivedBrokerRequest, ReceivedBrokerResponse}; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, ErrorCode, EventRequest, - EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, - ReadinessState, WaitOutcome, + AddEventRequest, AddEventResponse, BrokerRequest, BrokerResponse, ConsumeEventRequest, + ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, CreateEventResponse, + ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, + ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, ReadinessState, + ReceivedBrokerRequest, ReceivedBrokerResponse, WaitEventRequest, WaitEventResponse, + WaitOutcome, }; const REQUEST_TAG_NEGOTIATE: u8 = 0; @@ -22,7 +24,8 @@ const REQUEST_TAG_CORE: u8 = 1; const CORE_REQUEST_TAG_EVENT: u8 = 0; const EVENT_REQUEST_TAG_CREATE: u8 = 0; const EVENT_REQUEST_TAG_WAIT: u8 = 1; -const EVENT_REQUEST_TAG_SIGNAL: u8 = 2; +const EVENT_REQUEST_TAG_ADD: u8 = 2; +const EVENT_REQUEST_TAG_CONSUME: u8 = 3; const RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_CORE: u8 = 1; @@ -30,11 +33,14 @@ const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; const CORE_RESPONSE_TAG_EVENT: u8 = 0; const EVENT_RESPONSE_TAG_CREATED: u8 = 0; -const EVENT_RESPONSE_TAG_SIGNALED: u8 = 1; -const EVENT_RESPONSE_TAG_WAITED: u8 = 2; +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; /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -46,6 +52,8 @@ pub enum WireError { EncodeUnknownResponseTag, /// The encoder was asked to emit a wait-outcome tag this codec does not own. EncodeUnknownWaitOutcome, + /// The encoder was asked to emit an event consume mode this codec does not own. + EncodeUnknownEventConsumeMode, /// The frame ended before a complete field could be decoded. TruncatedFrame, /// The frame contained bytes after the decoded message. @@ -54,6 +62,8 @@ pub enum WireError { InvalidBoolean, /// The wait-outcome tag is unknown. UnknownWaitOutcome, + /// The event consume mode tag is unknown. + UnknownEventConsumeMode, /// A decoder offset overflowed. OffsetOverflow, } @@ -71,9 +81,13 @@ impl fmt::Display for WireError { Self::EncodeUnknownWaitOutcome => { f.write_str("cannot encode unknown broker wait outcome tag") } + Self::EncodeUnknownEventConsumeMode => { + f.write_str("cannot encode unknown broker event consume mode") + } Self::TrailingBytes => f.write_str("trailing broker wire bytes"), Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), Self::UnknownWaitOutcome => f.write_str("unknown broker wait outcome"), + Self::UnknownEventConsumeMode => f.write_str("unknown broker event consume mode"), Self::OffsetOverflow => f.write_str("broker wire offset overflow"), } } @@ -112,20 +126,27 @@ fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) -> Result<() fn encode_event_request(encoder: &mut Encoder, request: EventRequest) -> Result<(), WireError> { match request { - EventRequest::Create => { + EventRequest::Create(request) => { encoder.u8(EVENT_REQUEST_TAG_CREATE); + encoder.u64(request.initial_count); Ok(()) } - EventRequest::Wait { handle } => { + EventRequest::Wait(request) => { encoder.u8(EVENT_REQUEST_TAG_WAIT); - encoder.handle(handle); + encoder.handle(request.handle); Ok(()) } - EventRequest::Signal { handle } => { - encoder.u8(EVENT_REQUEST_TAG_SIGNAL); - encoder.handle(handle); + EventRequest::Add(request) => { + encoder.u8(EVENT_REQUEST_TAG_ADD); + encoder.handle(request.handle); + encoder.u64(request.value); Ok(()) } + EventRequest::Consume(request) => { + encoder.u8(EVENT_REQUEST_TAG_CONSUME); + encoder.handle(request.handle); + encode_event_consume_mode(encoder, request.mode) + } _ => Err(WireError::EncodeUnknownRequestTag), } } @@ -162,13 +183,15 @@ fn decode_core_request(decoder: &mut Decoder<'_>) -> Result, fn decode_event_request(decoder: &mut Decoder<'_>) -> Result, WireError> { let request = match decoder.u8()? { - EVENT_REQUEST_TAG_CREATE => EventRequest::Create, - EVENT_REQUEST_TAG_WAIT => EventRequest::Wait { - handle: decoder.handle()?, - }, - EVENT_REQUEST_TAG_SIGNAL => EventRequest::Signal { - handle: decoder.handle()?, - }, + 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()?, + decode_event_consume_mode(decoder)?, + )), _ => return Ok(None), }; @@ -219,19 +242,25 @@ fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) -> Result fn encode_event_response(encoder: &mut Encoder, response: EventResponse) -> Result<(), WireError> { match response { - EventResponse::Created { handle } => { + EventResponse::Create(response) => { encoder.u8(EVENT_RESPONSE_TAG_CREATED); - encoder.handle(handle); + encoder.handle(response.handle); Ok(()) } - EventResponse::Signaled { readiness } => { - encoder.u8(EVENT_RESPONSE_TAG_SIGNALED); - encoder.readiness(readiness); + EventResponse::Wait(response) => { + encoder.u8(EVENT_RESPONSE_TAG_WAITED); + encode_wait_outcome(encoder, response.outcome) + } + EventResponse::Add(response) => { + encoder.u8(EVENT_RESPONSE_TAG_ADDED); + encoder.readiness(response.readiness); Ok(()) } - EventResponse::Waited { outcome } => { - encoder.u8(EVENT_RESPONSE_TAG_WAITED); - encode_wait_outcome(encoder, outcome) + EventResponse::Consume(response) => { + encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); + encoder.u64(response.value); + encoder.readiness(response.readiness); + Ok(()) } _ => Err(WireError::EncodeUnknownResponseTag), } @@ -292,15 +321,17 @@ fn decode_core_response(decoder: &mut Decoder<'_>) -> Result) -> Result, WireError> { let response = match decoder.u8()? { - EVENT_RESPONSE_TAG_CREATED => EventResponse::Created { - handle: decoder.handle()?, - }, - EVENT_RESPONSE_TAG_SIGNALED => EventResponse::Signaled { - readiness: decoder.readiness()?, - }, - EVENT_RESPONSE_TAG_WAITED => EventResponse::Waited { - outcome: decode_wait_outcome(decoder)?, - }, + EVENT_RESPONSE_TAG_CREATED => { + EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) + } + EVENT_RESPONSE_TAG_WAITED => { + EventResponse::Wait(WaitEventResponse::new(decode_wait_outcome(decoder)?)) + } + EVENT_RESPONSE_TAG_ADDED => EventResponse::Add(AddEventResponse::new(decoder.readiness()?)), + EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( + decoder.u64()?, + decoder.readiness()?, + )), _ => return Ok(None), }; @@ -315,6 +346,31 @@ fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result Result<(), WireError> { + match mode { + EventConsumeMode::All => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ALL); + Ok(()) + } + EventConsumeMode::One => { + encoder.u8(EVENT_CONSUME_MODE_TAG_ONE); + Ok(()) + } + _ => Err(WireError::EncodeUnknownEventConsumeMode), + } +} + +fn decode_event_consume_mode(decoder: &mut Decoder<'_>) -> Result { + match decoder.u8()? { + EVENT_CONSUME_MODE_TAG_ALL => Ok(EventConsumeMode::All), + EVENT_CONSUME_MODE_TAG_ONE => Ok(EventConsumeMode::One), + _ => Err(WireError::UnknownEventConsumeMode), + } +} + #[derive(Default)] struct Encoder { bytes: Vec, @@ -431,9 +487,18 @@ mod tests { BrokerRequest::Negotiate { protocol_version: ProtocolVersion::new(1, 0), }, - event_request(EventRequest::Create), - event_request(EventRequest::Wait { handle }), - event_request(EventRequest::Signal { handle }), + 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 { @@ -454,17 +519,22 @@ mod tests { BrokerResponse::VersionMismatch { broker_protocol_version: ProtocolVersion::new(1, 0), }, - event_response(EventResponse::Created { handle }), - event_response(EventResponse::Signaled { - readiness: ReadinessState::new(false, 7), - }), - event_response(EventResponse::Waited { - outcome: WaitOutcome::Ready(ReadinessState::new(true, 8)), - }), - event_response(EventResponse::Waited { - outcome: WaitOutcome::WouldBlock(ReadinessState::new(false, 9)), - }), + event_response(EventResponse::Create(CreateEventResponse::new(handle))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, 8)), + ))), + event_response(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, 9)), + ))), + event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, 10), + ))), + event_response(EventResponse::Consume(ConsumeEventResponse::new( + 3, + ReadinessState::new(false, 11), + ))), BrokerResponse::Error(ErrorCode::PolicyDenied), + BrokerResponse::Error(ErrorCode::WouldBlock), BrokerResponse::Error(ErrorCode::Internal), ]; @@ -483,7 +553,10 @@ mod tests { Ok(ReceivedBrokerRequest::Unknown) ); assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); - let mut frame = encode_request(event_request(EventRequest::Create)).unwrap(); + let mut frame = encode_request(event_request(EventRequest::Create( + CreateEventRequest::new(0), + ))) + .unwrap(); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } @@ -495,7 +568,7 @@ mod tests { Ok(ReceivedBrokerResponse::Unknown) ); assert_eq!( - decode_response(&[1, 0, 2, 0xff]), + decode_response(&[1, 0, 1, 0xff]), Err(WireError::UnknownWaitOutcome) ); assert_eq!( @@ -505,7 +578,7 @@ mod tests { ))) ); - let mut invalid_bool = [1, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0]; + let mut invalid_bool = [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0]; assert_eq!( decode_response(&invalid_bool), Err(WireError::InvalidBoolean) @@ -521,11 +594,11 @@ mod tests { #[test] fn readiness_response_wire_shape_is_pinned() { assert_eq!( - encode_response(event_response(EventResponse::Signaled { - readiness: ReadinessState::new(true, 0x0102_0304_0506_0708) - })) + encode_response(event_response(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(true, 0x0102_0304_0506_0708) + )))) .unwrap(), - [1, 0, 1, 1, 8, 7, 6, 5, 4, 3, 2, 1] + [1, 0, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1] ); } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index e8b9e93a0..5c065a23b 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,21 +3,48 @@ use std::{ path::Path, - thread, + sync::{ + Condvar, Mutex, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, + thread::{self, JoinHandle, Thread}, time::{Duration, Instant}, }; +use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox_broker_client::BrokerClient; +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; use litebox_broker_unix_socket::UnixStreamClientControlChannel; +use litebox_shim_linux::{BrokerControl, BrokerControlError}; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const RETRY_DELAY: Duration = Duration::from_millis(20); +const PHASE_IDLE: u8 = 0; +const PHASE_RESERVED: u8 = 1; +const PHASE_REQUEST_READY: u8 = 2; +const PHASE_RESPONSE_READY: u8 = 3; +const PHASE_SHUTDOWN: u8 = 4; type Client = BrokerClient; pub(crate) struct BrokerConnection { - client: Option, + control: Arc, +} + +struct BrokerControlClient { + state: Arc, + worker: Mutex>>, +} + +struct BrokerControlState { + shutdown_requested: AtomicBool, + phase: AtomicU8, + request: Mutex>, + response: Mutex>>, + requester_wait: Mutex<()>, + requester_wakeup: Condvar, + worker_thread: Mutex>, } pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { @@ -28,14 +55,215 @@ pub(crate) fn connect(socket_path: Option<&Path>) -> Result Arc { + self.control.clone() + } + + pub(crate) fn shutdown(self) { + self.control.shutdown(); } } impl Drop for BrokerConnection { fn drop(&mut self) { - self.client.take(); + self.control.shutdown(); + } +} + +impl BrokerControlClient { + fn new(client: Client) -> Self { + let state = Arc::new(BrokerControlState { + shutdown_requested: AtomicBool::new(false), + phase: AtomicU8::new(PHASE_IDLE), + request: Mutex::new(None), + response: Mutex::new(None), + requester_wait: Mutex::new(()), + requester_wakeup: Condvar::new(), + worker_thread: Mutex::new(None), + }); + let worker_state = state.clone(); + let worker = thread::spawn(move || run_broker_control_worker(client, worker_state)); + *state + .worker_thread + .lock() + .expect("broker control worker-thread mutex poisoned") = Some(worker.thread().clone()); + Self { + state, + worker: Mutex::new(Some(worker)), + } + } + + fn shutdown(&self) { + let mut worker = self + .worker + .lock() + .expect("broker control worker mutex poisoned"); + if let Some(worker) = worker.take() { + self.state.request_shutdown(); + worker.join().expect("broker control worker panicked"); + } + } +} + +impl BrokerControl for BrokerControlClient { + fn request( + &self, + request: BrokerRequest, + ) -> core::result::Result { + self.state.submit(request) + } +} + +impl BrokerControlState { + fn submit( + &self, + request: BrokerRequest, + ) -> core::result::Result { + // Guest syscall handling must not perform host socket I/O on the + // rewritten thread, so this side publishes work to the dedicated worker. + loop { + if self.shutdown_requested.load(Ordering::Acquire) { + return Err(BrokerControlError); + } + match self.phase.load(Ordering::Acquire) { + PHASE_IDLE => { + if self + .phase + .compare_exchange( + PHASE_IDLE, + PHASE_RESERVED, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + if self.shutdown_requested.load(Ordering::Acquire) { + self.store_phase_and_notify(PHASE_IDLE); + return Err(BrokerControlError); + } + break; + } + } + PHASE_SHUTDOWN => return Err(BrokerControlError), + phase => self.wait_for_phase_change(phase, true), + } + } + + *self + .request + .lock() + .expect("broker control request mutex poisoned") = Some(request); + self.phase.store(PHASE_REQUEST_READY, Ordering::Release); + self.wake_worker(); + + loop { + match self.phase.load(Ordering::Acquire) { + PHASE_RESPONSE_READY => break, + PHASE_SHUTDOWN => return Err(BrokerControlError), + phase => self.wait_for_phase_change(phase, false), + } + } + + let response = self + .response + .lock() + .expect("broker control response mutex poisoned") + .take() + .expect("broker control worker did not publish a response"); + self.store_phase_and_notify(PHASE_IDLE); + response + } + + fn request_shutdown(&self) { + { + let _guard = self + .requester_wait + .lock() + .expect("broker control requester-wait mutex poisoned"); + self.shutdown_requested.store(true, Ordering::Release); + self.requester_wakeup.notify_all(); + } + loop { + match self.phase.load(Ordering::Acquire) { + PHASE_IDLE => { + if self + .phase + .compare_exchange( + PHASE_IDLE, + PHASE_SHUTDOWN, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + self.wake_worker(); + return; + } + } + PHASE_SHUTDOWN => return, + phase => self.wait_for_phase_change(phase, false), + } + } + } + + fn store_phase_and_notify(&self, phase: u8) { + let _guard = self + .requester_wait + .lock() + .expect("broker control requester-wait mutex poisoned"); + self.phase.store(phase, Ordering::Release); + self.requester_wakeup.notify_all(); + } + + fn wait_for_phase_change(&self, observed: u8, wake_on_shutdown: bool) { + let mut guard = self + .requester_wait + .lock() + .expect("broker control requester-wait mutex poisoned"); + while self.phase.load(Ordering::Acquire) == observed + && !(wake_on_shutdown && self.shutdown_requested.load(Ordering::Acquire)) + { + guard = self + .requester_wakeup + .wait(guard) + .expect("broker control requester-wait mutex poisoned"); + } + } + + fn wake_worker(&self) { + if let Some(worker) = self + .worker_thread + .lock() + .expect("broker control worker-thread mutex poisoned") + .as_ref() + { + worker.unpark(); + } + } +} + +fn run_broker_control_worker(mut client: Client, state: Arc) { + loop { + match state.phase.load(Ordering::Acquire) { + PHASE_REQUEST_READY => { + let request = state + .request + .lock() + .expect("broker control request mutex poisoned") + .take() + .expect("broker control request missing"); + let response = client + .active_raw_request(request) + .map_err(|_| BrokerControlError); + *state + .response + .lock() + .expect("broker control response mutex poisoned") = Some(response); + state.store_phase_and_notify(PHASE_RESPONSE_READY); + } + PHASE_SHUTDOWN => break, + _ => thread::park(), + } } } @@ -48,7 +276,7 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { .set_io_deadline(None) .context("failed to clear broker setup deadline")?; Ok(BrokerConnection { - client: Some(client), + control: Arc::new(BrokerControlClient::new(client)), }) } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index fdaef1e86..a5584cbe5 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -214,7 +214,10 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let mut shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + if let Some(broker_connection) = &broker_connection { + shim_builder = shim_builder.broker_control(broker_connection.control()); + } 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..7f21825dd --- /dev/null +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -0,0 +1,145 @@ +#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 writev_values(int fd, uint64_t first, uint64_t second) { + struct iovec iov[2] = { + {&first, sizeof(first)}, + {&second, sizeof(second)}, + }; + return writev(fd, iov, 2) == (ssize_t)(sizeof(first) + sizeof(second)) ? 0 : 1; +} + +static int readv_split_value(int fd, uint64_t expected) { + uint8_t bytes[sizeof(expected)] = {0}; + struct iovec iov[2] = { + {bytes, 4}, + {bytes + 4, 4}, + }; + uint64_t value = 0; + if (readv(fd, iov, 2) != (ssize_t)sizeof(value)) { + return 1; + } + memcpy(&value, bytes, sizeof(value)); + return value == expected ? 0 : 2; +} + +static int expect_einval_short_readv(int fd) { + uint32_t value = 0; + struct iovec iov = {&value, sizeof(value)}; + errno = 0; + if (readv(fd, &iov, 1) != -1) { + return 1; + } + return errno == EINVAL ? 0 : 2; +} + +static int expect_einval_split_writev(int fd) { + uint32_t low = 1; + uint32_t high = 0; + struct iovec iov[2] = { + {&low, sizeof(low)}, + {&high, sizeof(high)}, + }; + errno = 0; + if (writev(fd, iov, 2) != -1) { + return 1; + } + return errno == EINVAL ? 0 : 2; +} + +int main(void) { + int fd = eventfd(0, EFD_NONBLOCK); + if (fd < 0) { + return 10; + } + if (expect_eagain_read(fd) != 0) { + return 11; + } + if (write_value(fd, 3) != 0) { + return 12; + } + if (read_value(fd, 3) != 0) { + return 13; + } + if (writev_values(fd, 2, 5) != 0) { + return 14; + } + if (read_value(fd, 7) != 0) { + return 15; + } + if (write_value(fd, 9) != 0) { + return 16; + } + if (readv_split_value(fd, 9) != 0) { + return 17; + } + if (expect_einval_split_writev(fd) != 0) { + return 18; + } + if (write_value(fd, 11) != 0) { + return 19; + } + if (expect_einval_short_readv(fd) != 0) { + return 20; + } + if (read_value(fd, 11) != 0) { + return 21; + } + if (expect_eagain_read(fd) != 0) { + return 22; + } + uint64_t invalid = UINT64_MAX; + errno = 0; + if (write(fd, &invalid, sizeof(invalid)) != -1 || errno != EINVAL) { + return 23; + } + close(fd); + + int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); + if (semaphore_fd < 0) { + return 30; + } + if (write_value(semaphore_fd, 3) != 0) { + return 31; + } + if (read_value(semaphore_fd, 1) != 0) { + return 32; + } + if (read_value(semaphore_fd, 1) != 0) { + return 33; + } + if (read_value(semaphore_fd, 1) != 0) { + return 34; + } + if (expect_eagain_read(semaphore_fd) != 0) { + return 35; + } + close(semaphore_fd); + + return 0; +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 5d02a18f1..247362ddc 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -249,7 +249,7 @@ fn spawn_test_broker

(socket_path: &Path, policy: P) -> std::thread::JoinHandl where P: litebox_broker_core::PolicyEngine + Send + 'static, { - let _ = std::fs::remove_file(&socket_path); + let _ = std::fs::remove_file(socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); let server_socket_path = socket_path.to_path_buf(); @@ -313,7 +313,7 @@ fn test_broker_event_path_over_unix_socket() { client.wait_event(handle).expect("event wait failed"), WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) ); - let readiness = client.signal_event(handle).expect("event signal failed"); + let readiness = client.add_event(handle, 1).expect("event add failed"); assert_eq!(readiness, ReadinessState::new(true, 1)); assert_eq!( client.wait_event(handle).expect("event ready-wait failed"), @@ -325,6 +325,21 @@ fn test_broker_event_path_over_unix_socket() { let _ = std::fs::remove_file(socket_path); } +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +#[test] +fn test_broker_backed_eventfd_with_rewriter() { + let socket_path = unique_test_socket_path("broker-eventfd"); + let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::EventOnlyPolicy); + let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); + + Runner::new(&target, "broker_eventfd_rewriter") + .broker_socket(&socket_path) + .run(); + + broker_thread.join().expect("broker test server panicked"); + let _ = std::fs::remove_file(socket_path); +} + #[cfg(target_arch = "x86_64")] #[test] fn test_node_with_rewriter() { diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index 8d6e23154..b1f8f036e 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -8,6 +8,7 @@ arrayvec = { version = "0.7.6", default-features = false } bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] } bitflags = "2.9.0" litebox = { path = "../litebox/", version = "0.1.0" } +litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 09835c541..458e25451 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -61,9 +61,25 @@ pub(crate) type LinuxFS = litebox::fs::layered::FileSystem< litebox::fs::tar_ro::FileSystem, >, >; - pub(crate) type FileFd = litebox::fd::TypedFd; +/// Error returned by the runner-provided broker control path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct BrokerControlError; + +/// Local-core access to the negotiated broker control channel. +/// +/// The shim/local core owns Linux ABI translation and constructs broker protocol +/// requests. The runner owns endpoint selection and supplies the connected +/// transport behind this protocol-level boundary. +pub trait BrokerControl: Send + Sync { + /// Sends one active broker request and returns its response. + fn request( + &self, + request: litebox_broker_protocol::BrokerRequest, + ) -> core::result::Result; +} + /// A trait required for file systems to be used in the shim. pub trait ShimFS: litebox::fs::FileSystem + Send + Sync + 'static {} impl ShimFS for T {} @@ -153,6 +169,7 @@ impl LinuxShimEntrypoints { pub struct LinuxShimBuilder { platform: &'static Platform, litebox: LiteBox, + broker_control: Option>, } impl Default for LinuxShimBuilder { @@ -168,6 +185,7 @@ impl LinuxShimBuilder { Self { platform, litebox: LiteBox::new(platform), + broker_control: None, } } @@ -185,6 +203,13 @@ impl LinuxShimBuilder { default_fs(&self.litebox, in_mem_fs, tar_ro_fs) } + /// Installs a runner-provided broker control channel for broker-backed local-core objects. + #[must_use] + pub fn broker_control(mut self, broker_control: Arc) -> Self { + self.broker_control = Some(broker_control); + self + } + /// Build the shim. pub fn build(self) -> LinuxShim { let mut net = Network::new(&self.litebox); @@ -200,6 +225,7 @@ impl LinuxShimBuilder { litebox: self.litebox, unix_addr_table: litebox::sync::RwLock::new(syscalls::unix::UnixAddrTable::new()), elf_patch_cache: litebox::sync::Mutex::new(alloc::collections::BTreeMap::new()), + broker_control: self.broker_control, }); LinuxShim(global) } @@ -1104,6 +1130,8 @@ struct GlobalState { unix_addr_table: litebox::sync::RwLock>, /// Per-process collection of ELF patching state for runtime syscall rewriting. elf_patch_cache: litebox::sync::Mutex, + /// Optional broker control path installed by the runner. + broker_control: Option>, } struct Task { diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 1c18bc8ed..9f717d54b 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -3,8 +3,10 @@ //! Event file for notification +use alloc::sync::Arc; use core::sync::atomic::AtomicU32; +use crate::BrokerControl; use litebox::{ event::{ Events, IOPollable, @@ -17,6 +19,11 @@ use litebox::{ platform::TimeProvider, sync::RawSyncPrimitivesProvider, }; +use litebox_broker_protocol::{ + AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, CoreRequest, CoreResponse, + CreateEventRequest, ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, + WaitEventRequest, WaitOutcome, +}; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::Platform; @@ -27,20 +34,66 @@ impl FdEnabledSubsystem for EventfdSubsystem { impl FdEnabledSubsystemEntry for EventFile {} pub(crate) struct EventFile { - counter: litebox::sync::Mutex, + backend: EventBackend, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, pollee: Pollee, } +enum EventBackend { + Local { + counter: litebox::sync::Mutex, + }, + Broker { + /// Broker-backed eventfds are currently created only for `EFD_NONBLOCK`; + /// blocking broker wait/notification plumbing is not implemented yet. + broker: Arc, + handle: ObjectHandle, + }, +} + impl EventFile { pub(crate) fn new(count: u64, flags: EfdFlags) -> Self { + Self::new_with_backend( + EventBackend::Local { + counter: litebox::sync::Mutex::new(count), + }, + flags, + ) + } + + pub(crate) fn new_broker( + count: u64, + flags: EfdFlags, + broker: Arc, + ) -> Result { + if !flags.contains(EfdFlags::NONBLOCK) { + return Err(Errno::EINVAL); + } + let response = broker_request( + &broker, + EventRequest::Create(CreateEventRequest::new(count)), + )?; + let BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) = response + else { + return Err(response_to_errno(response)); + }; + Ok(Self::new_with_backend( + EventBackend::Broker { + broker, + handle: response.handle, + }, + flags, + )) + } + + fn new_with_backend(backend: EventBackend, flags: EfdFlags) -> Self { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); Self { - counter: litebox::sync::Mutex::new(count), + backend, status: AtomicU32::new(status.bits()), semaphore: flags.contains(EfdFlags::SEMAPHORE), pollee: Pollee::new(), @@ -48,20 +101,47 @@ impl EventFile { } fn try_read(&self) -> Result> { - let mut counter = self.counter.lock(); - if *counter == 0 { - return Err(TryOpError::TryAgain); - } + match &self.backend { + EventBackend::Local { counter } => { + let mut counter = counter.lock(); + if *counter == 0 { + return Err(TryOpError::TryAgain); + } - let res = if self.semaphore { 1 } else { *counter }; - *counter -= res; + let res = if self.semaphore { 1 } else { *counter }; + *counter -= res; - drop(counter); - self.pollee.notify_observers(Events::OUT); - Ok(res) + drop(counter); + self.pollee.notify_observers(Events::OUT); + Ok(res) + } + EventBackend::Broker { broker, handle } => { + let mode = if self.semaphore { + EventConsumeMode::One + } else { + EventConsumeMode::All + }; + let response = broker_request( + broker, + EventRequest::Consume(ConsumeEventRequest::new(*handle, mode)), + ) + .map_err(TryOpError::Other)?; + match response { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { + self.pollee.notify_observers(Events::OUT); + Ok(response.value) + } + BrokerResponse::Error(ErrorCode::WouldBlock) => Err(TryOpError::TryAgain), + response => Err(TryOpError::Other(response_to_errno(response))), + } + } + } } pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { + if matches!(&self.backend, EventBackend::Broker { .. }) { + return self.try_read().map_err(Errno::from); + } self.pollee .wait( cx, @@ -73,22 +153,48 @@ impl EventFile { } 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); - } + if value == u64::MAX { + return Err(TryOpError::Other(Errno::EINVAL)); } - Err(TryOpError::TryAgain) + match &self.backend { + EventBackend::Local { counter } => { + let mut counter = 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); + } + } + + Err(TryOpError::TryAgain) + } + EventBackend::Broker { broker, handle } => { + let response = broker_request( + broker, + EventRequest::Add(AddEventRequest::new(*handle, value)), + ) + .map_err(TryOpError::Other)?; + match response { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(_))) => { + self.pollee.notify_observers(Events::IN); + Ok(8) + } + BrokerResponse::Error(ErrorCode::WouldBlock) => Err(TryOpError::TryAgain), + response => Err(TryOpError::Other(response_to_errno(response))), + } + } + } } pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { + if matches!(&self.backend, EventBackend::Broker { .. }) { + return self.try_write(value).map_err(Errno::from); + } self.pollee .wait( cx, @@ -100,23 +206,52 @@ impl EventFile { } super::common_functions_for_file_status!(); + + pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { + let new_status = (self.get_status() & mask.complement()) | (requested & mask); + if matches!(&self.backend, EventBackend::Broker { .. }) + && !new_status.contains(OFlags::NONBLOCK) + { + return Err(Errno::EINVAL); + } + self.set_status(requested & mask, true); + self.set_status(requested.complement() & mask, false); + Ok(()) + } } 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; - } + match &self.backend { + EventBackend::Local { counter } => { + let counter = 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; + } - events + events + } + EventBackend::Broker { broker, handle } => { + // The broker protocol currently exposes read readiness only. Keep + // write readiness optimistic and surface counter-limit failures + // from write as EAGAIN until broker write-readiness plumbing exists. + let mut events = Events::OUT; + if let Ok(BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response)))) = + broker_request(broker, EventRequest::Wait(WaitEventRequest::new(*handle))) + && matches!(response.outcome, WaitOutcome::Ready(_)) + { + events |= Events::IN; + } + events + } + } } fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { @@ -124,12 +259,46 @@ impl IOPollable for EventFil } } +fn broker_request( + broker: &Arc, + request: EventRequest, +) -> Result { + broker + .request(BrokerRequest::Core(CoreRequest::Event(request))) + .map_err(|_| Errno::EIO) +} + +fn response_to_errno(response: BrokerResponse) -> Errno { + match response { + BrokerResponse::Error(error) => error_to_errno(error), + _ => Errno::EIO, + } +} + +fn error_to_errno(error: ErrorCode) -> Errno { + match error { + ErrorCode::InvalidRights | ErrorCode::WrongObjectType | ErrorCode::StaleHandle => { + Errno::EINVAL + } + ErrorCode::WouldBlock | ErrorCode::ResourceExhausted => Errno::EAGAIN, + _ => Errno::EIO, + } +} + #[cfg(test)] mod tests { + use alloc::sync::Arc; use litebox::event::wait::WaitState; + use litebox::fs::OFlags; + use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, + EventRequest, EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, + }; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::platform; + use crate::{BrokerControl, BrokerControlError}; + extern crate std; #[test] @@ -241,4 +410,38 @@ mod tests { // block until the second write read(&eventfd, u64::MAX - 1); } + + #[test] + fn broker_eventfd_rejects_clearing_nonblock() { + let _task = crate::syscalls::tests::init_platform(None); + let eventfd: super::EventFile = + super::EventFile::new_broker(0, EfdFlags::NONBLOCK, Arc::new(CreateOnlyBroker)) + .unwrap(); + + assert_eq!( + eventfd.set_status_flags(OFlags::empty(), OFlags::NONBLOCK), + Err(Errno::EINVAL) + ); + assert!(eventfd.get_status().contains(OFlags::NONBLOCK)); + } + + struct CreateOnlyBroker; + + impl BrokerControl for CreateOnlyBroker { + fn request( + &self, + request: BrokerRequest, + ) -> core::result::Result { + assert!(matches!( + request, + BrokerRequest::Core(CoreRequest::Event(EventRequest::Create(_))) + )); + Ok(BrokerResponse::Core(CoreResponse::Event( + EventResponse::Create(CreateEventResponse::new(ObjectHandle::new( + ObjectReferenceId::new(1), + ObjectReferenceGeneration::new(1), + ))), + ))) + } + } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index a857477f4..c907485e0 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -894,6 +894,38 @@ impl Task { self.check_raw_fd_exists(fd)?; check_iovcnt(iovcnt)?; let iovs: &[IoReadVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; + let raw_fd = usize::try_from(fd).map_err(|_| Errno::EBADF)?; + { + let files = self.files.borrow(); + if let Some(size) = files + .run_on_raw_fd( + raw_fd, + |_fd| Ok(None), + |_fd| Ok(None), + |_fd| Ok(None), + |fd| { + validate_eventfd_iovec_len(iovs.iter().map(|iov| iov.iov_len))?; + let handle = self + .global + .litebox + .descriptor_table() + .entry_handle(fd) + .ok_or(Errno::EBADF)?; + handle.with_entry(|file| { + let value = file.read(&self.wait_cx())?; + copy_eventfd_value_to_iovec(iovs, value)?; + Ok(Some(8)) + }) + }, + |_fd| Ok(None), + |_fd| Ok(None), + ) + .flatten()? + { + return Ok(size); + } + } + let mut kernel_buffer = vec![0u8; PAGE_SIZE]; // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with @@ -945,6 +977,87 @@ fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno Ok(()) } +fn validate_eventfd_iovec_len(iov_lens: impl IntoIterator) -> Result<(), Errno> { + let mut total_len = 0usize; + for iov_len in iov_lens { + total_len = total_len.checked_add(iov_len).ok_or(Errno::EINVAL)?; + if total_len > SSIZE_MAX { + return Err(Errno::EINVAL); + } + } + if total_len < size_of::() { + return Err(Errno::EINVAL); + } + Ok(()) +} + +fn copy_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Result<(), Errno> { + validate_eventfd_iovec_len(iovs.iter().map(|iov| iov.iov_len))?; + + let bytes = value.to_ne_bytes(); + let mut copied = 0; + for iov in iovs { + if iov.iov_len == 0 { + continue; + } + let size = (size_of::() - copied).min(iov.iov_len); + iov.iov_base + .copy_from_slice(0, &bytes[copied..copied + size]) + .ok_or(Errno::EFAULT)?; + copied += size; + if copied == size_of::() { + return Ok(()); + } + } + Err(Errno::EINVAL) +} + +fn write_eventfd_iovec( + iovs: &[IoWriteVec>], + mut write_value: F, +) -> Result +where + F: FnMut(u64) -> Result, +{ + check_iov_lens(iovs.iter().map(|iov| iov.iov_len))?; + + let Some(first_non_empty) = iovs.iter().position(|iov| iov.iov_len != 0) else { + return Ok(0); + }; + if first_non_empty != 0 { + return Err(Errno::EINVAL); + } + + let bail = |total: usize, e: Errno| if total > 0 { Ok(total) } else { Err(e) }; + let mut total_written = 0; + for iov in iovs { + if iov.iov_len == 0 { + continue; + } + if iov.iov_len != size_of::() { + return bail(total_written, Errno::EINVAL); + } + let Some(slice) = iov.iov_base.to_owned_slice(iov.iov_len) else { + return bail(total_written, Errno::EFAULT); + }; + let value = u64::from_ne_bytes( + slice + .as_ref() + .try_into() + .expect("eventfd writev validated an eight-byte iovec"), + ); + let size = match write_value(value) { + Ok(size) => size, + Err(err) => return bail(total_written, err), + }; + total_written += size; + if size < iov.iov_len { + break; + } + } + Ok(total_written) +} + /// Drain reads into a sequence of user iovecs. fn read_from_iovec( iovs: &[IoReadVec

], @@ -1052,10 +1165,50 @@ impl Task { check_iovcnt(iovcnt)?; let iovs: &[IoWriteVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; + let raw_fd = usize::try_from(fd).map_err(|_| Errno::EBADF)?; // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with // output from writes in other processes - write_to_iovec(iovs, |buf, _total| self.sys_write(fd, buf, None)) + let files = self.files.borrow(); + let res = files + .run_on_raw_fd( + raw_fd, + |fd| { + write_to_iovec(iovs, |buf, _total| { + files.fs.write(fd, buf, None).map_err(Errno::from) + }) + }, + |fd| { + write_to_iovec(iovs, |buf, _total| { + self.global.sendto( + &self.wait_cx(), + fd, + buf, + litebox_common_linux::SendFlags::empty(), + None, + ) + }) + }, + |_fd| todo!("pipes"), + |fd| { + let handle = self + .global + .litebox + .descriptor_table() + .entry_handle(fd) + .ok_or(Errno::EBADF)?; + write_eventfd_iovec(iovs, |value| { + handle.with_entry(|file| file.write(&self.wait_cx(), value)) + }) + }, + |_fd| Err(Errno::EINVAL), + |_fd| todo!("unix"), + ) + .flatten(); + if let Err(Errno::EPIPE) = res { + self.send_signal(Signal::SIGPIPE, signal::siginfo_kill(Signal::SIGPIPE)); + } + res } fn validate_access_mode(mode: &AccessFlags) -> Result<(), Errno> { @@ -1567,8 +1720,19 @@ impl Task { .set_linux_pipe_status_flags(fd, flags, setfl_mask) }, |fd| { - toggle_flags!(fd); - Ok(()) + let handle = self + .global + .litebox + .descriptor_table() + .entry_handle(fd) + .ok_or(Errno::EBADF)?; + handle.with_entry(|file| { + let diff = (file.get_status() & setfl_mask) ^ flags; + if diff.intersects(OFlags::APPEND | OFlags::DIRECT | OFlags::NOATIME) { + log_unsupported!("unsupported flags"); + } + file.set_status_flags(flags, setfl_mask) + }) }, |_fd| todo!("epoll"), |fd| { @@ -1734,7 +1898,19 @@ impl Task { return Err(Errno::EINVAL); } - let eventfd = super::eventfd::EventFile::new(u64::from(initval), flags); + let eventfd = if flags.contains(EfdFlags::NONBLOCK) { + if let Some(broker_control) = &self.global.broker_control { + super::eventfd::EventFile::new_broker( + u64::from(initval), + flags, + broker_control.clone(), + )? + } else { + super::eventfd::EventFile::new(u64::from(initval), flags) + } + } else { + super::eventfd::EventFile::new(u64::from(initval), flags) + }; let mut dt = self.global.litebox.descriptor_table_mut(); let typed = dt.insert::(eventfd); if flags.contains(EfdFlags::CLOEXEC) { @@ -1863,10 +2039,14 @@ impl Task { .descriptor_table() .entry_handle(fd) .ok_or(Errno::EBADF)?; + let requested = if val != 0 { + OFlags::NONBLOCK + } else { + OFlags::empty() + }; handle.with_entry(|file| { - file.set_status(OFlags::NONBLOCK, val != 0); - }); - Ok(()) + file.set_status_flags(requested, OFlags::NONBLOCK) + }) }, |fd| { let handle = self @@ -2671,6 +2851,127 @@ mod tests { assert_eq!(result, Ok(4)); assert_eq!(calls.get(), 2); assert_eq!(&first, b"xxxx"); + assert_eq!(&second, &[0u8; 4]); + } + + #[test] + fn eventfd_writev_rejects_split_value() { + let task = crate::syscalls::tests::init_platform(None); + let fd = task + .sys_eventfd2(0, EfdFlags::NONBLOCK) + .expect("eventfd2 failed"); + let fd = i32::try_from(fd).unwrap(); + let value = 0x0102_0304_0506_0708u64; + let bytes = value.to_ne_bytes(); + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(bytes.as_ptr()), + iov_len: 3, + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(bytes[3..].as_ptr()), + iov_len: 5, + }, + ]; + + assert_eq!( + task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Err(Errno::EINVAL) + ); + + let mut output = [0u8; 8]; + assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); + } + + #[test] + fn eventfd_writev_rejects_leading_zero_before_value() { + let task = crate::syscalls::tests::init_platform(None); + let fd = task + .sys_eventfd2(5, EfdFlags::NONBLOCK) + .expect("eventfd2 failed"); + let fd = i32::try_from(fd).unwrap(); + let value = 7u64.to_ne_bytes(); + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(value.as_ptr()), + iov_len: 0, + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(value.as_ptr()), + iov_len: value.len(), + }, + ]; + + assert_eq!( + task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Err(Errno::EINVAL) + ); + + let mut output = [0u8; 8]; + assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); + assert_eq!(u64::from_ne_bytes(output), 5); + assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); + } + + #[test] + fn eventfd_writev_all_zero_iovecs_is_noop() { + let task = crate::syscalls::tests::init_platform(None); + let fd = task + .sys_eventfd2(5, EfdFlags::NONBLOCK) + .expect("eventfd2 failed"); + let fd = i32::try_from(fd).unwrap(); + let value = 7u64.to_ne_bytes(); + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(value.as_ptr()), + iov_len: 0, + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(value.as_ptr()), + iov_len: 0, + }, + ]; + + assert_eq!( + task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Ok(0) + ); + + let mut output = [0u8; 8]; + assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); + assert_eq!(u64::from_ne_bytes(output), 5); + assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); + } + + #[test] + fn eventfd_writev_writes_each_full_value() { + let task = crate::syscalls::tests::init_platform(None); + let fd = task + .sys_eventfd2(0, EfdFlags::NONBLOCK) + .expect("eventfd2 failed"); + let fd = i32::try_from(fd).unwrap(); + let first = 7u64.to_ne_bytes(); + let second = 11u64.to_ne_bytes(); + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(first.as_ptr()), + iov_len: first.len(), + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(second.as_ptr()), + iov_len: second.len(), + }, + ]; + + assert_eq!( + task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Ok(16) + ); + + let mut output = [0u8; 8]; + assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); + assert_eq!(u64::from_ne_bytes(output), 18); + assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); } #[test] From b68d9bb55ede4101f1f1b449d42fc80e4ee99fd3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 14:01:07 -0700 Subject: [PATCH 22/66] Move broker boundary into local core Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 2 +- docs/broker-design.md | 8 +- docs/impl-plan.md | 2 +- litebox/Cargo.toml | 1 + litebox/src/broker.rs | 148 +++++++ litebox/src/lib.rs | 1 + litebox/src/litebox.rs | 20 + litebox_broker_client/Cargo.toml | 3 + litebox_broker_client/src/lib.rs | 6 +- litebox_broker_client/src/worker.rs | 404 ++++++++++++++++++++ litebox_broker_core/src/lib.rs | 3 +- litebox_broker_core/src/object.rs | 50 ++- litebox_broker_core/src/types.rs | 50 --- litebox_broker_server/src/server.rs | 15 +- litebox_runner_linux_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/src/broker.rs | 213 +---------- litebox_runner_linux_userland/src/lib.rs | 6 +- litebox_shim_linux/Cargo.toml | 1 - litebox_shim_linux/src/lib.rs | 31 +- litebox_shim_linux/src/syscalls/eventfd.rs | 147 ++----- litebox_shim_linux/src/syscalls/file.rs | 20 +- 21 files changed, 702 insertions(+), 431 deletions(-) create mode 100644 litebox/src/broker.rs create mode 100644 litebox_broker_client/src/worker.rs delete mode 100644 litebox_broker_core/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index bffa2cc19..2ef44865a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1460,6 +1460,7 @@ dependencies = [ "buddy_system_allocator", "either", "hashbrown", + "litebox_broker_protocol", "litebox_util_log", "rangemap", "ringbuf", @@ -1795,7 +1796,6 @@ dependencies = [ "bitvec", "libc", "litebox", - "litebox_broker_protocol", "litebox_common_linux", "litebox_platform_multiplex", "litebox_syscall_rewriter", diff --git a/docs/broker-design.md b/docs/broker-design.md index 8df62001c..01c2ab816 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -85,7 +85,7 @@ Because UserLiteBox always runs in user mode, it can use Rust `std` heavily in d ### BrokerClient adapter -Thin in-process adapter used by UserLiteBox and shim-specific user clients to call the broker authority interface. +Thin in-process adapter used by UserLiteBox/local-core code to call the broker authority interface. BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the control channel supplied by UserLiteBox's host-support layer. @@ -139,10 +139,10 @@ The broker architecture should use crate names that make the authority boundary | `litebox_broker_server` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | | `litebox_broker_userland` | Hosted `std` broker executable used by the userland POC. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket channel implementation together. | -| `litebox_broker_client` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/shims/runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | +| `litebox_broker_client` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | | `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | -`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and `litebox_user` decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. +`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. @@ -815,7 +815,7 @@ Then proceed incrementally: 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. 4. Create `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. 5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. -6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, and signal. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. +6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 805673b7b..3a5cb3d6e 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -95,7 +95,7 @@ Initial scope: Exit criteria: -- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket` and uses `litebox_broker_client` directly; routing through UserLiteBox remains part of Phase 3. +- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, and installs broker control on the `LiteBox` local core before starting the guest. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 44c0f35c7..945b6f447 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -20,6 +20,7 @@ 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_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.rs b/litebox/src/broker.rs new file mode 100644 index 000000000..7afa6cca3 --- /dev/null +++ b/litebox/src/broker.rs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +use litebox_broker_protocol::{ + AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, CoreRequest, CoreResponse, + CreateEventRequest, ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, + WaitEventRequest, WaitOutcome, +}; + +/// Error returned by the deployment-provided broker control path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct 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 broker request and returns its response. + fn request( + &self, + request: BrokerRequest, + ) -> core::result::Result; +} + +/// Error returned by broker-backed local-core objects. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum BrokerObjectError { + /// The broker control path failed. + Control, + /// The broker rejected the object handle, type, or rights. + InvalidObject, + /// The operation would block. + WouldBlock, + /// The broker object cannot accept more state. + ResourceExhausted, + /// The broker returned an unexpected response for the request. + UnexpectedResponse, + /// The broker returned an error not represented by this local-core object API. + Internal, +} + +/// How a broker event consume operation should remove readiness credits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BrokerEventConsumeMode { + /// Consume all currently available credits. + All, + /// Consume one credit. + One, +} + +impl BrokerEventConsumeMode { + const fn to_protocol(self) -> EventConsumeMode { + match self { + Self::All => EventConsumeMode::All, + Self::One => EventConsumeMode::One, + } + } +} + +/// Broker-backed event object owned by the local core. +#[derive(Clone)] +pub struct BrokerEvent { + broker: Arc, + handle: ObjectHandle, +} + +impl BrokerEvent { + pub(crate) fn create( + broker: Arc, + initial_count: u64, + ) -> Result { + let response = request_event( + &broker, + EventRequest::Create(CreateEventRequest::new(initial_count)), + )?; + let EventResponse::Create(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(Self { + broker, + handle: response.handle, + }) + } + + /// Consumes readiness credits from the event. + pub fn consume(&self, mode: BrokerEventConsumeMode) -> Result { + let response = self.request(EventRequest::Consume(ConsumeEventRequest::new( + self.handle, + mode.to_protocol(), + )))?; + let EventResponse::Consume(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(response.value) + } + + /// Adds readiness credits to the event. + pub fn add(&self, value: u64) -> Result<(), BrokerObjectError> { + let response = self.request(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; + let EventResponse::Add(_) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(()) + } + + /// Returns whether an event wait would complete now. + pub fn is_read_ready(&self) -> Result { + let response = self.request(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; + let EventResponse::Wait(response) = response else { + return Err(BrokerObjectError::UnexpectedResponse); + }; + Ok(matches!(response.outcome, WaitOutcome::Ready(_))) + } + + fn request(&self, request: EventRequest) -> Result { + request_event(&self.broker, request) + } +} + +fn request_event( + broker: &Arc, + request: EventRequest, +) -> Result { + let response = broker + .request(BrokerRequest::Core(CoreRequest::Event(request))) + .map_err(|_| BrokerObjectError::Control)?; + match response { + BrokerResponse::Core(CoreResponse::Event(response)) => Ok(response), + BrokerResponse::Error(error) => Err(error_to_object_error(error)), + _ => Err(BrokerObjectError::UnexpectedResponse), + } +} + +const fn error_to_object_error(error: ErrorCode) -> BrokerObjectError { + match error { + ErrorCode::InvalidRights | ErrorCode::WrongObjectType | ErrorCode::StaleHandle => { + BrokerObjectError::InvalidObject + } + ErrorCode::WouldBlock => BrokerObjectError::WouldBlock, + ErrorCode::ResourceExhausted => BrokerObjectError::ResourceExhausted, + _ => BrokerObjectError::Internal, + } +} diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index f3d80997a..315e9b4c3 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -16,6 +16,7 @@ extern crate alloc; +pub mod broker; pub mod event; pub mod fd; pub mod fs; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 2fb209c22..bd94f44cd 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,6 +6,7 @@ use alloc::sync::Arc; use crate::{ + broker::{BrokerControl, BrokerEvent, BrokerObjectError}, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -65,6 +66,7 @@ impl LiteBox { crate::sync::lock_tracing::LockTracker::init(platform); let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); + let broker_control = RwLock::new(None); litebox_util_log::trace!("LiteBox instance initialized"); @@ -72,6 +74,7 @@ impl LiteBox { x: Arc::new(LiteBoxX { platform, descriptors, + broker_control, }), } } @@ -106,10 +109,27 @@ impl LiteBox { ) -> impl core::ops::DerefMut> + use<'_, Platform> { self.x.descriptors.write() } + + /// Installs broker control for broker-backed local-core objects. + pub fn set_broker_control(&self, broker_control: Arc) { + *self.x.broker_control.write() = Some(broker_control); + } + + /// Creates a broker-backed event object when broker control is installed. + pub fn create_broker_event( + &self, + initial_count: u64, + ) -> Result, BrokerObjectError> { + let Some(broker) = self.x.broker_control.read().clone() else { + return Ok(None); + }; + BrokerEvent::create(broker, initial_count).map(Some) + } } /// The actual body of [`LiteBox`], containing any components that might be shared. pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, + broker_control: RwLock>>, } diff --git a/litebox_broker_client/Cargo.toml b/litebox_broker_client/Cargo.toml index 7fe967476..9d7a90a0b 100644 --- a/litebox_broker_client/Cargo.toml +++ b/litebox_broker_client/Cargo.toml @@ -3,6 +3,9 @@ name = "litebox_broker_client" version = "0.1.0" edition = "2024" +[features] +std = [] + [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index b34fc3754..0e354a3e6 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -9,18 +9,22 @@ #![no_std] -#[cfg(test)] +#[cfg(any(feature = "std", test))] extern crate std; mod error; mod event; mod negotiate; +#[cfg(feature = "std")] +mod worker; use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, ReceivedBrokerResponse, }; pub use error::{ClientError, Result}; +#[cfg(feature = "std")] +pub use worker::{BrokerClientWorker, BrokerClientWorkerError}; /// Protocol version this client implementation requests by default. pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); diff --git a/litebox_broker_client/src/worker.rs b/litebox_broker_client/src/worker.rs new file mode 100644 index 000000000..02e4aac1b --- /dev/null +++ b/litebox_broker_client/src/worker.rs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use core::fmt; +use std::{ + sync::{ + Arc, Condvar, Mutex, + atomic::{AtomicBool, AtomicU8, Ordering}, + }, + thread::{self, JoinHandle, Thread}, +}; + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ClientControlChannel}; + +use crate::{BrokerClient, ClientError}; + +const PHASE_IDLE: u8 = 0; +const PHASE_RESERVED: u8 = 1; +const PHASE_REQUEST_READY: u8 = 2; +const PHASE_RESPONSE_READY: u8 = 3; +const PHASE_SHUTDOWN: u8 = 4; + +/// Error returned by [`BrokerClientWorker`]. +#[derive(Debug)] +#[non_exhaustive] +pub enum BrokerClientWorkerError { + /// The wrapped broker client returned an error. + Client(ClientError), + /// The worker is shutting down or has already shut down. + Shutdown, +} + +impl fmt::Display for BrokerClientWorkerError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Client(error) => write!(f, "broker client worker request failed: {error}"), + Self::Shutdown => f.write_str("broker client worker is shut down"), + } + } +} + +impl core::error::Error for BrokerClientWorkerError +where + E: core::error::Error + 'static, +{ + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + match self { + Self::Client(error) => Some(error), + Self::Shutdown => None, + } + } +} + +type WorkerResult = core::result::Result>; + +/// Dedicated worker for broker clients that must not run channel I/O on the caller thread. +/// +/// The worker owns the [`BrokerClient`] and performs blocking channel operations +/// on a dedicated thread. Callers submit one raw broker request at a time and +/// block on an in-process condition variable until the worker publishes the +/// response. This preserves the serial control-channel contract while keeping +/// deployment-specific threads, such as rewritten guest syscall threads, away +/// from host IPC syscalls. +pub struct BrokerClientWorker +where + T: ClientControlChannel + Send + 'static, + T::Error: Send + 'static, +{ + state: Arc>, + worker: Mutex>>, +} + +struct BrokerClientWorkerState { + shutdown_requested: AtomicBool, + phase: AtomicU8, + request: Mutex>, + response: Mutex>>, + requester_wait: Mutex<()>, + requester_wakeup: Condvar, + worker_thread: Mutex>, +} + +impl BrokerClientWorker +where + T: ClientControlChannel + Send + 'static, + T::Error: Send + 'static, +{ + /// Starts a worker for an already-negotiated broker client. + /// + /// # Panics + /// + /// Panics if the worker-thread bookkeeping mutex is poisoned while the + /// worker is being started. + pub fn new(client: BrokerClient) -> Self { + let state = Arc::new(BrokerClientWorkerState { + shutdown_requested: AtomicBool::new(false), + phase: AtomicU8::new(PHASE_IDLE), + request: Mutex::new(None), + response: Mutex::new(None), + requester_wait: Mutex::new(()), + requester_wakeup: Condvar::new(), + worker_thread: Mutex::new(None), + }); + let worker_state = state.clone(); + let worker = thread::spawn(move || run_broker_client_worker(client, worker_state)); + *state + .worker_thread + .lock() + .expect("broker client worker-thread mutex poisoned") = Some(worker.thread().clone()); + + Self { + state, + worker: Mutex::new(Some(worker)), + } + } + + /// Sends one request on the active broker client and returns the raw protocol response. + pub fn active_raw_request( + &self, + request: BrokerRequest, + ) -> core::result::Result> { + self.state.submit(request) + } + + /// Shuts down the worker and waits for its thread to exit. + /// + /// # Panics + /// + /// Panics if the worker bookkeeping mutex is poisoned or if the worker + /// thread panicked before shutdown completed. + pub fn shutdown(&self) { + let mut worker = self + .worker + .lock() + .expect("broker client worker mutex poisoned"); + if let Some(worker) = worker.take() { + self.state.request_shutdown(); + worker.join().expect("broker client worker panicked"); + } + } +} + +impl Drop for BrokerClientWorker +where + T: ClientControlChannel + Send + 'static, + T::Error: Send + 'static, +{ + fn drop(&mut self) { + self.shutdown(); + } +} + +impl BrokerClientWorkerState { + fn submit(&self, request: BrokerRequest) -> WorkerResult { + loop { + if self.shutdown_requested.load(Ordering::Acquire) { + return Err(BrokerClientWorkerError::Shutdown); + } + match self.phase.load(Ordering::Acquire) { + PHASE_IDLE => { + if self + .phase + .compare_exchange( + PHASE_IDLE, + PHASE_RESERVED, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + if self.shutdown_requested.load(Ordering::Acquire) { + self.store_phase_and_notify(PHASE_IDLE); + return Err(BrokerClientWorkerError::Shutdown); + } + break; + } + } + PHASE_SHUTDOWN => return Err(BrokerClientWorkerError::Shutdown), + phase => self.wait_for_phase_change(phase, true), + } + } + + *self + .request + .lock() + .expect("broker client worker request mutex poisoned") = Some(request); + self.phase.store(PHASE_REQUEST_READY, Ordering::Release); + self.wake_worker(); + + loop { + match self.phase.load(Ordering::Acquire) { + PHASE_RESPONSE_READY => break, + PHASE_SHUTDOWN => return Err(BrokerClientWorkerError::Shutdown), + phase => self.wait_for_phase_change(phase, false), + } + } + + let response = self + .response + .lock() + .expect("broker client worker response mutex poisoned") + .take() + .expect("broker client worker did not publish a response"); + self.store_phase_and_notify(PHASE_IDLE); + response + } + + fn request_shutdown(&self) { + { + let _guard = self + .requester_wait + .lock() + .expect("broker client worker requester-wait mutex poisoned"); + self.shutdown_requested.store(true, Ordering::Release); + self.requester_wakeup.notify_all(); + } + loop { + match self.phase.load(Ordering::Acquire) { + PHASE_IDLE => { + if self + .phase + .compare_exchange( + PHASE_IDLE, + PHASE_SHUTDOWN, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + self.wake_worker(); + return; + } + } + PHASE_SHUTDOWN => return, + phase => self.wait_for_phase_change(phase, false), + } + } + } + + fn store_phase_and_notify(&self, phase: u8) { + let _guard = self + .requester_wait + .lock() + .expect("broker client worker requester-wait mutex poisoned"); + self.phase.store(phase, Ordering::Release); + self.requester_wakeup.notify_all(); + } + + fn wait_for_phase_change(&self, observed: u8, wake_on_shutdown: bool) { + let mut guard = self + .requester_wait + .lock() + .expect("broker client worker requester-wait mutex poisoned"); + while self.phase.load(Ordering::Acquire) == observed + && !(wake_on_shutdown && self.shutdown_requested.load(Ordering::Acquire)) + { + guard = self + .requester_wakeup + .wait(guard) + .expect("broker client worker requester-wait mutex poisoned"); + } + } + + fn wake_worker(&self) { + if let Some(worker) = self + .worker_thread + .lock() + .expect("broker client worker-thread mutex poisoned") + .as_ref() + { + worker.unpark(); + } + } +} + +fn run_broker_client_worker( + mut client: BrokerClient, + state: Arc>, +) where + T: ClientControlChannel, +{ + loop { + match state.phase.load(Ordering::Acquire) { + PHASE_REQUEST_READY => { + let request = state + .request + .lock() + .expect("broker client worker request mutex poisoned") + .take() + .expect("broker client worker request missing"); + let response = client + .active_raw_request(request) + .map_err(BrokerClientWorkerError::Client); + *state + .response + .lock() + .expect("broker client worker response mutex poisoned") = Some(response); + state.store_phase_and_notify(PHASE_RESPONSE_READY); + } + PHASE_SHUTDOWN => break, + _ => thread::park(), + } + } +} + +#[cfg(test)] +mod tests { + use core::convert::Infallible; + use std::{ + collections::VecDeque, + sync::{Arc, Mutex}, + vec::Vec, + }; + + use litebox_broker_protocol::{ + BrokerRequest, BrokerResponse, ClientControlChannel, ErrorCode, ReceivedBrokerResponse, + }; + + use super::*; + use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION}; + + #[test] + fn worker_returns_raw_protocol_errors() { + let sent = Arc::new(Mutex::new(Vec::new())); + let channel = FakeControlChannel::new( + sent.clone(), + [ + BrokerResponse::Negotiated { + broker_protocol_version: CLIENT_PROTOCOL_VERSION, + }, + BrokerResponse::Error(ErrorCode::WouldBlock), + ], + ); + let mut client = BrokerClient::new(channel); + client.negotiate().unwrap(); + let worker = BrokerClientWorker::new(client); + + let response = worker + .active_raw_request(BrokerRequest::Negotiate { + protocol_version: CLIENT_PROTOCOL_VERSION, + }) + .unwrap(); + + assert_eq!(response, BrokerResponse::Error(ErrorCode::WouldBlock)); + assert_eq!(sent.lock().unwrap().len(), 2); + worker.shutdown(); + } + + #[test] + fn worker_rejects_requests_after_shutdown() { + let sent = Arc::new(Mutex::new(Vec::new())); + let channel = FakeControlChannel::new( + sent, + [BrokerResponse::Negotiated { + broker_protocol_version: CLIENT_PROTOCOL_VERSION, + }], + ); + let mut client = BrokerClient::new(channel); + client.negotiate().unwrap(); + let worker = BrokerClientWorker::new(client); + + worker.shutdown(); + + assert!(matches!( + worker.active_raw_request(BrokerRequest::Negotiate { + protocol_version: CLIENT_PROTOCOL_VERSION, + }), + Err(BrokerClientWorkerError::Shutdown) + )); + } + + struct FakeControlChannel { + sent: Arc>>, + responses: VecDeque, + } + + impl FakeControlChannel { + fn new( + sent: Arc>>, + responses: [BrokerResponse; N], + ) -> Self { + Self { + sent, + responses: VecDeque::from(responses), + } + } + } + + impl ClientControlChannel for FakeControlChannel { + type Error = Infallible; + + fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error> { + self.sent.lock().unwrap().push(*request); + Ok(()) + } + + fn recv_response(&mut self) -> Result, Self::Error> { + Ok(self + .responses + .pop_front() + .map(ReceivedBrokerResponse::Response)) + } + } +} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index adacce3b3..739a5b873 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -19,7 +19,6 @@ mod event; mod identity; mod object; mod policy; -mod types; use alloc::collections::BTreeMap; @@ -30,11 +29,11 @@ pub use litebox_broker_protocol::{ ObjectReferenceId, ReadinessState, WaitOutcome, }; use object::{ObjectEntry, ObjectId, ObjectReference}; +pub use object::{ObjectRights, ObjectType}; pub use policy::{ DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, }; -pub use types::{ObjectRights, ObjectType}; /// BrokerCore result type. pub type Result = core::result::Result; diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 0f53b9d12..18dde901b 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -2,15 +2,61 @@ // Licensed under the MIT license. use alloc::vec::Vec; +use core::ops::BitOr; use crate::event::EventObject; use crate::identity::{AssociationIdentity, BrokerAssociation}; use crate::{ - BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyDecision, PolicyEngine, - PolicyOperation, Result, allocate_id, + BrokerCore, BrokerError, PolicyDecision, PolicyEngine, 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)] diff --git a/litebox_broker_core/src/types.rs b/litebox_broker_core/src/types.rs deleted file mode 100644 index 916a9b39f..000000000 --- a/litebox_broker_core/src/types.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use core::ops::BitOr; - -/// 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 signaling an event. - 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) - } -} diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 2b87cec47..9657ff7a5 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -120,13 +120,24 @@ fn handle_active_request( BrokerResponse::Error(ErrorCode::ProtocolState), CloseReason::ProtocolViolation, ), - BrokerRequest::Core(CoreRequest::Event(request)) => { - BrokerDispatch::continue_after(handle_event_request(core, association, request)) + 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, diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 164e2c23e..7a81c1afc 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -8,7 +8,7 @@ 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_client = { version = "0.1.0", path = "../litebox_broker_client" } +litebox_broker_client = { version = "0.1.0", path = "../litebox_broker_client", features = ["std"] } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } litebox_broker_unix_socket = { version = "0.1.0", path = "../litebox_broker_unix_socket" } litebox_common_linux = { version = "0.1.0", path = "../litebox_common_linux" } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 5c065a23b..93edc77eb 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,48 +3,29 @@ use std::{ path::Path, - sync::{ - Condvar, Mutex, - atomic::{AtomicBool, AtomicU8, Ordering}, - }, - thread::{self, JoinHandle, Thread}, + thread, time::{Duration, Instant}, }; use alloc::sync::Arc; use anyhow::{Context as _, Result}; -use litebox_broker_client::BrokerClient; +use litebox::broker::{BrokerControl, BrokerControlError}; +use litebox_broker_client::{BrokerClient, BrokerClientWorker}; use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; use litebox_broker_unix_socket::UnixStreamClientControlChannel; -use litebox_shim_linux::{BrokerControl, BrokerControlError}; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const RETRY_DELAY: Duration = Duration::from_millis(20); -const PHASE_IDLE: u8 = 0; -const PHASE_RESERVED: u8 = 1; -const PHASE_REQUEST_READY: u8 = 2; -const PHASE_RESPONSE_READY: u8 = 3; -const PHASE_SHUTDOWN: u8 = 4; type Client = BrokerClient; +type ClientWorker = BrokerClientWorker; pub(crate) struct BrokerConnection { control: Arc, } struct BrokerControlClient { - state: Arc, - worker: Mutex>>, -} - -struct BrokerControlState { - shutdown_requested: AtomicBool, - phase: AtomicU8, - request: Mutex>, - response: Mutex>>, - requester_wait: Mutex<()>, - requester_wakeup: Condvar, - worker_thread: Mutex>, + worker: ClientWorker, } pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { @@ -72,36 +53,13 @@ impl Drop for BrokerConnection { impl BrokerControlClient { fn new(client: Client) -> Self { - let state = Arc::new(BrokerControlState { - shutdown_requested: AtomicBool::new(false), - phase: AtomicU8::new(PHASE_IDLE), - request: Mutex::new(None), - response: Mutex::new(None), - requester_wait: Mutex::new(()), - requester_wakeup: Condvar::new(), - worker_thread: Mutex::new(None), - }); - let worker_state = state.clone(); - let worker = thread::spawn(move || run_broker_control_worker(client, worker_state)); - *state - .worker_thread - .lock() - .expect("broker control worker-thread mutex poisoned") = Some(worker.thread().clone()); Self { - state, - worker: Mutex::new(Some(worker)), + worker: BrokerClientWorker::new(client), } } fn shutdown(&self) { - let mut worker = self - .worker - .lock() - .expect("broker control worker mutex poisoned"); - if let Some(worker) = worker.take() { - self.state.request_shutdown(); - worker.join().expect("broker control worker panicked"); - } + self.worker.shutdown(); } } @@ -110,160 +68,9 @@ impl BrokerControl for BrokerControlClient { &self, request: BrokerRequest, ) -> core::result::Result { - self.state.submit(request) - } -} - -impl BrokerControlState { - fn submit( - &self, - request: BrokerRequest, - ) -> core::result::Result { - // Guest syscall handling must not perform host socket I/O on the - // rewritten thread, so this side publishes work to the dedicated worker. - loop { - if self.shutdown_requested.load(Ordering::Acquire) { - return Err(BrokerControlError); - } - match self.phase.load(Ordering::Acquire) { - PHASE_IDLE => { - if self - .phase - .compare_exchange( - PHASE_IDLE, - PHASE_RESERVED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - if self.shutdown_requested.load(Ordering::Acquire) { - self.store_phase_and_notify(PHASE_IDLE); - return Err(BrokerControlError); - } - break; - } - } - PHASE_SHUTDOWN => return Err(BrokerControlError), - phase => self.wait_for_phase_change(phase, true), - } - } - - *self - .request - .lock() - .expect("broker control request mutex poisoned") = Some(request); - self.phase.store(PHASE_REQUEST_READY, Ordering::Release); - self.wake_worker(); - - loop { - match self.phase.load(Ordering::Acquire) { - PHASE_RESPONSE_READY => break, - PHASE_SHUTDOWN => return Err(BrokerControlError), - phase => self.wait_for_phase_change(phase, false), - } - } - - let response = self - .response - .lock() - .expect("broker control response mutex poisoned") - .take() - .expect("broker control worker did not publish a response"); - self.store_phase_and_notify(PHASE_IDLE); - response - } - - fn request_shutdown(&self) { - { - let _guard = self - .requester_wait - .lock() - .expect("broker control requester-wait mutex poisoned"); - self.shutdown_requested.store(true, Ordering::Release); - self.requester_wakeup.notify_all(); - } - loop { - match self.phase.load(Ordering::Acquire) { - PHASE_IDLE => { - if self - .phase - .compare_exchange( - PHASE_IDLE, - PHASE_SHUTDOWN, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - self.wake_worker(); - return; - } - } - PHASE_SHUTDOWN => return, - phase => self.wait_for_phase_change(phase, false), - } - } - } - - fn store_phase_and_notify(&self, phase: u8) { - let _guard = self - .requester_wait - .lock() - .expect("broker control requester-wait mutex poisoned"); - self.phase.store(phase, Ordering::Release); - self.requester_wakeup.notify_all(); - } - - fn wait_for_phase_change(&self, observed: u8, wake_on_shutdown: bool) { - let mut guard = self - .requester_wait - .lock() - .expect("broker control requester-wait mutex poisoned"); - while self.phase.load(Ordering::Acquire) == observed - && !(wake_on_shutdown && self.shutdown_requested.load(Ordering::Acquire)) - { - guard = self - .requester_wakeup - .wait(guard) - .expect("broker control requester-wait mutex poisoned"); - } - } - - fn wake_worker(&self) { - if let Some(worker) = self - .worker_thread - .lock() - .expect("broker control worker-thread mutex poisoned") - .as_ref() - { - worker.unpark(); - } - } -} - -fn run_broker_control_worker(mut client: Client, state: Arc) { - loop { - match state.phase.load(Ordering::Acquire) { - PHASE_REQUEST_READY => { - let request = state - .request - .lock() - .expect("broker control request mutex poisoned") - .take() - .expect("broker control request missing"); - let response = client - .active_raw_request(request) - .map_err(|_| BrokerControlError); - *state - .response - .lock() - .expect("broker control response mutex poisoned") = Some(response); - state.store_phase_and_notify(PHASE_RESPONSE_READY); - } - PHASE_SHUTDOWN => break, - _ => thread::park(), - } + self.worker + .active_raw_request(request) + .map_err(|_| BrokerControlError) } } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index a5584cbe5..50c19ec80 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -214,9 +214,11 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; - let mut shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); if let Some(broker_connection) = &broker_connection { - shim_builder = shim_builder.broker_control(broker_connection.control()); + shim_builder + .litebox() + .set_broker_control(broker_connection.control()); } let litebox = shim_builder.litebox(); // SAFETY: `gettid` takes no pointer arguments and has no Rust-side aliasing requirements. diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index b1f8f036e..8d6e23154 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -8,7 +8,6 @@ arrayvec = { version = "0.7.6", default-features = false } bitvec = { version = "1.0.1", default-features = false, features = ["alloc"] } bitflags = "2.9.0" litebox = { path = "../litebox/", version = "0.1.0" } -litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } litebox_common_linux = { path = "../litebox_common_linux/", version = "0.1.0" } litebox_platform_multiplex = { path = "../litebox_platform_multiplex/", version = "0.1.0", default-features = false } litebox_util_log = { version = "0.1.0", path = "../litebox_util_log" } diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 458e25451..c9fd47917 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, @@ -63,23 +63,6 @@ pub(crate) type LinuxFS = litebox::fs::layered::FileSystem< >; pub(crate) type FileFd = litebox::fd::TypedFd; -/// Error returned by the runner-provided broker control path. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BrokerControlError; - -/// Local-core access to the negotiated broker control channel. -/// -/// The shim/local core owns Linux ABI translation and constructs broker protocol -/// requests. The runner owns endpoint selection and supplies the connected -/// transport behind this protocol-level boundary. -pub trait BrokerControl: Send + Sync { - /// Sends one active broker request and returns its response. - fn request( - &self, - request: litebox_broker_protocol::BrokerRequest, - ) -> core::result::Result; -} - /// A trait required for file systems to be used in the shim. pub trait ShimFS: litebox::fs::FileSystem + Send + Sync + 'static {} impl ShimFS for T {} @@ -169,7 +152,6 @@ impl LinuxShimEntrypoints { pub struct LinuxShimBuilder { platform: &'static Platform, litebox: LiteBox, - broker_control: Option>, } impl Default for LinuxShimBuilder { @@ -185,7 +167,6 @@ impl LinuxShimBuilder { Self { platform, litebox: LiteBox::new(platform), - broker_control: None, } } @@ -203,13 +184,6 @@ impl LinuxShimBuilder { default_fs(&self.litebox, in_mem_fs, tar_ro_fs) } - /// Installs a runner-provided broker control channel for broker-backed local-core objects. - #[must_use] - pub fn broker_control(mut self, broker_control: Arc) -> Self { - self.broker_control = Some(broker_control); - self - } - /// Build the shim. pub fn build(self) -> LinuxShim { let mut net = Network::new(&self.litebox); @@ -225,7 +199,6 @@ impl LinuxShimBuilder { litebox: self.litebox, unix_addr_table: litebox::sync::RwLock::new(syscalls::unix::UnixAddrTable::new()), elf_patch_cache: litebox::sync::Mutex::new(alloc::collections::BTreeMap::new()), - broker_control: self.broker_control, }); LinuxShim(global) } @@ -1130,8 +1103,6 @@ struct GlobalState { unix_addr_table: litebox::sync::RwLock>, /// Per-process collection of ELF patching state for runtime syscall rewriting. elf_patch_cache: litebox::sync::Mutex, - /// Optional broker control path installed by the runner. - broker_control: Option>, } struct Task { diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 9f717d54b..b7f334177 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -3,11 +3,10 @@ //! Event file for notification -use alloc::sync::Arc; use core::sync::atomic::AtomicU32; -use crate::BrokerControl; use litebox::{ + broker::{BrokerEvent, BrokerEventConsumeMode, BrokerObjectError}, event::{ Events, IOPollable, observer::Observer, @@ -19,11 +18,6 @@ use litebox::{ platform::TimeProvider, sync::RawSyncPrimitivesProvider, }; -use litebox_broker_protocol::{ - AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, CoreRequest, CoreResponse, - CreateEventRequest, ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, - WaitEventRequest, WaitOutcome, -}; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::Platform; @@ -48,8 +42,7 @@ enum EventBackend { Broker { /// Broker-backed eventfds are currently created only for `EFD_NONBLOCK`; /// blocking broker wait/notification plumbing is not implemented yet. - broker: Arc, - handle: ObjectHandle, + event: BrokerEvent, }, } @@ -63,27 +56,12 @@ impl EventFile { ) } - pub(crate) fn new_broker( - count: u64, - flags: EfdFlags, - broker: Arc, - ) -> Result { + pub(crate) fn new_broker(event: BrokerEvent, flags: EfdFlags) -> Result { if !flags.contains(EfdFlags::NONBLOCK) { return Err(Errno::EINVAL); } - let response = broker_request( - &broker, - EventRequest::Create(CreateEventRequest::new(count)), - )?; - let BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) = response - else { - return Err(response_to_errno(response)); - }; Ok(Self::new_with_backend( - EventBackend::Broker { - broker, - handle: response.handle, - }, + EventBackend::Broker { event }, flags, )) } @@ -115,24 +93,19 @@ impl EventFile { self.pollee.notify_observers(Events::OUT); Ok(res) } - EventBackend::Broker { broker, handle } => { + EventBackend::Broker { event } => { let mode = if self.semaphore { - EventConsumeMode::One + BrokerEventConsumeMode::One } else { - EventConsumeMode::All + BrokerEventConsumeMode::All }; - let response = broker_request( - broker, - EventRequest::Consume(ConsumeEventRequest::new(*handle, mode)), - ) - .map_err(TryOpError::Other)?; - match response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { + match event.consume(mode) { + Ok(value) => { self.pollee.notify_observers(Events::OUT); - Ok(response.value) + Ok(value) } - BrokerResponse::Error(ErrorCode::WouldBlock) => Err(TryOpError::TryAgain), - response => Err(TryOpError::Other(response_to_errno(response))), + Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(broker_object_error_to_errno(error))), } } } @@ -173,21 +146,16 @@ impl EventFile { Err(TryOpError::TryAgain) } - EventBackend::Broker { broker, handle } => { - let response = broker_request( - broker, - EventRequest::Add(AddEventRequest::new(*handle, value)), - ) - .map_err(TryOpError::Other)?; - match response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(_))) => { - self.pollee.notify_observers(Events::IN); - Ok(8) - } - BrokerResponse::Error(ErrorCode::WouldBlock) => Err(TryOpError::TryAgain), - response => Err(TryOpError::Other(response_to_errno(response))), + EventBackend::Broker { event } => match event.add(value) { + Ok(()) => { + self.pollee.notify_observers(Events::IN); + Ok(8) } - } + Err(BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted) => { + Err(TryOpError::TryAgain) + } + Err(error) => Err(TryOpError::Other(broker_object_error_to_errno(error))), + }, } } @@ -238,15 +206,12 @@ impl IOPollable for EventFil events } - EventBackend::Broker { broker, handle } => { + EventBackend::Broker { event } => { // The broker protocol currently exposes read readiness only. Keep // write readiness optimistic and surface counter-limit failures // from write as EAGAIN until broker write-readiness plumbing exists. let mut events = Events::OUT; - if let Ok(BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response)))) = - broker_request(broker, EventRequest::Wait(WaitEventRequest::new(*handle))) - && matches!(response.outcome, WaitOutcome::Ready(_)) - { + if event.is_read_ready().unwrap_or(false) { events |= Events::IN; } events @@ -259,46 +224,20 @@ impl IOPollable for EventFil } } -fn broker_request( - broker: &Arc, - request: EventRequest, -) -> Result { - broker - .request(BrokerRequest::Core(CoreRequest::Event(request))) - .map_err(|_| Errno::EIO) -} - -fn response_to_errno(response: BrokerResponse) -> Errno { - match response { - BrokerResponse::Error(error) => error_to_errno(error), - _ => Errno::EIO, - } -} - -fn error_to_errno(error: ErrorCode) -> Errno { +pub(crate) fn broker_object_error_to_errno(error: BrokerObjectError) -> Errno { match error { - ErrorCode::InvalidRights | ErrorCode::WrongObjectType | ErrorCode::StaleHandle => { - Errno::EINVAL - } - ErrorCode::WouldBlock | ErrorCode::ResourceExhausted => Errno::EAGAIN, + BrokerObjectError::InvalidObject => Errno::EINVAL, + BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted => Errno::EAGAIN, _ => Errno::EIO, } } #[cfg(test)] mod tests { - use alloc::sync::Arc; use litebox::event::wait::WaitState; - use litebox::fs::OFlags; - use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, - EventRequest, EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, - }; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::platform; - use crate::{BrokerControl, BrokerControlError}; - extern crate std; #[test] @@ -410,38 +349,4 @@ mod tests { // block until the second write read(&eventfd, u64::MAX - 1); } - - #[test] - fn broker_eventfd_rejects_clearing_nonblock() { - let _task = crate::syscalls::tests::init_platform(None); - let eventfd: super::EventFile = - super::EventFile::new_broker(0, EfdFlags::NONBLOCK, Arc::new(CreateOnlyBroker)) - .unwrap(); - - assert_eq!( - eventfd.set_status_flags(OFlags::empty(), OFlags::NONBLOCK), - Err(Errno::EINVAL) - ); - assert!(eventfd.get_status().contains(OFlags::NONBLOCK)); - } - - struct CreateOnlyBroker; - - impl BrokerControl for CreateOnlyBroker { - fn request( - &self, - request: BrokerRequest, - ) -> core::result::Result { - assert!(matches!( - request, - BrokerRequest::Core(CoreRequest::Event(EventRequest::Create(_))) - )); - Ok(BrokerResponse::Core(CoreResponse::Event( - EventResponse::Create(CreateEventResponse::new(ObjectHandle::new( - ObjectReferenceId::new(1), - ObjectReferenceGeneration::new(1), - ))), - ))) - } - } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index c907485e0..7fefd779d 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1898,16 +1898,16 @@ impl Task { return Err(Errno::EINVAL); } - let eventfd = if flags.contains(EfdFlags::NONBLOCK) { - if let Some(broker_control) = &self.global.broker_control { - super::eventfd::EventFile::new_broker( - u64::from(initval), - flags, - broker_control.clone(), - )? - } else { - super::eventfd::EventFile::new(u64::from(initval), flags) - } + let broker_event = if flags.contains(EfdFlags::NONBLOCK) { + self.global + .litebox + .create_broker_event(u64::from(initval)) + .map_err(super::eventfd::broker_object_error_to_errno)? + } else { + None + }; + let eventfd = if let Some(event) = broker_event { + super::eventfd::EventFile::new_broker(event, flags)? } else { super::eventfd::EventFile::new(u64::from(initval), flags) }; From f7cc115c274e102c9abc492fe28c91500561e8ac Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 14:43:55 -0700 Subject: [PATCH 23/66] Hide event backend behind local core Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 2 +- litebox/src/event/counter.rs | 240 +++++++++++++++++++++ litebox/src/event/mod.rs | 3 + litebox/src/litebox.rs | 21 +- litebox_common_linux/src/errno/mod.rs | 12 ++ litebox_shim_linux/src/syscalls/epoll.rs | 8 +- litebox_shim_linux/src/syscalls/eventfd.rs | 217 ++++--------------- litebox_shim_linux/src/syscalls/file.rs | 15 +- 8 files changed, 321 insertions(+), 197 deletions(-) create mode 100644 litebox/src/event/counter.rs diff --git a/docs/broker-design.md b/docs/broker-design.md index 01c2ab816..2dd3850b1 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -806,7 +806,7 @@ minimal PolicyEngine broker-owned event object ``` -Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through the broker, while unrelated operations continue through the existing local compatibility path. UserLiteBox handle entries can therefore contain either local compatibility objects or broker reference handles with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. +Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through broker-backed local-core wrappers, while unrelated operations continue through the existing local compatibility path. Shims should keep calling local-core object interfaces; UserLiteBox/local-core entries can contain either local compatibility state or broker-backed wrappers with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. Then proceed incrementally: diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs new file mode 100644 index 000000000..47dd3a160 --- /dev/null +++ b/litebox/src/event/counter.rs @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::{Arc, Weak}; + +use super::{ + Events, IOPollable, + observer::Observer, + polling::{Pollee, TryOpError}, + wait::WaitContext, +}; +use crate::{ + broker::{BrokerControl, BrokerEvent, BrokerEventConsumeMode, BrokerObjectError}, + platform::TimeProvider, + sync::{Mutex, RawSyncPrimitivesProvider}, +}; + +const MAX_EVENT_COUNT: u64 = u64::MAX - 1; + +/// 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, +} + +/// How an event counter read should consume readiness credits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventCounterConsumeMode { + /// Consume all currently available credits. + All, + /// Consume one credit. + One, +} + +impl EventCounterConsumeMode { + const fn to_broker(self) -> BrokerEventConsumeMode { + match self { + Self::All => BrokerEventConsumeMode::All, + Self::One => BrokerEventConsumeMode::One, + } + } +} + +/// A local-core event counter. +/// +/// This object provides eventfd-like counter semantics without exposing whether +/// the backing state is local-private or broker-mediated. +pub struct EventCounter { + backend: EventCounterBackend, + pollee: Pollee, +} + +enum EventCounterBackend { + Local { counter: Mutex }, + Broker { event: BrokerEvent }, +} + +impl EventCounter { + pub(crate) fn new_local(initial_count: u64) -> Result { + if initial_count > MAX_EVENT_COUNT { + return Err(EventCounterError::ResourceExhausted); + } + Ok(Self { + backend: EventCounterBackend::Local { + counter: Mutex::new(initial_count), + }, + pollee: Pollee::new(), + }) + } + + pub(crate) fn new_broker(event: BrokerEvent) -> Self { + Self { + backend: EventCounterBackend::Broker { event }, + pollee: Pollee::new(), + } + } + + pub(crate) fn new_broker_from_control( + broker: Arc, + initial_count: u64, + ) -> Result { + BrokerEvent::create(broker, initial_count) + .map(Self::new_broker) + .map_err(broker_error_to_event_counter_error) + } + + /// Returns whether blocking reads and writes are supported. + pub fn supports_blocking_operations(&self) -> bool { + match self.backend { + EventCounterBackend::Local { .. } => true, + EventCounterBackend::Broker { .. } => false, + } + } + + /// Reads the event counter, optionally blocking until the counter is ready. + pub fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + mode: EventCounterConsumeMode, + ) -> Result> { + if !self.supports_blocking_operations() { + return self.try_read(mode); + } + self.pollee + .wait(cx, nonblock, Events::IN, || self.try_read(mode)) + } + + /// Writes readiness credits to the event counter, optionally blocking until + /// the counter can accept them. + pub fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result> { + if value == u64::MAX { + return Err(TryOpError::Other(EventCounterError::InvalidInput)); + } + if !self.supports_blocking_operations() { + return self.try_write(value); + } + self.pollee + .wait(cx, nonblock, Events::OUT, || self.try_write(value)) + } + + fn try_read( + &self, + mode: EventCounterConsumeMode, + ) -> Result> { + match &self.backend { + EventCounterBackend::Local { counter } => { + let mut counter = counter.lock(); + if *counter == 0 { + return Err(TryOpError::TryAgain); + } + + let res = match mode { + EventCounterConsumeMode::All => *counter, + EventCounterConsumeMode::One => 1, + }; + *counter -= res; + + drop(counter); + self.pollee.notify_observers(Events::OUT); + Ok(res) + } + EventCounterBackend::Broker { event } => match event.consume(mode.to_broker()) { + Ok(value) => { + self.pollee.notify_observers(Events::OUT); + Ok(value) + } + Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( + error, + ))), + }, + } + } + + fn try_write(&self, value: u64) -> Result> { + match &self.backend { + EventCounterBackend::Local { counter } => { + let mut counter = counter.lock(); + if let Some(new_value) = (*counter) + .checked_add(value) + .filter(|count| *count <= MAX_EVENT_COUNT) + { + *counter = new_value; + drop(counter); + self.pollee.notify_observers(Events::IN); + return Ok(core::mem::size_of::()); + } + + Err(TryOpError::TryAgain) + } + EventCounterBackend::Broker { event } => match event.add(value) { + Ok(()) => { + self.pollee.notify_observers(Events::IN); + Ok(core::mem::size_of::()) + } + Err(BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted) => { + Err(TryOpError::TryAgain) + } + Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( + error, + ))), + }, + } + } +} + +impl IOPollable for EventCounter { + fn register_observer(&self, observer: Weak>, mask: Events) { + self.pollee.register_observer(observer, mask); + } + + fn check_io_events(&self) -> Events { + match &self.backend { + EventCounterBackend::Local { counter } => { + let counter = counter.lock(); + let mut events = Events::empty(); + if *counter != 0 { + events |= Events::IN; + } + if *counter < MAX_EVENT_COUNT { + events |= Events::OUT; + } + events + } + EventCounterBackend::Broker { event } => { + // The broker protocol currently exposes read readiness only. Keep + // write readiness optimistic and surface counter-limit failures + // from write until broker write-readiness plumbing exists. + let mut events = Events::OUT; + if event.is_read_ready().unwrap_or(false) { + events |= Events::IN; + } + events + } + } + } +} + +const fn broker_error_to_event_counter_error(error: BrokerObjectError) -> EventCounterError { + match error { + BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, + BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, + BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, + _ => EventCounterError::Io, + } +} diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 24d5b6832..6b54e898f 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -7,6 +7,9 @@ pub mod observer; pub mod polling; pub mod wait; +mod counter; +pub use counter::{EventCounter, EventCounterConsumeMode, EventCounterError}; + bitflags::bitflags! { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Events: u32 { diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index bd94f44cd..c312172ef 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,8 +6,10 @@ use alloc::sync::Arc; use crate::{ - broker::{BrokerControl, BrokerEvent, BrokerObjectError}, + broker::BrokerControl, + event::{EventCounter, EventCounterError}, fd::Descriptors, + platform::TimeProvider, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -115,15 +117,22 @@ impl LiteBox { *self.x.broker_control.write() = Some(broker_control); } - /// Creates a broker-backed event object when broker control is installed. - pub fn create_broker_event( + /// Creates an event counter, using broker backing when available and compatible. + pub fn create_event_counter( &self, initial_count: u64, - ) -> Result, BrokerObjectError> { + requires_blocking: bool, + ) -> Result, EventCounterError> + where + Platform: TimeProvider, + { + if requires_blocking { + return EventCounter::new_local(initial_count); + } let Some(broker) = self.x.broker_control.read().clone() else { - return Ok(None); + return EventCounter::new_local(initial_count); }; - BrokerEvent::create(broker, initial_count).map(Some) + EventCounter::new_broker_from_control(broker, initial_count) } } diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 5153a83fa..0096e1e91 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,6 +536,18 @@ where } } +impl From for Errno { + fn from(value: litebox::event::EventCounterError) -> Self { + match value { + litebox::event::EventCounterError::InvalidInput => Errno::EINVAL, + litebox::event::EventCounterError::WouldBlock + | litebox::event::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::event::EventCounterError::Io => Errno::EIO, + _ => Errno::EIO, + } + } +} + impl From for Errno { fn from(value: litebox::fs::errors::ReadDirError) -> Self { match value { diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 3c9d30077..f7c838761 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -635,7 +635,9 @@ 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 = + crate::syscalls::eventfd::EventFile::new(&task.global.litebox, 0, EfdFlags::CLOEXEC) + .unwrap(); let typed = task .global .litebox @@ -730,7 +732,9 @@ 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 = + crate::syscalls::eventfd::EventFile::new(&task.global.litebox, 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 b7f334177..1ef8b6855 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -6,11 +6,9 @@ use core::sync::atomic::AtomicU32; use litebox::{ - broker::{BrokerEvent, BrokerEventConsumeMode, BrokerObjectError}, + LiteBox, event::{ - Events, IOPollable, - observer::Observer, - polling::{Pollee, TryOpError}, + EventCounter, EventCounterConsumeMode, Events, IOPollable, observer::Observer, wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, @@ -28,148 +26,45 @@ impl FdEnabledSubsystem for EventfdSubsystem { impl FdEnabledSubsystemEntry for EventFile {} pub(crate) struct EventFile { - backend: EventBackend, + event: EventCounter, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, - pollee: Pollee, -} - -enum EventBackend { - Local { - counter: litebox::sync::Mutex, - }, - Broker { - /// Broker-backed eventfds are currently created only for `EFD_NONBLOCK`; - /// blocking broker wait/notification plumbing is not implemented yet. - event: BrokerEvent, - }, } impl EventFile { - pub(crate) fn new(count: u64, flags: EfdFlags) -> Self { - Self::new_with_backend( - EventBackend::Local { - counter: litebox::sync::Mutex::new(count), - }, - flags, - ) - } - - pub(crate) fn new_broker(event: BrokerEvent, flags: EfdFlags) -> Result { - if !flags.contains(EfdFlags::NONBLOCK) { - return Err(Errno::EINVAL); - } - Ok(Self::new_with_backend( - EventBackend::Broker { event }, - flags, - )) - } - - fn new_with_backend(backend: EventBackend, flags: EfdFlags) -> Self { + pub(crate) fn new( + litebox: &LiteBox, + count: u64, + flags: EfdFlags, + ) -> Result { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); + let event = litebox + .create_event_counter(count, !flags.contains(EfdFlags::NONBLOCK)) + .map_err(Errno::from)?; - Self { - backend, + Ok(Self { + event, status: AtomicU32::new(status.bits()), semaphore: flags.contains(EfdFlags::SEMAPHORE), - pollee: Pollee::new(), - } - } - - fn try_read(&self) -> Result> { - match &self.backend { - EventBackend::Local { counter } => { - let mut counter = counter.lock(); - if *counter == 0 { - return Err(TryOpError::TryAgain); - } - - let res = if self.semaphore { 1 } else { *counter }; - *counter -= res; - - drop(counter); - self.pollee.notify_observers(Events::OUT); - Ok(res) - } - EventBackend::Broker { event } => { - let mode = if self.semaphore { - BrokerEventConsumeMode::One - } else { - BrokerEventConsumeMode::All - }; - match event.consume(mode) { - Ok(value) => { - self.pollee.notify_observers(Events::OUT); - Ok(value) - } - Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(broker_object_error_to_errno(error))), - } - } - } + }) } pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - if matches!(&self.backend, EventBackend::Broker { .. }) { - return self.try_read().map_err(Errno::from); - } - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::IN, - || self.try_read(), - ) + let mode = if self.semaphore { + EventCounterConsumeMode::One + } else { + EventCounterConsumeMode::All + }; + self.event + .read(cx, self.get_status().contains(OFlags::NONBLOCK), mode) .map_err(Errno::from) } - fn try_write(&self, value: u64) -> Result> { - if value == u64::MAX { - return Err(TryOpError::Other(Errno::EINVAL)); - } - - match &self.backend { - EventBackend::Local { counter } => { - let mut counter = 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); - } - } - - Err(TryOpError::TryAgain) - } - EventBackend::Broker { event } => match event.add(value) { - Ok(()) => { - self.pollee.notify_observers(Events::IN); - Ok(8) - } - Err(BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted) => { - Err(TryOpError::TryAgain) - } - Err(error) => Err(TryOpError::Other(broker_object_error_to_errno(error))), - }, - } - } - pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - if matches!(&self.backend, EventBackend::Broker { .. }) { - return self.try_write(value).map_err(Errno::from); - } - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::OUT, - || self.try_write(value), - ) + self.event + .write(cx, self.get_status().contains(OFlags::NONBLOCK), value) .map_err(Errno::from) } @@ -177,9 +72,7 @@ impl EventFile { pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { let new_status = (self.get_status() & mask.complement()) | (requested & mask); - if matches!(&self.backend, EventBackend::Broker { .. }) - && !new_status.contains(OFlags::NONBLOCK) - { + if !new_status.contains(OFlags::NONBLOCK) && !self.event.supports_blocking_operations() { return Err(Errno::EINVAL); } self.set_status(requested & mask, true); @@ -190,45 +83,11 @@ impl EventFile { impl IOPollable for EventFile { fn check_io_events(&self) -> Events { - match &self.backend { - EventBackend::Local { counter } => { - let counter = 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; - } - - events - } - EventBackend::Broker { event } => { - // The broker protocol currently exposes read readiness only. Keep - // write readiness optimistic and surface counter-limit failures - // from write as EAGAIN until broker write-readiness plumbing exists. - let mut events = Events::OUT; - if event.is_read_ready().unwrap_or(false) { - events |= Events::IN; - } - events - } - } + self.event.check_io_events() } fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - self.pollee.register_observer(observer, mask); - } -} - -pub(crate) fn broker_object_error_to_errno(error: BrokerObjectError) -> Errno { - match error { - BrokerObjectError::InvalidObject => Errno::EINVAL, - BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted => Errno::EAGAIN, - _ => Errno::EIO, + self.event.register_observer(observer, mask); } } @@ -242,9 +101,11 @@ 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( + super::EventFile::new(&task.global.litebox, 0, EfdFlags::SEMAPHORE).unwrap(), + ); let total = 8; for _ in 0..total { let copied_eventfd = eventfd.clone(); @@ -263,9 +124,11 @@ mod tests { #[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( + super::EventFile::new(&task.global.litebox, 0, EfdFlags::empty()).unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { copied_eventfd @@ -288,9 +151,11 @@ 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( + super::EventFile::new(&task.global.litebox, 0, EfdFlags::empty()).unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { for _ in 0..10000 { @@ -308,9 +173,11 @@ mod tests { #[test] fn test_nonblocking_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::NONBLOCK)); + let eventfd = alloc::sync::Arc::new( + super::EventFile::new(&task.global.litebox, 0, EfdFlags::NONBLOCK).unwrap(), + ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { // first write should succeed immediately diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 7fefd779d..5663e2dac 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1898,19 +1898,8 @@ impl Task { return Err(Errno::EINVAL); } - let broker_event = if flags.contains(EfdFlags::NONBLOCK) { - self.global - .litebox - .create_broker_event(u64::from(initval)) - .map_err(super::eventfd::broker_object_error_to_errno)? - } else { - None - }; - let eventfd = if let Some(event) = broker_event { - super::eventfd::EventFile::new_broker(event, flags)? - } else { - super::eventfd::EventFile::new(u64::from(initval), flags) - }; + let eventfd = + super::eventfd::EventFile::new(&self.global.litebox, u64::from(initval), flags)?; let mut dt = self.global.litebox.descriptor_table_mut(); let typed = dt.insert::(eventfd); if flags.contains(EfdFlags::CLOEXEC) { From 31186443e1c395868d3aca1eb18a186924ec5a40 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 15:07:56 -0700 Subject: [PATCH 24/66] Clean up local core broker layering Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker.rs | 151 +++++++++---- litebox/src/event/counter.rs | 221 +++++++++----------- litebox/src/event/mod.rs | 1 + litebox/src/lib.rs | 4 +- litebox/src/litebox.rs | 22 +- litebox_runner_linux_userland/src/broker.rs | 2 +- 6 files changed, 224 insertions(+), 177 deletions(-) diff --git a/litebox/src/broker.rs b/litebox/src/broker.rs index 7afa6cca3..f2fdf7576 100644 --- a/litebox/src/broker.rs +++ b/litebox/src/broker.rs @@ -9,6 +9,12 @@ use litebox_broker_protocol::{ WaitEventRequest, WaitOutcome, }; +use crate::{ + event::{EventCounter, EventCounterConsumeMode, EventCounterError, EventCounterSource, Events}, + platform::TimeProvider, + sync::{RawSyncPrimitivesProvider, RwLock}, +}; + /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct BrokerControlError; @@ -26,51 +32,61 @@ pub trait BrokerControl: Send + Sync { ) -> core::result::Result; } -/// Error returned by broker-backed local-core objects. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum BrokerObjectError { - /// The broker control path failed. - Control, - /// The broker rejected the object handle, type, or rights. - InvalidObject, - /// The operation would block. - WouldBlock, - /// The broker object cannot accept more state. - ResourceExhausted, - /// The broker returned an unexpected response for the request. - UnexpectedResponse, - /// The broker returned an error not represented by this local-core object API. - Internal, +pub(crate) struct BrokerState { + control: RwLock>>, } -/// How a broker event consume operation should remove readiness credits. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum BrokerEventConsumeMode { - /// Consume all currently available credits. - All, - /// Consume one credit. - One, -} +impl BrokerState { + pub(crate) fn new() -> Self { + Self { + control: RwLock::new(None), + } + } -impl BrokerEventConsumeMode { - const fn to_protocol(self) -> EventConsumeMode { - match self { - Self::All => EventConsumeMode::All, - Self::One => EventConsumeMode::One, + pub(crate) fn set_control(&self, broker_control: Arc) { + *self.control.write() = Some(broker_control); + } + + pub(crate) fn create_event_counter( + &self, + initial_count: u64, + requires_blocking: bool, + ) -> Result, EventCounterError> + where + Platform: TimeProvider + 'static, + { + if requires_blocking { + return EventCounter::new_local(initial_count); } + let Some(control) = self.control.read().clone() else { + return EventCounter::new_local(initial_count); + }; + create_broker_event_counter(control, initial_count) } } -/// Broker-backed event object owned by the local core. #[derive(Clone)] -pub struct BrokerEvent { +struct BrokerEvent { broker: Arc, handle: ObjectHandle, } +struct BrokerEventCounter { + event: BrokerEvent, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum BrokerObjectError { + Control, + InvalidObject, + WouldBlock, + ResourceExhausted, + UnexpectedResponse, + Internal, +} + impl BrokerEvent { - pub(crate) fn create( + fn create( broker: Arc, initial_count: u64, ) -> Result { @@ -87,11 +103,10 @@ impl BrokerEvent { }) } - /// Consumes readiness credits from the event. - pub fn consume(&self, mode: BrokerEventConsumeMode) -> Result { + fn consume(&self, mode: EventCounterConsumeMode) -> Result { let response = self.request(EventRequest::Consume(ConsumeEventRequest::new( self.handle, - mode.to_protocol(), + to_protocol_consume_mode(mode), )))?; let EventResponse::Consume(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); @@ -99,8 +114,7 @@ impl BrokerEvent { Ok(response.value) } - /// Adds readiness credits to the event. - pub fn add(&self, value: u64) -> Result<(), BrokerObjectError> { + fn add(&self, value: u64) -> Result<(), BrokerObjectError> { let response = self.request(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; let EventResponse::Add(_) = response else { return Err(BrokerObjectError::UnexpectedResponse); @@ -108,8 +122,7 @@ impl BrokerEvent { Ok(()) } - /// Returns whether an event wait would complete now. - pub fn is_read_ready(&self) -> Result { + fn is_read_ready(&self) -> Result { let response = self.request(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; let EventResponse::Wait(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); @@ -122,6 +135,50 @@ impl BrokerEvent { } } +impl EventCounterSource for BrokerEventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn supports_blocking_operations(&self) -> bool { + false + } + + fn read(&self, mode: EventCounterConsumeMode) -> Result { + self.event + .consume(mode) + .map_err(broker_error_to_event_counter_error) + } + + fn write(&self, value: u64) -> Result<(), EventCounterError> { + self.event + .add(value) + .map_err(broker_error_to_event_counter_error) + } + + fn check_io_events(&self) -> Events { + // The broker protocol currently exposes read readiness only. Keep write + // readiness optimistic and surface counter-limit failures from write + // until broker write-readiness plumbing exists. + let mut events = Events::OUT; + if self.event.is_read_ready().unwrap_or(false) { + events |= Events::IN; + } + events + } +} + +fn create_broker_event_counter( + broker: Arc, + initial_count: u64, +) -> Result, EventCounterError> +where + Platform: RawSyncPrimitivesProvider + TimeProvider + 'static, +{ + let event = + BrokerEvent::create(broker, initial_count).map_err(broker_error_to_event_counter_error)?; + Ok(EventCounter::from_source(BrokerEventCounter { event })) +} + fn request_event( broker: &Arc, request: EventRequest, @@ -136,6 +193,13 @@ fn request_event( } } +const fn to_protocol_consume_mode(mode: EventCounterConsumeMode) -> EventConsumeMode { + match mode { + EventCounterConsumeMode::All => EventConsumeMode::All, + EventCounterConsumeMode::One => EventConsumeMode::One, + } +} + const fn error_to_object_error(error: ErrorCode) -> BrokerObjectError { match error { ErrorCode::InvalidRights | ErrorCode::WrongObjectType | ErrorCode::StaleHandle => { @@ -146,3 +210,12 @@ const fn error_to_object_error(error: ErrorCode) -> BrokerObjectError { _ => BrokerObjectError::Internal, } } + +const fn broker_error_to_event_counter_error(error: BrokerObjectError) -> EventCounterError { + match error { + BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, + BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, + BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, + _ => EventCounterError::Io, + } +} diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 47dd3a160..29637de75 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::sync::{Arc, Weak}; +use alloc::{boxed::Box, sync::Weak}; use super::{ Events, IOPollable, @@ -10,7 +10,6 @@ use super::{ wait::WaitContext, }; use crate::{ - broker::{BrokerControl, BrokerEvent, BrokerEventConsumeMode, BrokerObjectError}, platform::TimeProvider, sync::{Mutex, RawSyncPrimitivesProvider}, }; @@ -40,64 +39,57 @@ pub enum EventCounterConsumeMode { One, } -impl EventCounterConsumeMode { - const fn to_broker(self) -> BrokerEventConsumeMode { - match self { - Self::All => BrokerEventConsumeMode::All, - Self::One => BrokerEventConsumeMode::One, - } - } -} - /// A local-core event counter. /// /// This object provides eventfd-like counter semantics without exposing whether -/// the backing state is local-private or broker-mediated. +/// the backing state is local-private or externally mediated. pub struct EventCounter { - backend: EventCounterBackend, + source: Box>, pollee: Pollee, } -enum EventCounterBackend { - Local { counter: Mutex }, - Broker { event: BrokerEvent }, +pub(crate) trait EventCounterSource: Send + Sync +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn supports_blocking_operations(&self) -> bool; + fn read(&self, mode: EventCounterConsumeMode) -> Result; + fn write(&self, value: u64) -> Result<(), EventCounterError>; + fn check_io_events(&self) -> Events; } -impl EventCounter { +struct LocalEventCounter { + counter: Mutex, +} + +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider + 'static, +{ pub(crate) fn new_local(initial_count: u64) -> Result { if initial_count > MAX_EVENT_COUNT { return Err(EventCounterError::ResourceExhausted); } - Ok(Self { - backend: EventCounterBackend::Local { - counter: Mutex::new(initial_count), - }, - pollee: Pollee::new(), - }) + Ok(Self::from_source(LocalEventCounter { + counter: Mutex::new(initial_count), + })) } - pub(crate) fn new_broker(event: BrokerEvent) -> Self { + pub(crate) fn from_source(source: impl EventCounterSource + 'static) -> Self { Self { - backend: EventCounterBackend::Broker { event }, + source: Box::new(source), pollee: Pollee::new(), } } +} - pub(crate) fn new_broker_from_control( - broker: Arc, - initial_count: u64, - ) -> Result { - BrokerEvent::create(broker, initial_count) - .map(Self::new_broker) - .map_err(broker_error_to_event_counter_error) - } - +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ /// Returns whether blocking reads and writes are supported. pub fn supports_blocking_operations(&self) -> bool { - match self.backend { - EventCounterBackend::Local { .. } => true, - EventCounterBackend::Broker { .. } => false, - } + self.source.supports_blocking_operations() } /// Reads the event counter, optionally blocking until the counter is ready. @@ -136,105 +128,88 @@ impl EventCounter &self, mode: EventCounterConsumeMode, ) -> Result> { - match &self.backend { - EventCounterBackend::Local { counter } => { - let mut counter = counter.lock(); - if *counter == 0 { - return Err(TryOpError::TryAgain); - } - - let res = match mode { - EventCounterConsumeMode::All => *counter, - EventCounterConsumeMode::One => 1, - }; - *counter -= res; - - drop(counter); - self.pollee.notify_observers(Events::OUT); - Ok(res) - } - EventCounterBackend::Broker { event } => match event.consume(mode.to_broker()) { - Ok(value) => { - self.pollee.notify_observers(Events::OUT); - Ok(value) - } - Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( - error, - ))), - }, - } + let value = map_source_result(self.source.read(mode))?; + self.pollee.notify_observers(Events::OUT); + Ok(value) } fn try_write(&self, value: u64) -> Result> { - match &self.backend { - EventCounterBackend::Local { counter } => { - let mut counter = counter.lock(); - if let Some(new_value) = (*counter) - .checked_add(value) - .filter(|count| *count <= MAX_EVENT_COUNT) - { - *counter = new_value; - drop(counter); - self.pollee.notify_observers(Events::IN); - return Ok(core::mem::size_of::()); - } - - Err(TryOpError::TryAgain) - } - EventCounterBackend::Broker { event } => match event.add(value) { - Ok(()) => { - self.pollee.notify_observers(Events::IN); - Ok(core::mem::size_of::()) - } - Err(BrokerObjectError::WouldBlock | BrokerObjectError::ResourceExhausted) => { - Err(TryOpError::TryAgain) - } - Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( - error, - ))), - }, - } + let size = map_write_result(self.source.write(value))?; + self.pollee.notify_observers(Events::IN); + Ok(size) } } -impl IOPollable for EventCounter { +impl IOPollable for EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ fn register_observer(&self, observer: Weak>, mask: Events) { self.pollee.register_observer(observer, mask); } fn check_io_events(&self) -> Events { - match &self.backend { - EventCounterBackend::Local { counter } => { - let counter = counter.lock(); - let mut events = Events::empty(); - if *counter != 0 { - events |= Events::IN; - } - if *counter < MAX_EVENT_COUNT { - events |= Events::OUT; - } - events - } - EventCounterBackend::Broker { event } => { - // The broker protocol currently exposes read readiness only. Keep - // write readiness optimistic and surface counter-limit failures - // from write until broker write-readiness plumbing exists. - let mut events = Events::OUT; - if event.is_read_ready().unwrap_or(false) { - events |= Events::IN; - } - events - } + self.source.check_io_events() + } +} + +impl EventCounterSource for LocalEventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn supports_blocking_operations(&self) -> bool { + true + } + + fn read(&self, mode: EventCounterConsumeMode) -> Result { + let mut counter = self.counter.lock(); + if *counter == 0 { + return Err(EventCounterError::WouldBlock); + } + + let res = match mode { + EventCounterConsumeMode::All => *counter, + EventCounterConsumeMode::One => 1, + }; + *counter -= res; + Ok(res) + } + + fn write(&self, value: u64) -> Result<(), EventCounterError> { + let mut counter = self.counter.lock(); + let new_value = (*counter) + .checked_add(value) + .filter(|count| *count <= MAX_EVENT_COUNT) + .ok_or(EventCounterError::WouldBlock)?; + *counter = new_value; + Ok(()) + } + + fn check_io_events(&self) -> Events { + let counter = self.counter.lock(); + let mut events = Events::empty(); + if *counter != 0 { + events |= Events::IN; } + if *counter < MAX_EVENT_COUNT { + events |= Events::OUT; + } + events } } -const fn broker_error_to_event_counter_error(error: BrokerObjectError) -> EventCounterError { - match error { - BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, - BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, - BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, - _ => EventCounterError::Io, +fn map_write_result( + result: Result<(), EventCounterError>, +) -> Result> { + map_source_result(result.map(|()| core::mem::size_of::())) +} + +fn map_source_result( + result: Result, +) -> Result> { + match result { + Ok(value) => Ok(value), + Err(EventCounterError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(error)), } } diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 6b54e898f..895e4ff86 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -8,6 +8,7 @@ pub mod polling; pub mod wait; mod counter; +pub(crate) use counter::EventCounterSource; pub use counter::{EventCounter, EventCounterConsumeMode, EventCounterError}; bitflags::bitflags! { diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index 315e9b4c3..f01e028f9 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -16,7 +16,6 @@ extern crate alloc; -pub mod broker; pub mod event; pub mod fd; pub mod fs; @@ -40,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 c312172ef..7ae207e36 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,7 +6,7 @@ use alloc::sync::Arc; use crate::{ - broker::BrokerControl, + broker::{BrokerControl, BrokerState}, event::{EventCounter, EventCounterError}, fd::Descriptors, platform::TimeProvider, @@ -68,7 +68,7 @@ impl LiteBox { crate::sync::lock_tracing::LockTracker::init(platform); let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); - let broker_control = RwLock::new(None); + let broker = BrokerState::new(); litebox_util_log::trace!("LiteBox instance initialized"); @@ -76,7 +76,7 @@ impl LiteBox { x: Arc::new(LiteBoxX { platform, descriptors, - broker_control, + broker, }), } } @@ -114,7 +114,7 @@ impl LiteBox { /// Installs broker control for broker-backed local-core objects. pub fn set_broker_control(&self, broker_control: Arc) { - *self.x.broker_control.write() = Some(broker_control); + self.x.broker.set_control(broker_control); } /// Creates an event counter, using broker backing when available and compatible. @@ -124,15 +124,11 @@ impl LiteBox { requires_blocking: bool, ) -> Result, EventCounterError> where - Platform: TimeProvider, + Platform: TimeProvider + 'static, { - if requires_blocking { - return EventCounter::new_local(initial_count); - } - let Some(broker) = self.x.broker_control.read().clone() else { - return EventCounter::new_local(initial_count); - }; - EventCounter::new_broker_from_control(broker, initial_count) + self.x + .broker + .create_event_counter(initial_count, requires_blocking) } } @@ -140,5 +136,5 @@ impl LiteBox { pub(crate) struct LiteBoxX { pub(crate) platform: &'static Platform, descriptors: RwLock>, - broker_control: RwLock>>, + broker: BrokerState, } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 93edc77eb..85071141f 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -9,7 +9,7 @@ use std::{ use alloc::sync::Arc; use anyhow::{Context as _, Result}; -use litebox::broker::{BrokerControl, BrokerControlError}; +use litebox::{BrokerControl, BrokerControlError}; use litebox_broker_client::{BrokerClient, BrokerClientWorker}; use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; use litebox_broker_unix_socket::UnixStreamClientControlChannel; From a2e465a87c28119dc0ed47c0a909663ff06190a6 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 15:19:09 -0700 Subject: [PATCH 25/66] Simplify broker event counter surface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker.rs | 172 +++++++++++------ litebox/src/event/counter.rs | 215 --------------------- litebox/src/event/mod.rs | 4 - litebox/src/lib.rs | 4 +- litebox/src/litebox.rs | 14 +- litebox_common_linux/src/errno/mod.rs | 12 +- litebox_shim_linux/src/syscalls/eventfd.rs | 123 ++++++++++-- 7 files changed, 235 insertions(+), 309 deletions(-) delete mode 100644 litebox/src/event/counter.rs diff --git a/litebox/src/broker.rs b/litebox/src/broker.rs index f2fdf7576..8b8ad3e4e 100644 --- a/litebox/src/broker.rs +++ b/litebox/src/broker.rs @@ -10,7 +10,10 @@ use litebox_broker_protocol::{ }; use crate::{ - event::{EventCounter, EventCounterConsumeMode, EventCounterError, EventCounterSource, Events}, + event::{ + Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, + wait::WaitContext, + }, platform::TimeProvider, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -32,6 +35,35 @@ pub trait BrokerControl: Send + Sync { ) -> core::result::Result; } +/// Errors returned by broker-backed 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, +} + +/// How an event counter read should consume readiness credits. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum EventCounterConsumeMode { + /// Consume all currently available credits. + All, + /// Consume one credit. + One, +} + +/// A broker-backed local-core event counter. +pub struct EventCounter { + event: BrokerEvent, + pollee: Pollee, +} + pub(crate) struct BrokerState { control: RwLock>>, } @@ -50,18 +82,14 @@ impl BrokerState { pub(crate) fn create_event_counter( &self, initial_count: u64, - requires_blocking: bool, - ) -> Result, EventCounterError> + ) -> Result>, EventCounterError> where - Platform: TimeProvider + 'static, + Platform: TimeProvider, { - if requires_blocking { - return EventCounter::new_local(initial_count); - } let Some(control) = self.control.read().clone() else { - return EventCounter::new_local(initial_count); + return Ok(None); }; - create_broker_event_counter(control, initial_count) + EventCounter::new(control, initial_count).map(Some) } } @@ -71,10 +99,6 @@ struct BrokerEvent { handle: ObjectHandle, } -struct BrokerEventCounter { - event: BrokerEvent, -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum BrokerObjectError { Control, @@ -85,6 +109,72 @@ enum BrokerObjectError { Internal, } +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn new(broker: Arc, initial_count: u64) -> Result { + let event = BrokerEvent::create(broker, initial_count) + .map_err(broker_error_to_event_counter_error)?; + Ok(Self { + event, + pollee: Pollee::new(), + }) + } + + /// Returns whether blocking reads and writes are supported. + pub fn supports_blocking_operations(&self) -> bool { + false + } + + /// Reads the event counter. + pub fn read( + &self, + _cx: &WaitContext<'_, Platform>, + _nonblock: bool, + mode: EventCounterConsumeMode, + ) -> Result> { + let value = map_broker_result(self.event.consume(mode))?; + self.pollee.notify_observers(Events::OUT); + Ok(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)); + } + map_broker_result(self.event.add(value))?; + self.pollee.notify_observers(Events::IN); + Ok(core::mem::size_of::()) + } +} + +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 { + // The broker protocol currently exposes read readiness only. Keep write + // readiness optimistic and surface counter-limit failures from write + // until broker write-readiness plumbing exists. + let mut events = Events::OUT; + if self.event.is_read_ready().unwrap_or(false) { + events |= Events::IN; + } + events + } +} + impl BrokerEvent { fn create( broker: Arc, @@ -135,50 +225,6 @@ impl BrokerEvent { } } -impl EventCounterSource for BrokerEventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - fn supports_blocking_operations(&self) -> bool { - false - } - - fn read(&self, mode: EventCounterConsumeMode) -> Result { - self.event - .consume(mode) - .map_err(broker_error_to_event_counter_error) - } - - fn write(&self, value: u64) -> Result<(), EventCounterError> { - self.event - .add(value) - .map_err(broker_error_to_event_counter_error) - } - - fn check_io_events(&self) -> Events { - // The broker protocol currently exposes read readiness only. Keep write - // readiness optimistic and surface counter-limit failures from write - // until broker write-readiness plumbing exists. - let mut events = Events::OUT; - if self.event.is_read_ready().unwrap_or(false) { - events |= Events::IN; - } - events - } -} - -fn create_broker_event_counter( - broker: Arc, - initial_count: u64, -) -> Result, EventCounterError> -where - Platform: RawSyncPrimitivesProvider + TimeProvider + 'static, -{ - let event = - BrokerEvent::create(broker, initial_count).map_err(broker_error_to_event_counter_error)?; - Ok(EventCounter::from_source(BrokerEventCounter { event })) -} - fn request_event( broker: &Arc, request: EventRequest, @@ -211,6 +257,18 @@ const fn error_to_object_error(error: ErrorCode) -> BrokerObjectError { } } +fn map_broker_result( + result: Result, +) -> Result> { + match result { + Ok(value) => Ok(value), + Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), + Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( + error, + ))), + } +} + const fn broker_error_to_event_counter_error(error: BrokerObjectError) -> EventCounterError { match error { BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs deleted file mode 100644 index 29637de75..000000000 --- a/litebox/src/event/counter.rs +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use alloc::{boxed::Box, sync::Weak}; - -use super::{ - Events, IOPollable, - observer::Observer, - polling::{Pollee, TryOpError}, - wait::WaitContext, -}; -use crate::{ - platform::TimeProvider, - sync::{Mutex, RawSyncPrimitivesProvider}, -}; - -const MAX_EVENT_COUNT: u64 = u64::MAX - 1; - -/// 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, -} - -/// How an event counter read should consume readiness credits. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EventCounterConsumeMode { - /// Consume all currently available credits. - All, - /// Consume one credit. - One, -} - -/// A local-core event counter. -/// -/// This object provides eventfd-like counter semantics without exposing whether -/// the backing state is local-private or externally mediated. -pub struct EventCounter { - source: Box>, - pollee: Pollee, -} - -pub(crate) trait EventCounterSource: Send + Sync -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - fn supports_blocking_operations(&self) -> bool; - fn read(&self, mode: EventCounterConsumeMode) -> Result; - fn write(&self, value: u64) -> Result<(), EventCounterError>; - fn check_io_events(&self) -> Events; -} - -struct LocalEventCounter { - counter: Mutex, -} - -impl EventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider + 'static, -{ - pub(crate) fn new_local(initial_count: u64) -> Result { - if initial_count > MAX_EVENT_COUNT { - return Err(EventCounterError::ResourceExhausted); - } - Ok(Self::from_source(LocalEventCounter { - counter: Mutex::new(initial_count), - })) - } - - pub(crate) fn from_source(source: impl EventCounterSource + 'static) -> Self { - Self { - source: Box::new(source), - pollee: Pollee::new(), - } - } -} - -impl EventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - /// Returns whether blocking reads and writes are supported. - pub fn supports_blocking_operations(&self) -> bool { - self.source.supports_blocking_operations() - } - - /// Reads the event counter, optionally blocking until the counter is ready. - pub fn read( - &self, - cx: &WaitContext<'_, Platform>, - nonblock: bool, - mode: EventCounterConsumeMode, - ) -> Result> { - if !self.supports_blocking_operations() { - return self.try_read(mode); - } - self.pollee - .wait(cx, nonblock, Events::IN, || self.try_read(mode)) - } - - /// Writes readiness credits to the event counter, optionally blocking until - /// the counter can accept them. - pub fn write( - &self, - cx: &WaitContext<'_, Platform>, - nonblock: bool, - value: u64, - ) -> Result> { - if value == u64::MAX { - return Err(TryOpError::Other(EventCounterError::InvalidInput)); - } - if !self.supports_blocking_operations() { - return self.try_write(value); - } - self.pollee - .wait(cx, nonblock, Events::OUT, || self.try_write(value)) - } - - fn try_read( - &self, - mode: EventCounterConsumeMode, - ) -> Result> { - let value = map_source_result(self.source.read(mode))?; - self.pollee.notify_observers(Events::OUT); - Ok(value) - } - - fn try_write(&self, value: u64) -> Result> { - let size = map_write_result(self.source.write(value))?; - self.pollee.notify_observers(Events::IN); - Ok(size) - } -} - -impl IOPollable for EventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - fn register_observer(&self, observer: Weak>, mask: Events) { - self.pollee.register_observer(observer, mask); - } - - fn check_io_events(&self) -> Events { - self.source.check_io_events() - } -} - -impl EventCounterSource for LocalEventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - fn supports_blocking_operations(&self) -> bool { - true - } - - fn read(&self, mode: EventCounterConsumeMode) -> Result { - let mut counter = self.counter.lock(); - if *counter == 0 { - return Err(EventCounterError::WouldBlock); - } - - let res = match mode { - EventCounterConsumeMode::All => *counter, - EventCounterConsumeMode::One => 1, - }; - *counter -= res; - Ok(res) - } - - fn write(&self, value: u64) -> Result<(), EventCounterError> { - let mut counter = self.counter.lock(); - let new_value = (*counter) - .checked_add(value) - .filter(|count| *count <= MAX_EVENT_COUNT) - .ok_or(EventCounterError::WouldBlock)?; - *counter = new_value; - Ok(()) - } - - fn check_io_events(&self) -> Events { - let counter = self.counter.lock(); - let mut events = Events::empty(); - if *counter != 0 { - events |= Events::IN; - } - if *counter < MAX_EVENT_COUNT { - events |= Events::OUT; - } - events - } -} - -fn map_write_result( - result: Result<(), EventCounterError>, -) -> Result> { - map_source_result(result.map(|()| core::mem::size_of::())) -} - -fn map_source_result( - result: Result, -) -> Result> { - match result { - Ok(value) => Ok(value), - Err(EventCounterError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(error)), - } -} diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 895e4ff86..24d5b6832 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -7,10 +7,6 @@ pub mod observer; pub mod polling; pub mod wait; -mod counter; -pub(crate) use counter::EventCounterSource; -pub use counter::{EventCounter, EventCounterConsumeMode, EventCounterError}; - bitflags::bitflags! { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Events: u32 { diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index f01e028f9..b4a53dd6b 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -41,4 +41,6 @@ mod utilities; pub mod utils; mod broker; -pub use broker::{BrokerControl, BrokerControlError}; +pub use broker::{ + BrokerControl, BrokerControlError, EventCounter, EventCounterConsumeMode, EventCounterError, +}; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 7ae207e36..5efbaa1e2 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,8 +6,7 @@ use alloc::sync::Arc; use crate::{ - broker::{BrokerControl, BrokerState}, - event::{EventCounter, EventCounterError}, + broker::{BrokerControl, BrokerState, EventCounter, EventCounterError}, fd::Descriptors, platform::TimeProvider, sync::{RawSyncPrimitivesProvider, RwLock}, @@ -117,18 +116,15 @@ impl LiteBox { self.x.broker.set_control(broker_control); } - /// Creates an event counter, using broker backing when available and compatible. + /// Creates a broker-backed event counter when broker control is installed. pub fn create_event_counter( &self, initial_count: u64, - requires_blocking: bool, - ) -> Result, EventCounterError> + ) -> Result>, EventCounterError> where - Platform: TimeProvider + 'static, + Platform: TimeProvider, { - self.x - .broker - .create_event_counter(initial_count, requires_blocking) + self.x.broker.create_event_counter(initial_count) } } diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 0096e1e91..1072ae00c 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,13 +536,13 @@ where } } -impl From for Errno { - fn from(value: litebox::event::EventCounterError) -> Self { +impl From for Errno { + fn from(value: litebox::EventCounterError) -> Self { match value { - litebox::event::EventCounterError::InvalidInput => Errno::EINVAL, - litebox::event::EventCounterError::WouldBlock - | litebox::event::EventCounterError::ResourceExhausted => Errno::EAGAIN, - litebox::event::EventCounterError::Io => Errno::EIO, + litebox::EventCounterError::InvalidInput => Errno::EINVAL, + litebox::EventCounterError::WouldBlock + | litebox::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::EventCounterError::Io => Errno::EIO, _ => Errno::EIO, } } diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 1ef8b6855..07da15391 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -6,15 +6,15 @@ use core::sync::atomic::AtomicU32; use litebox::{ - LiteBox, + EventCounter, EventCounterConsumeMode, LiteBox, event::{ - EventCounter, EventCounterConsumeMode, Events, IOPollable, observer::Observer, + Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, platform::TimeProvider, - sync::RawSyncPrimitivesProvider, + sync::{Mutex, RawSyncPrimitivesProvider}, }; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::Platform; @@ -26,10 +26,12 @@ impl FdEnabledSubsystem for EventfdSubsystem { impl FdEnabledSubsystemEntry for EventFile {} pub(crate) struct EventFile { - event: EventCounter, + local_counter: Mutex, + local_core_event: Option>, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, + pollee: Pollee, } impl EventFile { @@ -40,31 +42,97 @@ impl EventFile { ) -> Result { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); - let event = litebox - .create_event_counter(count, !flags.contains(EfdFlags::NONBLOCK)) - .map_err(Errno::from)?; + let local_core_event = if flags.contains(EfdFlags::NONBLOCK) { + litebox.create_event_counter(count).map_err(Errno::from)? + } else { + None + }; Ok(Self { - event, + local_counter: Mutex::new(count), + local_core_event, status: AtomicU32::new(status.bits()), semaphore: flags.contains(EfdFlags::SEMAPHORE), + pollee: Pollee::new(), }) } - pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - let mode = if self.semaphore { + fn consume_mode(&self) -> EventCounterConsumeMode { + if self.semaphore { EventCounterConsumeMode::One } else { EventCounterConsumeMode::All + } + } + + fn try_read(&self) -> Result> { + let mut counter = self.local_counter.lock(); + if *counter == 0 { + return Err(TryOpError::TryAgain); + } + + let res = match self.consume_mode() { + EventCounterConsumeMode::All => *counter, + EventCounterConsumeMode::One => 1, }; - self.event - .read(cx, self.get_status().contains(OFlags::NONBLOCK), mode) + *counter -= res; + + drop(counter); + self.pollee.notify_observers(Events::OUT); + Ok(res) + } + + pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { + if let Some(event) = &self.local_core_event { + return event + .read( + cx, + self.get_status().contains(OFlags::NONBLOCK), + self.consume_mode(), + ) + .map_err(Errno::from); + } + 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> { + if value == u64::MAX { + return Err(TryOpError::Other(Errno::EINVAL)); + } + + let mut counter = self.local_counter.lock(); + if let Some(new_value) = (*counter).checked_add(value) + && new_value != u64::MAX + { + *counter = new_value; + drop(counter); + self.pollee.notify_observers(Events::IN); + return Ok(core::mem::size_of::()); + } + + Err(TryOpError::TryAgain) + } + pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - self.event - .write(cx, self.get_status().contains(OFlags::NONBLOCK), value) + if let Some(event) = &self.local_core_event { + return event + .write(cx, self.get_status().contains(OFlags::NONBLOCK), value) + .map_err(Errno::from); + } + self.pollee + .wait( + cx, + self.get_status().contains(OFlags::NONBLOCK), + Events::OUT, + || self.try_write(value), + ) .map_err(Errno::from) } @@ -72,7 +140,12 @@ impl EventFile { pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { let new_status = (self.get_status() & mask.complement()) | (requested & mask); - if !new_status.contains(OFlags::NONBLOCK) && !self.event.supports_blocking_operations() { + if !new_status.contains(OFlags::NONBLOCK) + && self + .local_core_event + .as_ref() + .is_some_and(|event| !event.supports_blocking_operations()) + { return Err(Errno::EINVAL); } self.set_status(requested & mask, true); @@ -83,11 +156,27 @@ impl EventFile { impl IOPollable for EventFile { fn check_io_events(&self) -> Events { - self.event.check_io_events() + if let Some(event) = &self.local_core_event { + return event.check_io_events(); + } + + let counter = self.local_counter.lock(); + let mut events = Events::empty(); + if *counter != 0 { + events |= Events::IN; + } + if *counter < u64::MAX - 1 { + events |= Events::OUT; + } + events } fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - self.event.register_observer(observer, mask); + if let Some(event) = &self.local_core_event { + event.register_observer(observer, mask); + } else { + self.pollee.register_observer(observer, mask); + } } } From 03b2764aa5b5a476521378e7f79c94104ea54c74 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 15:27:50 -0700 Subject: [PATCH 26/66] Split local core broker adapters Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../{broker.rs => broker/event_counter.rs} | 34 +------------- litebox/src/broker/mod.rs | 44 +++++++++++++++++++ 2 files changed, 46 insertions(+), 32 deletions(-) rename litebox/src/{broker.rs => broker/event_counter.rs} (87%) create mode 100644 litebox/src/broker/mod.rs diff --git a/litebox/src/broker.rs b/litebox/src/broker/event_counter.rs similarity index 87% rename from litebox/src/broker.rs rename to litebox/src/broker/event_counter.rs index 8b8ad3e4e..d8c984c93 100644 --- a/litebox/src/broker.rs +++ b/litebox/src/broker/event_counter.rs @@ -9,32 +9,16 @@ use litebox_broker_protocol::{ WaitEventRequest, WaitOutcome, }; +use super::{BrokerControl, BrokerState}; use crate::{ event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, wait::WaitContext, }, platform::TimeProvider, - sync::{RawSyncPrimitivesProvider, RwLock}, + sync::RawSyncPrimitivesProvider, }; -/// Error returned by the deployment-provided broker control path. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct 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 broker request and returns its response. - fn request( - &self, - request: BrokerRequest, - ) -> core::result::Result; -} - /// Errors returned by broker-backed local-core event counters. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -64,21 +48,7 @@ pub struct EventCounter { pollee: Pollee, } -pub(crate) struct BrokerState { - control: RwLock>>, -} - impl BrokerState { - pub(crate) fn new() -> Self { - Self { - control: RwLock::new(None), - } - } - - pub(crate) fn set_control(&self, broker_control: Arc) { - *self.control.write() = Some(broker_control); - } - pub(crate) fn create_event_counter( &self, initial_count: u64, diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs new file mode 100644 index 000000000..1a1524d50 --- /dev/null +++ b/litebox/src/broker/mod.rs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use alloc::sync::Arc; + +use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; + +use crate::sync::{RawSyncPrimitivesProvider, RwLock}; + +mod event_counter; +pub use event_counter::{EventCounter, EventCounterConsumeMode, EventCounterError}; + +/// Error returned by the deployment-provided broker control path. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct 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 broker request and returns its response. + fn request( + &self, + request: BrokerRequest, + ) -> core::result::Result; +} + +pub(crate) struct BrokerState { + control: RwLock>>, +} + +impl BrokerState { + pub(crate) fn new() -> Self { + Self { + control: RwLock::new(None), + } + } + + pub(crate) fn set_control(&self, broker_control: Arc) { + *self.control.write() = Some(broker_control); + } +} From bc82f18f55813d08ba9932a56dd6444db3031421 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Tue, 2 Jun 2026 16:04:15 -0700 Subject: [PATCH 27/66] Clean up broker event layering Narrow the local-core broker control boundary to CoreRequest/CoreResponse and let the runner handle full broker envelopes. Reuse the shared protocol EventConsumeMode for broker-backed event counters and keep event-specific adaptation in the private broker event module. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/broker/{event_counter.rs => event.rs} | 38 +++++++------------ litebox/src/broker/mod.rs | 23 +++++++---- litebox/src/lib.rs | 2 +- litebox_runner_linux_userland/src/broker.rs | 18 ++++++--- litebox_shim_linux/src/syscalls/eventfd.rs | 13 +++---- 5 files changed, 48 insertions(+), 46 deletions(-) rename litebox/src/broker/{event_counter.rs => event.rs} (85%) diff --git a/litebox/src/broker/event_counter.rs b/litebox/src/broker/event.rs similarity index 85% rename from litebox/src/broker/event_counter.rs rename to litebox/src/broker/event.rs index d8c984c93..4ea2ab0b7 100644 --- a/litebox/src/broker/event_counter.rs +++ b/litebox/src/broker/event.rs @@ -4,12 +4,11 @@ use alloc::sync::Arc; use litebox_broker_protocol::{ - AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, CoreRequest, CoreResponse, - CreateEventRequest, ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, - WaitEventRequest, WaitOutcome, + AddEventRequest, ConsumeEventRequest, CoreRequest, CoreResponse, CreateEventRequest, ErrorCode, + EventRequest, EventResponse, ObjectHandle, WaitEventRequest, WaitOutcome, }; -use super::{BrokerControl, BrokerState}; +use super::{BrokerControl, BrokerControlError, BrokerState, EventConsumeMode}; use crate::{ event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, @@ -33,15 +32,6 @@ pub enum EventCounterError { Io, } -/// How an event counter read should consume readiness credits. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EventCounterConsumeMode { - /// Consume all currently available credits. - All, - /// Consume one credit. - One, -} - /// A broker-backed local-core event counter. pub struct EventCounter { event: BrokerEvent, @@ -102,7 +92,7 @@ where &self, _cx: &WaitContext<'_, Platform>, _nonblock: bool, - mode: EventCounterConsumeMode, + mode: EventConsumeMode, ) -> Result> { let value = map_broker_result(self.event.consume(mode))?; self.pollee.notify_observers(Events::OUT); @@ -163,10 +153,10 @@ impl BrokerEvent { }) } - fn consume(&self, mode: EventCounterConsumeMode) -> Result { + fn consume(&self, mode: EventConsumeMode) -> Result { let response = self.request(EventRequest::Consume(ConsumeEventRequest::new( self.handle, - to_protocol_consume_mode(mode), + mode, )))?; let EventResponse::Consume(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); @@ -200,19 +190,19 @@ fn request_event( request: EventRequest, ) -> Result { let response = broker - .request(BrokerRequest::Core(CoreRequest::Event(request))) - .map_err(|_| BrokerObjectError::Control)?; + .request(CoreRequest::Event(request)) + .map_err(control_error_to_object_error)?; match response { - BrokerResponse::Core(CoreResponse::Event(response)) => Ok(response), - BrokerResponse::Error(error) => Err(error_to_object_error(error)), + CoreResponse::Event(response) => Ok(response), _ => Err(BrokerObjectError::UnexpectedResponse), } } -const fn to_protocol_consume_mode(mode: EventCounterConsumeMode) -> EventConsumeMode { - match mode { - EventCounterConsumeMode::All => EventConsumeMode::All, - EventCounterConsumeMode::One => EventConsumeMode::One, +const fn control_error_to_object_error(error: BrokerControlError) -> BrokerObjectError { + match error { + BrokerControlError::Transport => BrokerObjectError::Control, + BrokerControlError::Broker(error) => error_to_object_error(error), + BrokerControlError::UnexpectedResponse => BrokerObjectError::UnexpectedResponse, } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 1a1524d50..2e759f2a9 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -3,16 +3,25 @@ use alloc::sync::Arc; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; +use litebox_broker_protocol::{CoreRequest, CoreResponse, ErrorCode}; use crate::sync::{RawSyncPrimitivesProvider, RwLock}; -mod event_counter; -pub use event_counter::{EventCounter, EventCounterConsumeMode, EventCounterError}; +mod event; +pub use event::{EventCounter, EventCounterError}; +pub use litebox_broker_protocol::EventConsumeMode; /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct BrokerControlError; +#[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, +} /// Local-core access to the negotiated broker control channel. /// @@ -20,11 +29,11 @@ pub struct BrokerControlError; /// requests. Deployment code owns endpoint selection and supplies the connected /// transport behind this protocol-level boundary. pub trait BrokerControl: Send + Sync { - /// Sends one active broker request and returns its response. + /// Sends one active BrokerCore request and returns its response. fn request( &self, - request: BrokerRequest, - ) -> core::result::Result; + request: CoreRequest, + ) -> core::result::Result; } pub(crate) struct BrokerState { diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index b4a53dd6b..fcde2e28c 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -42,5 +42,5 @@ pub mod utils; mod broker; pub use broker::{ - BrokerControl, BrokerControlError, EventCounter, EventCounterConsumeMode, EventCounterError, + BrokerControl, BrokerControlError, EventConsumeMode, EventCounter, EventCounterError, }; diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 85071141f..e1479e8f5 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -11,7 +11,7 @@ use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox::{BrokerControl, BrokerControlError}; use litebox_broker_client::{BrokerClient, BrokerClientWorker}; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse}; +use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; use litebox_broker_unix_socket::UnixStreamClientControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -66,11 +66,17 @@ impl BrokerControlClient { impl BrokerControl for BrokerControlClient { fn request( &self, - request: BrokerRequest, - ) -> core::result::Result { - self.worker - .active_raw_request(request) - .map_err(|_| BrokerControlError) + request: CoreRequest, + ) -> core::result::Result { + match self + .worker + .active_raw_request(BrokerRequest::Core(request)) + .map_err(|_| BrokerControlError::Transport)? + { + BrokerResponse::Core(response) => Ok(response), + BrokerResponse::Error(error) => Err(BrokerControlError::Broker(error)), + _ => Err(BrokerControlError::UnexpectedResponse), + } } } diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 07da15391..1cb689abf 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -6,7 +6,7 @@ use core::sync::atomic::AtomicU32; use litebox::{ - EventCounter, EventCounterConsumeMode, LiteBox, + EventConsumeMode, EventCounter, LiteBox, event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, wait::WaitContext, @@ -57,11 +57,11 @@ impl EventFile { }) } - fn consume_mode(&self) -> EventCounterConsumeMode { + fn consume_mode(&self) -> EventConsumeMode { if self.semaphore { - EventCounterConsumeMode::One + EventConsumeMode::One } else { - EventCounterConsumeMode::All + EventConsumeMode::All } } @@ -71,10 +71,7 @@ impl EventFile { return Err(TryOpError::TryAgain); } - let res = match self.consume_mode() { - EventCounterConsumeMode::All => *counter, - EventCounterConsumeMode::One => 1, - }; + let res = if self.semaphore { 1 } else { *counter }; *counter -= res; drop(counter); From 6da49eed155f76da1331bf756442a138f306686c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 09:24:13 -0700 Subject: [PATCH 28/66] Simplify broker event adapter Collapse the broker-backed EventCounter wrapper so it directly owns the broker control and object handle. Move broker adapter errors into a private broker error module and tighten broker-backed event readiness notifications. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 85 ++++++++++ litebox/src/broker/event.rs | 303 ++++++++++++++++++++++-------------- litebox/src/broker/mod.rs | 18 +-- 3 files changed, 276 insertions(+), 130 deletions(-) create mode 100644 litebox/src/broker/error.rs diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs new file mode 100644 index 000000000..2c668039e --- /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::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, +} + +/// Errors returned by broker-backed 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, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(super) enum BrokerObjectError { + Control, + InvalidObject, + WouldBlock, + ResourceExhausted, + UnexpectedResponse, + Internal, +} + +pub(super) const fn control_error_to_object_error(error: BrokerControlError) -> BrokerObjectError { + match error { + BrokerControlError::Transport => BrokerObjectError::Control, + BrokerControlError::Broker(error) => error_code_to_object_error(error), + BrokerControlError::UnexpectedResponse => BrokerObjectError::UnexpectedResponse, + } +} + +const fn error_code_to_object_error(error: ErrorCode) -> BrokerObjectError { + match error { + ErrorCode::InvalidRights + | ErrorCode::UnknownObject + | ErrorCode::WrongObjectType + | ErrorCode::StaleHandle => BrokerObjectError::InvalidObject, + ErrorCode::WouldBlock => BrokerObjectError::WouldBlock, + ErrorCode::ResourceExhausted => BrokerObjectError::ResourceExhausted, + _ => BrokerObjectError::Internal, + } +} + +pub(super) 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(object_error_to_event_counter_error( + error, + ))), + } +} + +pub(super) const fn object_error_to_event_counter_error( + error: BrokerObjectError, +) -> EventCounterError { + match error { + BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, + BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, + BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, + _ => EventCounterError::Io, + } +} diff --git a/litebox/src/broker/event.rs b/litebox/src/broker/event.rs index 4ea2ab0b7..5cac5a775 100644 --- a/litebox/src/broker/event.rs +++ b/litebox/src/broker/event.rs @@ -4,11 +4,18 @@ use alloc::sync::Arc; use litebox_broker_protocol::{ - AddEventRequest, ConsumeEventRequest, CoreRequest, CoreResponse, CreateEventRequest, ErrorCode, - EventRequest, EventResponse, ObjectHandle, WaitEventRequest, WaitOutcome, + AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, + CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, + WaitEventRequest, WaitOutcome, }; -use super::{BrokerControl, BrokerControlError, BrokerState, EventConsumeMode}; +use super::{ + BrokerControl, BrokerState, EventConsumeMode, + error::{ + BrokerObjectError, EventCounterError, control_error_to_object_error, + map_broker_object_result, object_error_to_event_counter_error, + }, +}; use crate::{ event::{ Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, @@ -18,23 +25,10 @@ use crate::{ sync::RawSyncPrimitivesProvider, }; -/// Errors returned by broker-backed 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, -} - /// A broker-backed local-core event counter. pub struct EventCounter { - event: BrokerEvent, + broker: Arc, + handle: ObjectHandle, pollee: Pollee, } @@ -53,31 +47,22 @@ impl BrokerState { } } -#[derive(Clone)] -struct BrokerEvent { - broker: Arc, - handle: ObjectHandle, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum BrokerObjectError { - Control, - InvalidObject, - WouldBlock, - ResourceExhausted, - UnexpectedResponse, - Internal, -} - impl EventCounter where Platform: RawSyncPrimitivesProvider + TimeProvider, { fn new(broker: Arc, initial_count: u64) -> Result { - let event = BrokerEvent::create(broker, initial_count) - .map_err(broker_error_to_event_counter_error)?; + let response = request_event( + &broker, + EventRequest::Create(CreateEventRequest::new(initial_count)), + ) + .map_err(object_error_to_event_counter_error)?; + let EventResponse::Create(response) = response else { + return Err(EventCounterError::Io); + }; Ok(Self { - event, + broker, + handle: response.handle, pollee: Pollee::new(), }) } @@ -94,9 +79,8 @@ where _nonblock: bool, mode: EventConsumeMode, ) -> Result> { - let value = map_broker_result(self.event.consume(mode))?; - self.pollee.notify_observers(Events::OUT); - Ok(value) + let response = map_broker_object_result(self.consume(mode))?; + Ok(response.value) } /// Writes readiness credits to the event counter. @@ -109,51 +93,14 @@ where if value == u64::MAX { return Err(TryOpError::Other(EventCounterError::InvalidInput)); } - map_broker_result(self.event.add(value))?; - self.pollee.notify_observers(Events::IN); - Ok(core::mem::size_of::()) - } -} - -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 { - // The broker protocol currently exposes read readiness only. Keep write - // readiness optimistic and surface counter-limit failures from write - // until broker write-readiness plumbing exists. - let mut events = Events::OUT; - if self.event.is_read_ready().unwrap_or(false) { - events |= Events::IN; + let readiness = map_broker_object_result(self.add(value))?; + if value != 0 && readiness.ready { + self.pollee.notify_observers(Events::IN); } - events - } -} - -impl BrokerEvent { - fn create( - broker: Arc, - initial_count: u64, - ) -> Result { - let response = request_event( - &broker, - EventRequest::Create(CreateEventRequest::new(initial_count)), - )?; - let EventResponse::Create(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse); - }; - Ok(Self { - broker, - handle: response.handle, - }) + Ok(core::mem::size_of::()) } - fn consume(&self, mode: EventConsumeMode) -> Result { + fn consume(&self, mode: EventConsumeMode) -> Result { let response = self.request(EventRequest::Consume(ConsumeEventRequest::new( self.handle, mode, @@ -161,15 +108,15 @@ impl BrokerEvent { let EventResponse::Consume(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); }; - Ok(response.value) + Ok(response) } - fn add(&self, value: u64) -> Result<(), BrokerObjectError> { + fn add(&self, value: u64) -> Result { let response = self.request(EventRequest::Add(AddEventRequest::new(self.handle, value)))?; - let EventResponse::Add(_) = response else { + let EventResponse::Add(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); }; - Ok(()) + Ok(response.readiness) } fn is_read_ready(&self) -> Result { @@ -185,6 +132,26 @@ impl BrokerEvent { } } +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 { + // The broker protocol currently exposes read readiness only. Keep write + // readiness optimistic and surface counter-limit failures from write + // until broker write-readiness plumbing exists. + let mut events = Events::OUT; + if self.is_read_ready().unwrap_or(false) { + events |= Events::IN; + } + events + } +} + fn request_event( broker: &Arc, request: EventRequest, @@ -198,42 +165,146 @@ fn request_event( } } -const fn control_error_to_object_error(error: BrokerControlError) -> BrokerObjectError { - match error { - BrokerControlError::Transport => BrokerObjectError::Control, - BrokerControlError::Broker(error) => error_to_object_error(error), - BrokerControlError::UnexpectedResponse => BrokerObjectError::UnexpectedResponse, +#[cfg(test)] +mod tests { + extern crate std; + + use alloc::sync::Arc; + use core::sync::atomic::{AtomicUsize, Ordering}; + use std::{collections::VecDeque, sync::Mutex}; + + use litebox_broker_protocol::{ + AddEventResponse, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventResponse, + EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, + }; + + use super::*; + use crate::{ + BrokerControlError, + event::{Events, IOPollable, observer::Observer, wait::WaitState}, + platform::mock::MockPlatform, + }; + + struct MockBrokerControl { + responses: Mutex>, } -} -const fn error_to_object_error(error: ErrorCode) -> BrokerObjectError { - match error { - ErrorCode::InvalidRights | ErrorCode::WrongObjectType | ErrorCode::StaleHandle => { - BrokerObjectError::InvalidObject + impl MockBrokerControl { + fn new(responses: impl IntoIterator) -> Self { + Self { + responses: Mutex::new(responses.into_iter().collect()), + } } - ErrorCode::WouldBlock => BrokerObjectError::WouldBlock, - ErrorCode::ResourceExhausted => BrokerObjectError::ResourceExhausted, - _ => BrokerObjectError::Internal, } -} -fn map_broker_result( - result: Result, -) -> Result> { - match result { - Ok(value) => Ok(value), - Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(broker_error_to_event_counter_error( - error, - ))), + impl BrokerControl for MockBrokerControl { + fn request( + &self, + _request: CoreRequest, + ) -> core::result::Result { + self.responses + .lock() + .unwrap() + .pop_front() + .ok_or(BrokerControlError::UnexpectedResponse) + } } -} -const fn broker_error_to_event_counter_error(error: BrokerObjectError) -> EventCounterError { - match error { - BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, - BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, - BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, - _ => EventCounterError::Io, + struct CountingObserver { + notifications: AtomicUsize, + } + + impl CountingObserver { + fn new() -> Self { + Self { + notifications: AtomicUsize::new(0), + } + } + + fn notifications(&self) -> usize { + self.notifications.load(Ordering::Relaxed) + } + } + + impl Observer for CountingObserver { + fn on_events(&self, _events: &Events) { + self.notifications.fetch_add(1, Ordering::Relaxed); + } + } + + fn sample_handle() -> ObjectHandle { + ObjectHandle::new(ObjectReferenceId::new(1), ObjectReferenceGeneration::new(1)) + } + + fn create_response() -> CoreResponse { + CoreResponse::Event(EventResponse::Create(CreateEventResponse::new( + sample_handle(), + ))) + } + + fn add_response(ready: bool) -> CoreResponse { + CoreResponse::Event(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(ready, 1), + ))) + } + + fn consume_response(value: u64, ready: bool) -> CoreResponse { + CoreResponse::Event(EventResponse::Consume(ConsumeEventResponse::new( + value, + ReadinessState::new(ready, 1), + ))) + } + + fn event_counter( + responses: impl IntoIterator, + ) -> EventCounter { + EventCounter::new(Arc::new(MockBrokerControl::new(responses)), 0).unwrap() + } + + fn observer() -> (Arc, Arc>) { + let observer = Arc::new(CountingObserver::new()); + let observer_dyn: Arc> = observer.clone(); + (observer, observer_dyn) + } + + #[test] + fn zero_write_does_not_notify_read_observers() { + let event = event_counter([create_response(), add_response(false)]); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!(event.write(&wait.context(), true, 0).unwrap(), 8); + + assert_eq!(observer.notifications(), 0); + } + + #[test] + fn nonzero_write_notifies_read_observers_when_broker_reports_ready() { + let event = event_counter([create_response(), add_response(true)]); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!(event.write(&wait.context(), true, 1).unwrap(), 8); + + assert_eq!(observer.notifications(), 1); + } + + #[test] + fn read_does_not_notify_write_observers_without_broker_write_readiness() { + let event = event_counter([create_response(), consume_response(1, false)]); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!( + event + .read(&wait.context(), true, EventConsumeMode::All) + .unwrap(), + 1 + ); + + assert_eq!(observer.notifications(), 0); } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 2e759f2a9..934541f8a 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -3,26 +3,16 @@ use alloc::sync::Arc; -use litebox_broker_protocol::{CoreRequest, CoreResponse, ErrorCode}; +use litebox_broker_protocol::{CoreRequest, CoreResponse}; use crate::sync::{RawSyncPrimitivesProvider, RwLock}; +mod error; mod event; -pub use event::{EventCounter, EventCounterError}; +pub use error::{BrokerControlError, EventCounterError}; +pub use event::EventCounter; pub use litebox_broker_protocol::EventConsumeMode; -/// 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, -} - /// Local-core access to the negotiated broker control channel. /// /// LiteBox owns broker-backed local objects and constructs broker protocol From c91bebd76755baf1706f7bed76afdf61a58c2605 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 10:27:30 -0700 Subject: [PATCH 29/66] Refine broker-backed event counter interface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/impl-plan.md | 2 +- litebox/src/broker/error.rs | 79 +++++----- litebox/src/broker/event.rs | 148 +++++++++++++----- litebox/src/broker/mod.rs | 20 +-- litebox/src/event/counter.rs | 111 +++++++++++++ litebox/src/event/mod.rs | 3 + litebox/src/lib.rs | 5 +- litebox/src/litebox.rs | 38 +++-- litebox/src/objects.rs | 47 ++++++ litebox_broker_client/src/lib.rs | 2 +- litebox_broker_core/src/event.rs | 8 +- litebox_broker_protocol/src/event.rs | 20 ++- litebox_broker_server/src/server.rs | 8 +- .../tests/userland_broker.rs | 6 +- litebox_broker_wire/src/lib.rs | 22 +-- litebox_common_linux/src/errno/mod.rs | 16 +- litebox_runner_linux_userland/src/lib.rs | 19 ++- litebox_runner_linux_userland/tests/run.rs | 4 +- litebox_shim_linux/src/lib.rs | 11 +- litebox_shim_linux/src/syscalls/eventfd.rs | 64 ++------ litebox_shim_linux/src/syscalls/file.rs | 8 +- litebox_shim_linux/src/syscalls/tests.rs | 7 +- 22 files changed, 429 insertions(+), 219 deletions(-) create mode 100644 litebox/src/event/counter.rs create mode 100644 litebox/src/objects.rs diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 3a5cb3d6e..50a34c6a6 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -95,7 +95,7 @@ Initial scope: Exit criteria: -- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, and installs broker control on the `LiteBox` local core before starting the guest. +- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated objects through the local-core object factory namespace rather than through broker-specific APIs. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 2c668039e..991fa12f0 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -3,7 +3,7 @@ use litebox_broker_protocol::ErrorCode; -use crate::event::polling::TryOpError; +use crate::event::{counter::EventCounterError, polling::TryOpError}; /// Error returned by the deployment-provided broker control path. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -17,47 +17,47 @@ pub enum BrokerControlError { UnexpectedResponse, } -/// Errors returned by broker-backed 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, -} - +/// 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(super) 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, } -pub(super) const fn control_error_to_object_error(error: BrokerControlError) -> BrokerObjectError { - match error { - BrokerControlError::Transport => BrokerObjectError::Control, - BrokerControlError::Broker(error) => error_code_to_object_error(error), - BrokerControlError::UnexpectedResponse => BrokerObjectError::UnexpectedResponse, +impl From for BrokerObjectError { + fn from(error: BrokerControlError) -> Self { + match error { + BrokerControlError::Transport => Self::Control, + BrokerControlError::Broker(error) => error.into(), + BrokerControlError::UnexpectedResponse => Self::UnexpectedResponse, + } } } -const fn error_code_to_object_error(error: ErrorCode) -> BrokerObjectError { - match error { - ErrorCode::InvalidRights - | ErrorCode::UnknownObject - | ErrorCode::WrongObjectType - | ErrorCode::StaleHandle => BrokerObjectError::InvalidObject, - ErrorCode::WouldBlock => BrokerObjectError::WouldBlock, - ErrorCode::ResourceExhausted => BrokerObjectError::ResourceExhausted, - _ => BrokerObjectError::Internal, +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, + } } } @@ -67,19 +67,18 @@ pub(super) fn map_broker_object_result( match result { Ok(value) => Ok(value), Err(BrokerObjectError::WouldBlock) => Err(TryOpError::TryAgain), - Err(error) => Err(TryOpError::Other(object_error_to_event_counter_error( - error, - ))), + Err(error) => Err(TryOpError::Other(error.into())), } } -pub(super) const fn object_error_to_event_counter_error( - error: BrokerObjectError, -) -> EventCounterError { - match error { - BrokerObjectError::InvalidObject => EventCounterError::InvalidInput, - BrokerObjectError::WouldBlock => EventCounterError::WouldBlock, - BrokerObjectError::ResourceExhausted => EventCounterError::ResourceExhausted, - _ => EventCounterError::Io, +impl From for EventCounterError { + fn from(error: BrokerObjectError) -> Self { + match error { + BrokerObjectError::InvalidObject => Self::InvalidInput, + BrokerObjectError::WouldBlock => Self::WouldBlock, + BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, + BrokerObjectError::UnexpectedResponse => Self::UnexpectedResponse, + _ => Self::Io, + } } } diff --git a/litebox/src/broker/event.rs b/litebox/src/broker/event.rs index 5cac5a775..c20f3bb2a 100644 --- a/litebox/src/broker/event.rs +++ b/litebox/src/broker/event.rs @@ -10,23 +10,24 @@ use litebox_broker_protocol::{ }; use super::{ - BrokerControl, BrokerState, EventConsumeMode, - error::{ - BrokerObjectError, EventCounterError, control_error_to_object_error, - map_broker_object_result, object_error_to_event_counter_error, - }, + BrokerControl, BrokerState, + error::{BrokerObjectError, map_broker_object_result}, }; use crate::{ event::{ - Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, + Events, IOPollable, + counter::{EventCounter, EventCounterError, EventCounterReadMode}, + observer::Observer, + polling::Pollee, + polling::TryOpError, wait::WaitContext, }, platform::TimeProvider, sync::RawSyncPrimitivesProvider, }; -/// A broker-backed local-core event counter. -pub struct EventCounter { +/// Broker-backed implementation of a local-core event counter. +pub(crate) struct BrokerEventCounter { broker: Arc, handle: ObjectHandle, pollee: Pollee, @@ -36,18 +37,18 @@ impl BrokerState { pub(crate) fn create_event_counter( &self, initial_count: u64, - ) -> Result>, EventCounterError> + ) -> Result, EventCounterError> where Platform: TimeProvider, { - let Some(control) = self.control.read().clone() else { - return Ok(None); + let Some(control) = self.control.clone() else { + return Err(EventCounterError::Unavailable); }; - EventCounter::new(control, initial_count).map(Some) + BrokerEventCounter::new(control, initial_count).map(EventCounter::from_broker) } } -impl EventCounter +impl BrokerEventCounter where Platform: RawSyncPrimitivesProvider + TimeProvider, { @@ -56,9 +57,9 @@ where &broker, EventRequest::Create(CreateEventRequest::new(initial_count)), ) - .map_err(object_error_to_event_counter_error)?; + .map_err(EventCounterError::from)?; let EventResponse::Create(response) = response else { - return Err(EventCounterError::Io); + return Err(BrokerObjectError::UnexpectedResponse.into()); }; Ok(Self { broker, @@ -68,23 +69,26 @@ where } /// Returns whether blocking reads and writes are supported. - pub fn supports_blocking_operations(&self) -> bool { + pub(crate) fn supports_blocking_operations() -> bool { false } /// Reads the event counter. - pub fn read( + pub(crate) fn read( &self, _cx: &WaitContext<'_, Platform>, _nonblock: bool, - mode: EventConsumeMode, + mode: EventCounterReadMode, ) -> Result> { 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( + pub(crate) fn write( &self, _cx: &WaitContext<'_, Platform>, _nonblock: bool, @@ -94,16 +98,19 @@ where return Err(TryOpError::Other(EventCounterError::InvalidInput)); } let readiness = map_broker_object_result(self.add(value))?; - if value != 0 && readiness.ready { + if value != 0 && readiness.read_ready { self.pollee.notify_observers(Events::IN); } Ok(core::mem::size_of::()) } - fn consume(&self, mode: EventConsumeMode) -> Result { + fn consume( + &self, + mode: EventCounterReadMode, + ) -> Result { let response = self.request(EventRequest::Consume(ConsumeEventRequest::new( self.handle, - mode, + to_protocol_consume_mode(mode), )))?; let EventResponse::Consume(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); @@ -119,12 +126,15 @@ where Ok(response.readiness) } - fn is_read_ready(&self) -> Result { + fn readiness_state(&self) -> Result { let response = self.request(EventRequest::Wait(WaitEventRequest::new(self.handle)))?; let EventResponse::Wait(response) = response else { return Err(BrokerObjectError::UnexpectedResponse); }; - Ok(matches!(response.outcome, WaitOutcome::Ready(_))) + Ok(match response.outcome { + WaitOutcome::Ready(readiness) | WaitOutcome::WouldBlock(readiness) => readiness, + _ => return Err(BrokerObjectError::UnexpectedResponse), + }) } fn request(&self, request: EventRequest) -> Result { @@ -132,7 +142,7 @@ where } } -impl IOPollable for EventCounter +impl IOPollable for BrokerEventCounter where Platform: RawSyncPrimitivesProvider + TimeProvider, { @@ -141,24 +151,36 @@ where } fn check_io_events(&self) -> Events { - // The broker protocol currently exposes read readiness only. Keep write - // readiness optimistic and surface counter-limit failures from write - // until broker write-readiness plumbing exists. - let mut events = Events::OUT; - if self.is_read_ready().unwrap_or(false) { + 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 to_protocol_consume_mode( + mode: EventCounterReadMode, +) -> litebox_broker_protocol::EventConsumeMode { + match mode { + EventCounterReadMode::All => litebox_broker_protocol::EventConsumeMode::All, + EventCounterReadMode::One => litebox_broker_protocol::EventConsumeMode::One, + } +} + fn request_event( broker: &Arc, request: EventRequest, ) -> Result { let response = broker .request(CoreRequest::Event(request)) - .map_err(control_error_to_object_error)?; + .map_err(BrokerObjectError::from)?; match response { CoreResponse::Event(response) => Ok(response), _ => Err(BrokerObjectError::UnexpectedResponse), @@ -176,6 +198,7 @@ mod tests { use litebox_broker_protocol::{ AddEventResponse, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventResponse, EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, + WaitEventResponse, }; use super::*; @@ -242,23 +265,23 @@ mod tests { ))) } - fn add_response(ready: bool) -> CoreResponse { + fn add_response(read_ready: bool, write_ready: bool) -> CoreResponse { CoreResponse::Event(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(ready, 1), + ReadinessState::new(read_ready, write_ready, 1), ))) } - fn consume_response(value: u64, ready: bool) -> CoreResponse { + fn consume_response(value: u64, read_ready: bool, write_ready: bool) -> CoreResponse { CoreResponse::Event(EventResponse::Consume(ConsumeEventResponse::new( value, - ReadinessState::new(ready, 1), + ReadinessState::new(read_ready, write_ready, 1), ))) } fn event_counter( responses: impl IntoIterator, - ) -> EventCounter { - EventCounter::new(Arc::new(MockBrokerControl::new(responses)), 0).unwrap() + ) -> BrokerEventCounter { + BrokerEventCounter::new(Arc::new(MockBrokerControl::new(responses)), 0).unwrap() } fn observer() -> (Arc, Arc>) { @@ -267,9 +290,19 @@ mod tests { (observer, observer_dyn) } + #[test] + fn create_maps_unexpected_response_shape() { + let result = BrokerEventCounter::::new( + Arc::new(MockBrokerControl::new([add_response(false, true)])), + 0, + ); + + assert!(matches!(result, Err(EventCounterError::UnexpectedResponse))); + } + #[test] fn zero_write_does_not_notify_read_observers() { - let event = event_counter([create_response(), add_response(false)]); + let event = event_counter([create_response(), add_response(false, true)]); let (observer, observer_dyn) = observer(); event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); @@ -281,7 +314,7 @@ mod tests { #[test] fn nonzero_write_notifies_read_observers_when_broker_reports_ready() { - let event = event_counter([create_response(), add_response(true)]); + let event = event_counter([create_response(), add_response(true, true)]); let (observer, observer_dyn) = observer(); event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); @@ -293,18 +326,51 @@ mod tests { #[test] fn read_does_not_notify_write_observers_without_broker_write_readiness() { - let event = event_counter([create_response(), consume_response(1, false)]); + let event = event_counter([create_response(), consume_response(1, false, false)]); let (observer, observer_dyn) = observer(); event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); let wait = WaitState::new(MockPlatform::new()); assert_eq!( event - .read(&wait.context(), true, EventConsumeMode::All) + .read(&wait.context(), true, EventCounterReadMode::All) .unwrap(), 1 ); assert_eq!(observer.notifications(), 0); } + + #[test] + fn read_notifies_write_observers_when_broker_reports_write_ready() { + let event = event_counter([create_response(), consume_response(1, false, true)]); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!( + event + .read(&wait.context(), true, EventCounterReadMode::All) + .unwrap(), + 1 + ); + + assert_eq!(observer.notifications(), 1); + } + + #[test] + fn poll_uses_broker_read_and_write_readiness() { + let event = event_counter([ + create_response(), + CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, false, 1)), + ))), + CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 2)), + ))), + ]); + + assert_eq!(event.check_io_events(), Events::IN); + assert_eq!(event.check_io_events(), Events::OUT); + } } diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 934541f8a..1dc8a68e2 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -5,13 +5,11 @@ use alloc::sync::Arc; use litebox_broker_protocol::{CoreRequest, CoreResponse}; -use crate::sync::{RawSyncPrimitivesProvider, RwLock}; +use crate::sync::RawSyncPrimitivesProvider; mod error; -mod event; -pub use error::{BrokerControlError, EventCounterError}; -pub use event::EventCounter; -pub use litebox_broker_protocol::EventConsumeMode; +pub(crate) mod event; +pub use error::BrokerControlError; /// Local-core access to the negotiated broker control channel. /// @@ -27,17 +25,15 @@ pub trait BrokerControl: Send + Sync { } pub(crate) struct BrokerState { - control: RwLock>>, + control: Option>, + _marker: core::marker::PhantomData, } impl BrokerState { - pub(crate) fn new() -> Self { + pub(crate) fn new(control: Option>) -> Self { Self { - control: RwLock::new(None), + control, + _marker: core::marker::PhantomData, } } - - pub(crate) fn set_control(&self, broker_control: Arc) { - *self.control.write() = Some(broker_control); - } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs new file mode 100644 index 000000000..80f7c0802 --- /dev/null +++ b/litebox/src/event/counter.rs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use crate::{ + broker::event::BrokerEventCounter, + event::{Events, IOPollable, observer::Observer, polling::TryOpError, wait::WaitContext}, + platform::TimeProvider, + sync::RawSyncPrimitivesProvider, +}; + +/// How an event-counter read consumes the current counter value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub enum EventCounterReadMode { + /// Consume the full counter value. + All, + /// Consume one counter credit. + One, +} + +/// 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, +} + +enum EventCounterBackend { + Broker(BrokerEventCounter), +} + +/// A local-core event counter object. +/// +/// The object API is independent of the current backing implementation. The +/// split-broker POC uses a broker-backed counter, while future migrations can add +/// other backends without changing shim-facing call sites. +pub struct EventCounter { + backend: EventCounterBackend, +} + +impl EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + pub(crate) fn from_broker(event: BrokerEventCounter) -> Self { + Self { + backend: EventCounterBackend::Broker(event), + } + } + + /// Returns whether blocking reads and writes are supported. + pub fn supports_blocking_operations(&self) -> bool { + match &self.backend { + EventCounterBackend::Broker(_) => { + BrokerEventCounter::::supports_blocking_operations() + } + } + } + + /// Reads the event counter. + pub fn read( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + mode: EventCounterReadMode, + ) -> Result> { + match &self.backend { + EventCounterBackend::Broker(event) => event.read(cx, nonblock, mode), + } + } + + /// Writes readiness credits to the event counter. + pub fn write( + &self, + cx: &WaitContext<'_, Platform>, + nonblock: bool, + value: u64, + ) -> Result> { + match &self.backend { + EventCounterBackend::Broker(event) => event.write(cx, nonblock, value), + } + } +} + +impl IOPollable for EventCounter +where + Platform: RawSyncPrimitivesProvider + TimeProvider, +{ + fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { + match &self.backend { + EventCounterBackend::Broker(event) => event.register_observer(observer, mask), + } + } + + fn check_io_events(&self) -> Events { + match &self.backend { + EventCounterBackend::Broker(event) => event.check_io_events(), + } + } +} diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 24d5b6832..efefe9642 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -3,10 +3,13 @@ //! Events related functionality +pub mod counter; pub mod observer; pub mod polling; pub mod wait; +pub use counter::{EventCounter, EventCounterError, EventCounterReadMode}; + bitflags::bitflags! { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Events: u32 { diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index fcde2e28c..7d25ff95d 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -21,6 +21,7 @@ pub mod fd; pub mod fs; pub mod mm; pub mod net; +pub mod objects; pub mod path; pub mod pipes; pub mod platform; @@ -41,6 +42,4 @@ mod utilities; pub mod utils; mod broker; -pub use broker::{ - BrokerControl, BrokerControlError, EventConsumeMode, EventCounter, EventCounterError, -}; +pub use broker::{BrokerControl, BrokerControlError}; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 5efbaa1e2..db5d38e2b 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -6,9 +6,9 @@ use alloc::sync::Arc; use crate::{ - broker::{BrokerControl, BrokerState, EventCounter, EventCounterError}, + broker::{BrokerControl, BrokerState}, fd::Descriptors, - platform::TimeProvider, + objects::Objects, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -32,6 +32,21 @@ 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)) + } + + 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 @@ -67,7 +82,7 @@ impl LiteBox { crate::sync::lock_tracing::LockTracker::init(platform); let descriptors = RwLock::new(Descriptors::new_from_litebox_creation()); - let broker = BrokerState::new(); + let broker = BrokerState::new(broker_control); litebox_util_log::trace!("LiteBox instance initialized"); @@ -111,20 +126,9 @@ impl LiteBox { self.x.descriptors.write() } - /// Installs broker control for broker-backed local-core objects. - pub fn set_broker_control(&self, broker_control: Arc) { - self.x.broker.set_control(broker_control); - } - - /// Creates a broker-backed event counter when broker control is installed. - pub fn create_event_counter( - &self, - initial_count: u64, - ) -> Result>, EventCounterError> - where - Platform: TimeProvider, - { - self.x.broker.create_event_counter(initial_count) + /// Returns local-core object factories. + pub fn objects(&self) -> Objects<'_, Platform> { + Objects::new(&self.x.broker) } } diff --git a/litebox/src/objects.rs b/litebox/src/objects.rs new file mode 100644 index 000000000..bb3d28491 --- /dev/null +++ b/litebox/src/objects.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Local-core object factory namespaces. + +use crate::{ + broker::BrokerState, + event::counter::{EventCounter, EventCounterError}, + platform::TimeProvider, + sync::RawSyncPrimitivesProvider, +}; + +/// Factories for local-core objects that may be backed by local or broker authority. +pub struct Objects<'a, Platform: RawSyncPrimitivesProvider> { + broker: &'a BrokerState, +} + +impl<'a, Platform: RawSyncPrimitivesProvider> Objects<'a, Platform> { + pub(crate) fn new(broker: &'a BrokerState) -> Self { + Self { broker } + } + + /// Returns factories for local-core event objects. + pub fn events(&self) -> EventObjects<'a, Platform> { + EventObjects { + broker: self.broker, + } + } +} + +/// Factories for local-core event objects. +pub struct EventObjects<'a, Platform: RawSyncPrimitivesProvider> { + broker: &'a BrokerState, +} + +impl EventObjects<'_, Platform> { + /// Creates a local-core event counter. + pub fn create_counter( + &self, + initial_count: u64, + ) -> Result, EventCounterError> + where + Platform: TimeProvider, + { + self.broker.create_event_counter(initial_count) + } +} diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index 0e354a3e6..5bd4b3bdb 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -27,7 +27,7 @@ pub use error::{ClientError, Result}; pub use worker::{BrokerClientWorker, BrokerClientWorkerError}; /// Protocol version this client implementation requests by default. -pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); +pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); /// Typed client for broker operations. pub struct BrokerClient { diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index e92c76414..f69118d69 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -50,7 +50,7 @@ impl BrokerCore

{ let object_id = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; let state = self.event_state(object_id)?; - Ok(if state.ready { + Ok(if state.read_ready { WaitOutcome::Ready(state) } else { WaitOutcome::WouldBlock(state) @@ -107,7 +107,11 @@ impl EventObject { } pub(crate) const fn readiness_state(self) -> ReadinessState { - ReadinessState::new(self.count > 0, self.readiness_generation) + ReadinessState::new( + self.count > 0, + self.count < MAX_EVENT_COUNT, + self.readiness_generation, + ) } fn add(&mut self, value: u64) -> Result { diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index 655980c89..653be6677 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -6,26 +6,32 @@ use crate::ObjectHandle; /// Broker-authoritative readiness state for one object. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct ReadinessState { - /// Whether the object is currently ready. - pub ready: bool, + /// 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(ready: bool, generation: u64) -> Self { - Self { ready, generation } + 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 wait would complete now. +/// 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 ready now. + /// The object is read-ready now. Ready(ReadinessState), - /// The object is not ready; deployment-specific wait plumbing may block. + /// The object is not read-ready; deployment-specific wait plumbing may block. WouldBlock(ReadinessState), } diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 9657ff7a5..70432e059 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -13,7 +13,7 @@ use litebox_broker_protocol::{ }; /// Protocol version this broker server implementation supports. -pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 1); +pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); /// Serves one broker connection over the provided connected control channel. pub fn serve_connection( @@ -420,7 +420,7 @@ mod tests { assert_eq!( dispatch.response, event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); @@ -434,7 +434,7 @@ mod tests { assert_eq!( dispatch.response, event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, 1) + ReadinessState::new(true, true, 1) ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); @@ -448,7 +448,7 @@ mod tests { assert_eq!( dispatch.response, event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, 1)) + WaitOutcome::Ready(ReadinessState::new(true, true, 1)) ))) ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index 3190d0f5d..d68da00fa 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -26,17 +26,17 @@ fn separate_process_broker_serves_event_object_requests() { let handle = client.create_event().unwrap(); assert_eq!( client.wait_event(handle).unwrap(), - WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) ); assert_eq!( client.add_event(handle, 1).unwrap(), - ReadinessState::new(true, 1) + ReadinessState::new(true, true, 1) ); assert_eq!( client.wait_event(handle).unwrap(), - WaitOutcome::Ready(ReadinessState::new(true, 1)) + WaitOutcome::Ready(ReadinessState::new(true, true, 1)) ); drop(client); diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index 9317d268f..c8da62581 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -403,7 +403,8 @@ impl Encoder { } fn readiness(&mut self, readiness: ReadinessState) { - self.bool(readiness.ready); + self.bool(readiness.read_ready); + self.bool(readiness.write_ready); self.u64(readiness.generation); } } @@ -459,7 +460,7 @@ impl<'a> Decoder<'a> { } fn readiness(&mut self) -> Result { - Ok(ReadinessState::new(self.bool()?, self.u64()?)) + Ok(ReadinessState::new(self.bool()?, self.bool()?, self.u64()?)) } fn take(&mut self, len: usize) -> Result<&'a [u8], WireError> { @@ -521,17 +522,17 @@ mod tests { }, event_response(EventResponse::Create(CreateEventResponse::new(handle))), event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, 8)), + WaitOutcome::Ready(ReadinessState::new(true, false, 8)), ))), event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, 9)), + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 9)), ))), event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, 10), + ReadinessState::new(true, true, 10), ))), event_response(EventResponse::Consume(ConsumeEventResponse::new( 3, - ReadinessState::new(false, 11), + ReadinessState::new(false, true, 11), ))), BrokerResponse::Error(ErrorCode::PolicyDenied), BrokerResponse::Error(ErrorCode::WouldBlock), @@ -578,14 +579,15 @@ mod tests { ))) ); - let mut invalid_bool = [1, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0]; + 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[11] = 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)); @@ -595,10 +597,10 @@ mod tests { fn readiness_response_wire_shape_is_pinned() { assert_eq!( encode_response(event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, 0x0102_0304_0506_0708) + ReadinessState::new(true, false, 0x0102_0304_0506_0708) )))) .unwrap(), - [1, 0, 2, 1, 8, 7, 6, 5, 4, 3, 2, 1] + [1, 0, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1] ); } diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 1072ae00c..63822dcc7 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,13 +536,15 @@ where } } -impl From for Errno { - fn from(value: litebox::EventCounterError) -> Self { - match value { - litebox::EventCounterError::InvalidInput => Errno::EINVAL, - litebox::EventCounterError::WouldBlock - | litebox::EventCounterError::ResourceExhausted => Errno::EAGAIN, - litebox::EventCounterError::Io => Errno::EIO, +impl From for Errno { + fn from(value: litebox::event::EventCounterError) -> Self { + match value { + litebox::event::EventCounterError::InvalidInput => Errno::EINVAL, + litebox::event::EventCounterError::WouldBlock + | litebox::event::EventCounterError::ResourceExhausted => Errno::EAGAIN, + litebox::event::EventCounterError::Io + | litebox::event::EventCounterError::UnexpectedResponse + | litebox::event::EventCounterError::Unavailable => Errno::EIO, _ => Errno::EIO, } } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 50c19ec80..427769d85 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -3,7 +3,10 @@ use anyhow::{Context as _, Result, anyhow}; use clap::Parser; -use litebox::fs::{FileSystem as _, Mode}; +use litebox::{ + LiteBox, + fs::{FileSystem as _, Mode}, +}; use litebox_platform_multiplex::Platform; use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; @@ -214,12 +217,14 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; - let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(); - if let Some(broker_connection) = &broker_connection { - shim_builder - .litebox() - .set_broker_control(broker_connection.control()); - } + let shim_builder = if let Some(broker_connection) = &broker_connection { + litebox_shim_linux::LinuxShimBuilder::from_litebox(LiteBox::new_with_broker_control( + litebox_platform_multiplex::platform(), + broker_connection.control(), + )) + } 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/run.rs b/litebox_runner_linux_userland/tests/run.rs index 247362ddc..69dc09009 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -311,10 +311,10 @@ fn test_broker_event_path_over_unix_socket() { let handle = client.create_event().expect("event create failed"); assert_eq!( client.wait_event(handle).expect("event wait failed"), - WaitOutcome::WouldBlock(ReadinessState::new(false, 0)) + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) ); let readiness = client.add_event(handle, 1).expect("event add failed"); - assert_eq!(readiness, ReadinessState::new(true, 1)); + assert_eq!(readiness, ReadinessState::new(true, true, 1)); assert_eq!( client.wait_event(handle).expect("event ready-wait failed"), WaitOutcome::Ready(readiness) diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index c9fd47917..1bc89b16d 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -164,10 +164,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::from_litebox(LiteBox::new(platform)) + } + + /// Returns a new shim builder using an already-created LiteBox instance. + pub fn from_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/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 1cb689abf..3c506d532 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -6,10 +6,10 @@ use core::sync::atomic::AtomicU32; use litebox::{ - EventConsumeMode, EventCounter, LiteBox, + LiteBox, event::{ - Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, - wait::WaitContext, + EventCounter, EventCounterReadMode, Events, IOPollable, observer::Observer, + polling::Pollee, polling::TryOpError, wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, @@ -43,7 +43,13 @@ impl EventFile { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); let local_core_event = if flags.contains(EfdFlags::NONBLOCK) { - litebox.create_event_counter(count).map_err(Errno::from)? + Some( + litebox + .objects() + .events() + .create_counter(count) + .map_err(Errno::from)?, + ) } else { None }; @@ -57,11 +63,11 @@ impl EventFile { }) } - fn consume_mode(&self) -> EventConsumeMode { + fn consume_mode(&self) -> EventCounterReadMode { if self.semaphore { - EventConsumeMode::One + EventCounterReadMode::One } else { - EventConsumeMode::All + EventCounterReadMode::All } } @@ -258,48 +264,12 @@ mod tests { } #[test] - fn test_nonblocking_eventfd() { + fn test_nonblocking_eventfd_requires_broker_control() { let task = crate::syscalls::tests::init_platform(None); - let eventfd = alloc::sync::Arc::new( - super::EventFile::new(&task.global.litebox, 0, EfdFlags::NONBLOCK).unwrap(), + assert_eq!( + super::EventFile::new(&task.global.litebox, 0, EfdFlags::NONBLOCK).map(|_| ()), + Err(Errno::EIO) ); - 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(); - } - }; - - // block until the first write - read(&eventfd, 1); - // block until the second write - read(&eventfd, u64::MAX - 1); } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 5663e2dac..85940c226 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2847,7 +2847,7 @@ mod tests { fn eventfd_writev_rejects_split_value() { let task = crate::syscalls::tests::init_platform(None); let fd = task - .sys_eventfd2(0, EfdFlags::NONBLOCK) + .sys_eventfd2(0, EfdFlags::empty()) .expect("eventfd2 failed"); let fd = i32::try_from(fd).unwrap(); let value = 0x0102_0304_0506_0708u64; @@ -2867,9 +2867,6 @@ mod tests { task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), Err(Errno::EINVAL) ); - - let mut output = [0u8; 8]; - assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); } #[test] @@ -2936,7 +2933,7 @@ mod tests { fn eventfd_writev_writes_each_full_value() { let task = crate::syscalls::tests::init_platform(None); let fd = task - .sys_eventfd2(0, EfdFlags::NONBLOCK) + .sys_eventfd2(0, EfdFlags::empty()) .expect("eventfd2 failed"); let fd = i32::try_from(fd).unwrap(); let first = 7u64.to_ne_bytes(); @@ -2960,7 +2957,6 @@ mod tests { let mut output = [0u8; 8]; assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); assert_eq!(u64::from_ne_bytes(output), 18); - assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); } #[test] diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 81f4e07ed..8fe5854f0 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -88,13 +88,10 @@ fn test_fcntl() { // Test eventfd let eventfd = task - .sys_eventfd2( - 0, - EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE | EfdFlags::NONBLOCK, - ) + .sys_eventfd2(0, EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE) .expect("Failed to create eventfd"); let eventfd = i32::try_from(eventfd).unwrap(); - check(eventfd, OFlags::RDWR | OFlags::NONBLOCK, OFlags::RDWR); + check(eventfd, OFlags::RDWR, OFlags::RDWR); // Test fcntl with DUPFD let fd = task From f2f803459e371b28db8e6fcd7e48a6d079602bda Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 10:39:28 -0700 Subject: [PATCH 30/66] Align event counter factory with event domain Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/impl-plan.md | 2 +- litebox/src/event/mod.rs | 31 +++++++++++++- litebox/src/lib.rs | 1 - litebox/src/litebox.rs | 8 ++-- litebox/src/objects.rs | 47 ---------------------- litebox_common_linux/src/errno/mod.rs | 18 ++++----- litebox_shim_linux/src/syscalls/eventfd.rs | 9 +++-- 7 files changed, 50 insertions(+), 66 deletions(-) delete mode 100644 litebox/src/objects.rs diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 50a34c6a6..4f1459893 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -95,7 +95,7 @@ Initial scope: Exit criteria: -- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated objects through the local-core object factory namespace rather than through broker-specific APIs. +- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index efefe9642..639400534 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -3,12 +3,41 @@ //! Events related functionality +use crate::{ + broker::BrokerState, + event::counter::{EventCounter, EventCounterError}, + platform::TimeProvider, + sync::RawSyncPrimitivesProvider, +}; + pub mod counter; pub mod observer; pub mod polling; pub mod wait; -pub use counter::{EventCounter, EventCounterError, EventCounterReadMode}; +/// Factories for local-core event objects. +pub struct EventObjects<'a, Platform: RawSyncPrimitivesProvider> { + broker: &'a BrokerState, +} + +impl<'a, Platform: RawSyncPrimitivesProvider> EventObjects<'a, Platform> { + pub(crate) fn new(broker: &'a BrokerState) -> Self { + Self { broker } + } +} + +impl EventObjects<'_, Platform> { + /// Creates a local-core event counter. + pub fn create_counter( + &self, + initial_count: u64, + ) -> Result, EventCounterError> + where + Platform: TimeProvider, + { + self.broker.create_event_counter(initial_count) + } +} bitflags::bitflags! { #[derive(Clone, Copy, PartialEq, Eq, Debug)] diff --git a/litebox/src/lib.rs b/litebox/src/lib.rs index 7d25ff95d..f01e028f9 100644 --- a/litebox/src/lib.rs +++ b/litebox/src/lib.rs @@ -21,7 +21,6 @@ pub mod fd; pub mod fs; pub mod mm; pub mod net; -pub mod objects; pub mod path; pub mod pipes; pub mod platform; diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index db5d38e2b..a1369e99e 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -7,8 +7,8 @@ use alloc::sync::Arc; use crate::{ broker::{BrokerControl, BrokerState}, + event::EventObjects, fd::Descriptors, - objects::Objects, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -126,9 +126,9 @@ impl LiteBox { self.x.descriptors.write() } - /// Returns local-core object factories. - pub fn objects(&self) -> Objects<'_, Platform> { - Objects::new(&self.x.broker) + /// Returns local-core event object factories. + pub fn events(&self) -> EventObjects<'_, Platform> { + EventObjects::new(&self.x.broker) } } diff --git a/litebox/src/objects.rs b/litebox/src/objects.rs deleted file mode 100644 index bb3d28491..000000000 --- a/litebox/src/objects.rs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Local-core object factory namespaces. - -use crate::{ - broker::BrokerState, - event::counter::{EventCounter, EventCounterError}, - platform::TimeProvider, - sync::RawSyncPrimitivesProvider, -}; - -/// Factories for local-core objects that may be backed by local or broker authority. -pub struct Objects<'a, Platform: RawSyncPrimitivesProvider> { - broker: &'a BrokerState, -} - -impl<'a, Platform: RawSyncPrimitivesProvider> Objects<'a, Platform> { - pub(crate) fn new(broker: &'a BrokerState) -> Self { - Self { broker } - } - - /// Returns factories for local-core event objects. - pub fn events(&self) -> EventObjects<'a, Platform> { - EventObjects { - broker: self.broker, - } - } -} - -/// Factories for local-core event objects. -pub struct EventObjects<'a, Platform: RawSyncPrimitivesProvider> { - broker: &'a BrokerState, -} - -impl EventObjects<'_, Platform> { - /// Creates a local-core event counter. - pub fn create_counter( - &self, - initial_count: u64, - ) -> Result, EventCounterError> - where - Platform: TimeProvider, - { - self.broker.create_event_counter(initial_count) - } -} diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 63822dcc7..3c1177a76 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -536,15 +536,15 @@ where } } -impl From for Errno { - fn from(value: litebox::event::EventCounterError) -> Self { - match value { - litebox::event::EventCounterError::InvalidInput => Errno::EINVAL, - litebox::event::EventCounterError::WouldBlock - | litebox::event::EventCounterError::ResourceExhausted => Errno::EAGAIN, - litebox::event::EventCounterError::Io - | litebox::event::EventCounterError::UnexpectedResponse - | litebox::event::EventCounterError::Unavailable => Errno::EIO, +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, } } diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 3c506d532..7535c70ff 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -8,8 +8,12 @@ use core::sync::atomic::AtomicU32; use litebox::{ LiteBox, event::{ - EventCounter, EventCounterReadMode, Events, IOPollable, observer::Observer, - polling::Pollee, polling::TryOpError, wait::WaitContext, + Events, IOPollable, + counter::{EventCounter, EventCounterReadMode}, + observer::Observer, + polling::Pollee, + polling::TryOpError, + wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, fs::OFlags, @@ -45,7 +49,6 @@ impl EventFile { let local_core_event = if flags.contains(EfdFlags::NONBLOCK) { Some( litebox - .objects() .events() .create_counter(count) .map_err(Errno::from)?, From 813550e7f2216b2e266081c3ab6462a4f1c4171b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 12:42:17 -0700 Subject: [PATCH 31/66] Simplify event counter local-core interface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 4 +- litebox/src/broker/event.rs | 376 --------------------- litebox/src/broker/mod.rs | 7 +- litebox/src/event/counter.rs | 354 ++++++++++++++++--- litebox/src/event/mod.rs | 31 -- litebox/src/litebox.rs | 6 +- litebox_shim_linux/src/syscalls/eventfd.rs | 7 +- 7 files changed, 321 insertions(+), 464 deletions(-) delete mode 100644 litebox/src/broker/event.rs diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 991fa12f0..53936a3b6 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -22,7 +22,7 @@ pub enum BrokerControlError { /// 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(super) enum BrokerObjectError { +pub(crate) enum BrokerObjectError { /// The deployment-provided broker control path failed. Control, /// The broker rejected the cached object handle, type, or rights. @@ -61,7 +61,7 @@ impl From for BrokerObjectError { } } -pub(super) fn map_broker_object_result( +pub(crate) fn map_broker_object_result( result: Result, ) -> Result> { match result { diff --git a/litebox/src/broker/event.rs b/litebox/src/broker/event.rs deleted file mode 100644 index c20f3bb2a..000000000 --- a/litebox/src/broker/event.rs +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use alloc::sync::Arc; - -use litebox_broker_protocol::{ - AddEventRequest, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, - CreateEventRequest, EventRequest, EventResponse, ObjectHandle, ReadinessState, - WaitEventRequest, WaitOutcome, -}; - -use super::{ - BrokerControl, BrokerState, - error::{BrokerObjectError, map_broker_object_result}, -}; -use crate::{ - event::{ - Events, IOPollable, - counter::{EventCounter, EventCounterError, EventCounterReadMode}, - observer::Observer, - polling::Pollee, - polling::TryOpError, - wait::WaitContext, - }, - platform::TimeProvider, - sync::RawSyncPrimitivesProvider, -}; - -/// Broker-backed implementation of a local-core event counter. -pub(crate) struct BrokerEventCounter { - broker: Arc, - handle: ObjectHandle, - pollee: Pollee, -} - -impl BrokerState { - pub(crate) fn create_event_counter( - &self, - initial_count: u64, - ) -> Result, EventCounterError> - where - Platform: TimeProvider, - { - let Some(control) = self.control.clone() else { - return Err(EventCounterError::Unavailable); - }; - BrokerEventCounter::new(control, initial_count).map(EventCounter::from_broker) - } -} - -impl BrokerEventCounter -where - Platform: RawSyncPrimitivesProvider + TimeProvider, -{ - fn new(broker: Arc, initial_count: u64) -> Result { - let response = request_event( - &broker, - EventRequest::Create(CreateEventRequest::new(initial_count)), - ) - .map_err(EventCounterError::from)?; - let EventResponse::Create(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse.into()); - }; - Ok(Self { - broker, - handle: response.handle, - pollee: Pollee::new(), - }) - } - - /// Returns whether blocking reads and writes are supported. - pub(crate) fn supports_blocking_operations() -> bool { - false - } - - /// Reads the event counter. - pub(crate) fn read( - &self, - _cx: &WaitContext<'_, Platform>, - _nonblock: bool, - mode: EventCounterReadMode, - ) -> Result> { - 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(crate) fn write( - &self, - _cx: &WaitContext<'_, Platform>, - _nonblock: bool, - value: u64, - ) -> Result> { - if value == u64::MAX { - return Err(TryOpError::Other(EventCounterError::InvalidInput)); - } - 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(EventRequest::Consume(ConsumeEventRequest::new( - self.handle, - to_protocol_consume_mode(mode), - )))?; - let EventResponse::Consume(response) = response else { - return Err(BrokerObjectError::UnexpectedResponse); - }; - Ok(response) - } - - fn add(&self, value: u64) -> Result { - let response = self.request(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(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(&self, request: EventRequest) -> Result { - request_event(&self.broker, request) - } -} - -impl IOPollable for BrokerEventCounter -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 to_protocol_consume_mode( - mode: EventCounterReadMode, -) -> litebox_broker_protocol::EventConsumeMode { - match mode { - EventCounterReadMode::All => litebox_broker_protocol::EventConsumeMode::All, - EventCounterReadMode::One => litebox_broker_protocol::EventConsumeMode::One, - } -} - -fn request_event( - broker: &Arc, - request: EventRequest, -) -> Result { - let response = broker - .request(CoreRequest::Event(request)) - .map_err(BrokerObjectError::from)?; - match response { - CoreResponse::Event(response) => Ok(response), - _ => Err(BrokerObjectError::UnexpectedResponse), - } -} - -#[cfg(test)] -mod tests { - extern crate std; - - use alloc::sync::Arc; - use core::sync::atomic::{AtomicUsize, Ordering}; - use std::{collections::VecDeque, sync::Mutex}; - - use litebox_broker_protocol::{ - AddEventResponse, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventResponse, - EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, - WaitEventResponse, - }; - - use super::*; - use crate::{ - BrokerControlError, - event::{Events, IOPollable, observer::Observer, wait::WaitState}, - platform::mock::MockPlatform, - }; - - struct MockBrokerControl { - responses: Mutex>, - } - - impl MockBrokerControl { - fn new(responses: impl IntoIterator) -> Self { - Self { - responses: Mutex::new(responses.into_iter().collect()), - } - } - } - - impl BrokerControl for MockBrokerControl { - fn request( - &self, - _request: CoreRequest, - ) -> core::result::Result { - self.responses - .lock() - .unwrap() - .pop_front() - .ok_or(BrokerControlError::UnexpectedResponse) - } - } - - struct CountingObserver { - notifications: AtomicUsize, - } - - impl CountingObserver { - fn new() -> Self { - Self { - notifications: AtomicUsize::new(0), - } - } - - fn notifications(&self) -> usize { - self.notifications.load(Ordering::Relaxed) - } - } - - impl Observer for CountingObserver { - fn on_events(&self, _events: &Events) { - self.notifications.fetch_add(1, Ordering::Relaxed); - } - } - - fn sample_handle() -> ObjectHandle { - ObjectHandle::new(ObjectReferenceId::new(1), ObjectReferenceGeneration::new(1)) - } - - fn create_response() -> CoreResponse { - CoreResponse::Event(EventResponse::Create(CreateEventResponse::new( - sample_handle(), - ))) - } - - fn add_response(read_ready: bool, write_ready: bool) -> CoreResponse { - CoreResponse::Event(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(read_ready, write_ready, 1), - ))) - } - - fn consume_response(value: u64, read_ready: bool, write_ready: bool) -> CoreResponse { - CoreResponse::Event(EventResponse::Consume(ConsumeEventResponse::new( - value, - ReadinessState::new(read_ready, write_ready, 1), - ))) - } - - fn event_counter( - responses: impl IntoIterator, - ) -> BrokerEventCounter { - BrokerEventCounter::new(Arc::new(MockBrokerControl::new(responses)), 0).unwrap() - } - - fn observer() -> (Arc, Arc>) { - let observer = Arc::new(CountingObserver::new()); - let observer_dyn: Arc> = observer.clone(); - (observer, observer_dyn) - } - - #[test] - fn create_maps_unexpected_response_shape() { - let result = BrokerEventCounter::::new( - Arc::new(MockBrokerControl::new([add_response(false, true)])), - 0, - ); - - assert!(matches!(result, Err(EventCounterError::UnexpectedResponse))); - } - - #[test] - fn zero_write_does_not_notify_read_observers() { - let event = event_counter([create_response(), add_response(false, true)]); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!(event.write(&wait.context(), true, 0).unwrap(), 8); - - assert_eq!(observer.notifications(), 0); - } - - #[test] - fn nonzero_write_notifies_read_observers_when_broker_reports_ready() { - let event = event_counter([create_response(), add_response(true, true)]); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!(event.write(&wait.context(), true, 1).unwrap(), 8); - - assert_eq!(observer.notifications(), 1); - } - - #[test] - fn read_does_not_notify_write_observers_without_broker_write_readiness() { - let event = event_counter([create_response(), consume_response(1, false, false)]); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!( - event - .read(&wait.context(), true, EventCounterReadMode::All) - .unwrap(), - 1 - ); - - assert_eq!(observer.notifications(), 0); - } - - #[test] - fn read_notifies_write_observers_when_broker_reports_write_ready() { - let event = event_counter([create_response(), consume_response(1, false, true)]); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!( - event - .read(&wait.context(), true, EventCounterReadMode::All) - .unwrap(), - 1 - ); - - assert_eq!(observer.notifications(), 1); - } - - #[test] - fn poll_uses_broker_read_and_write_readiness() { - let event = event_counter([ - create_response(), - CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, false, 1)), - ))), - CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 2)), - ))), - ]); - - assert_eq!(event.check_io_events(), Events::IN); - assert_eq!(event.check_io_events(), Events::OUT); - } -} diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index 1dc8a68e2..ad2e5b43c 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -7,8 +7,7 @@ use litebox_broker_protocol::{CoreRequest, CoreResponse}; use crate::sync::RawSyncPrimitivesProvider; -mod error; -pub(crate) mod event; +pub(crate) mod error; pub use error::BrokerControlError; /// Local-core access to the negotiated broker control channel. @@ -36,4 +35,8 @@ impl BrokerState { _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 index 80f7c0802..74c56e79c 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -1,23 +1,29 @@ // 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::{ - broker::event::BrokerEventCounter, - event::{Events, IOPollable, observer::Observer, polling::TryOpError, wait::WaitContext}, + LiteBox, + broker::{ + BrokerControl, + error::{BrokerObjectError, map_broker_object_result}, + }, + event::{ + Events, IOPollable, observer::Observer, polling::Pollee, polling::TryOpError, + wait::WaitContext, + }, platform::TimeProvider, sync::RawSyncPrimitivesProvider, }; -/// How an event-counter read consumes the current counter value. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -#[non_exhaustive] -pub enum EventCounterReadMode { - /// Consume the full counter value. - All, - /// Consume one counter credit. - One, -} - /// Errors returned by local-core event counters. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] @@ -36,60 +42,117 @@ pub enum EventCounterError { Unavailable, } -enum EventCounterBackend { - Broker(BrokerEventCounter), -} - /// A local-core event counter object. -/// -/// The object API is independent of the current backing implementation. The -/// split-broker POC uses a broker-backed counter, while future migrations can add -/// other backends without changing shim-facing call sites. pub struct EventCounter { - backend: EventCounterBackend, + broker: Arc, + handle: ObjectHandle, + pollee: Pollee, + blocking_operations_supported: bool, } impl EventCounter where Platform: RawSyncPrimitivesProvider + TimeProvider, { - pub(crate) fn from_broker(event: BrokerEventCounter) -> Self { - Self { - backend: EventCounterBackend::Broker(event), - } + /// 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: false, + }) } /// Returns whether blocking reads and writes are supported. pub fn supports_blocking_operations(&self) -> bool { - match &self.backend { - EventCounterBackend::Broker(_) => { - BrokerEventCounter::::supports_blocking_operations() - } - } + self.blocking_operations_supported } /// Reads the event counter. pub fn read( &self, - cx: &WaitContext<'_, Platform>, - nonblock: bool, + _cx: &WaitContext<'_, Platform>, + _nonblock: bool, mode: EventCounterReadMode, ) -> Result> { - match &self.backend { - EventCounterBackend::Broker(event) => event.read(cx, nonblock, mode), + 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, + _cx: &WaitContext<'_, Platform>, + _nonblock: bool, value: u64, ) -> Result> { - match &self.backend { - EventCounterBackend::Broker(event) => event.write(cx, nonblock, value), + if value == u64::MAX { + return Err(TryOpError::Other(EventCounterError::InvalidInput)); + } + 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) } } @@ -98,14 +161,219 @@ where Platform: RawSyncPrimitivesProvider + TimeProvider, { fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - match &self.backend { - EventCounterBackend::Broker(event) => event.register_observer(observer, mask), - } + self.pollee.register_observer(observer, mask); } fn check_io_events(&self) -> Events { - match &self.backend { - EventCounterBackend::Broker(event) => event.check_io_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), + } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use alloc::sync::Arc; + use core::sync::atomic::{AtomicUsize, Ordering}; + use std::{collections::VecDeque, sync::Mutex}; + + use litebox_broker_protocol::{ + AddEventResponse, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventResponse, + EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, + WaitEventResponse, + }; + + use super::*; + use crate::{ + BrokerControlError, + event::{Events, IOPollable, observer::Observer, wait::WaitState}, + platform::mock::MockPlatform, + }; + + struct MockBrokerControl { + responses: Mutex>, + } + + impl MockBrokerControl { + fn new(responses: impl IntoIterator) -> Self { + Self { + responses: Mutex::new(responses.into_iter().collect()), + } } } + + impl BrokerControl for MockBrokerControl { + fn request( + &self, + _request: CoreRequest, + ) -> core::result::Result { + self.responses + .lock() + .unwrap() + .pop_front() + .ok_or(BrokerControlError::UnexpectedResponse) + } + } + + struct CountingObserver { + notifications: AtomicUsize, + } + + impl CountingObserver { + fn new() -> Self { + Self { + notifications: AtomicUsize::new(0), + } + } + + fn notifications(&self) -> usize { + self.notifications.load(Ordering::Relaxed) + } + } + + impl Observer for CountingObserver { + fn on_events(&self, _events: &Events) { + self.notifications.fetch_add(1, Ordering::Relaxed); + } + } + + fn sample_handle() -> ObjectHandle { + ObjectHandle::new(ObjectReferenceId::new(1), ObjectReferenceGeneration::new(1)) + } + + fn create_response() -> CoreResponse { + CoreResponse::Event(EventResponse::Create(CreateEventResponse::new( + sample_handle(), + ))) + } + + fn add_response(read_ready: bool, write_ready: bool) -> CoreResponse { + CoreResponse::Event(EventResponse::Add(AddEventResponse::new( + ReadinessState::new(read_ready, write_ready, 1), + ))) + } + + fn consume_response(value: u64, read_ready: bool, write_ready: bool) -> CoreResponse { + CoreResponse::Event(EventResponse::Consume(ConsumeEventResponse::new( + value, + ReadinessState::new(read_ready, write_ready, 1), + ))) + } + + fn observer() -> (Arc, Arc>) { + let observer = Arc::new(CountingObserver::new()); + let observer_dyn: Arc> = observer.clone(); + (observer, observer_dyn) + } + + fn litebox(responses: impl IntoIterator) -> LiteBox { + LiteBox::new_with_broker_control( + MockPlatform::new(), + Arc::new(MockBrokerControl::new(responses)), + ) + } + + #[test] + fn create_maps_unexpected_response_shape() { + let litebox = litebox([add_response(false, true)]); + let result = EventCounter::::new(&litebox, 0); + + assert!(matches!(result, Err(EventCounterError::UnexpectedResponse))); + } + + #[test] + fn zero_write_does_not_notify_read_observers() { + let litebox = litebox([create_response(), add_response(false, true)]); + let event = EventCounter::new(&litebox, 0).unwrap(); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!(event.write(&wait.context(), true, 0).unwrap(), 8); + + assert_eq!(observer.notifications(), 0); + } + + #[test] + fn nonzero_write_notifies_read_observers_when_broker_reports_ready() { + let litebox = litebox([create_response(), add_response(true, true)]); + let event = EventCounter::new(&litebox, 0).unwrap(); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!(event.write(&wait.context(), true, 1).unwrap(), 8); + + assert_eq!(observer.notifications(), 1); + } + + #[test] + fn read_does_not_notify_write_observers_without_broker_write_readiness() { + let litebox = litebox([create_response(), consume_response(1, false, false)]); + let event = EventCounter::new(&litebox, 0).unwrap(); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!( + event + .read(&wait.context(), true, EventCounterReadMode::All) + .unwrap(), + 1 + ); + + assert_eq!(observer.notifications(), 0); + } + + #[test] + fn read_notifies_write_observers_when_broker_reports_write_ready() { + let litebox = litebox([create_response(), consume_response(1, false, true)]); + let event = EventCounter::new(&litebox, 0).unwrap(); + let (observer, observer_dyn) = observer(); + event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); + + let wait = WaitState::new(MockPlatform::new()); + assert_eq!( + event + .read(&wait.context(), true, EventCounterReadMode::All) + .unwrap(), + 1 + ); + + assert_eq!(observer.notifications(), 1); + } + + #[test] + fn poll_uses_broker_read_and_write_readiness() { + let litebox = litebox([ + create_response(), + CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::Ready(ReadinessState::new(true, false, 1)), + ))), + CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( + WaitOutcome::WouldBlock(ReadinessState::new(false, true, 2)), + ))), + ]); + let event = EventCounter::new(&litebox, 0).unwrap(); + + assert_eq!(event.check_io_events(), Events::IN); + assert_eq!(event.check_io_events(), Events::OUT); + } } diff --git a/litebox/src/event/mod.rs b/litebox/src/event/mod.rs index 639400534..6089b6b08 100644 --- a/litebox/src/event/mod.rs +++ b/litebox/src/event/mod.rs @@ -3,42 +3,11 @@ //! Events related functionality -use crate::{ - broker::BrokerState, - event::counter::{EventCounter, EventCounterError}, - platform::TimeProvider, - sync::RawSyncPrimitivesProvider, -}; - pub mod counter; pub mod observer; pub mod polling; pub mod wait; -/// Factories for local-core event objects. -pub struct EventObjects<'a, Platform: RawSyncPrimitivesProvider> { - broker: &'a BrokerState, -} - -impl<'a, Platform: RawSyncPrimitivesProvider> EventObjects<'a, Platform> { - pub(crate) fn new(broker: &'a BrokerState) -> Self { - Self { broker } - } -} - -impl EventObjects<'_, Platform> { - /// Creates a local-core event counter. - pub fn create_counter( - &self, - initial_count: u64, - ) -> Result, EventCounterError> - where - Platform: TimeProvider, - { - self.broker.create_event_counter(initial_count) - } -} - bitflags::bitflags! { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Events: u32 { diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index a1369e99e..05a2a3f33 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -7,7 +7,6 @@ use alloc::sync::Arc; use crate::{ broker::{BrokerControl, BrokerState}, - event::EventObjects, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -126,9 +125,8 @@ impl LiteBox { self.x.descriptors.write() } - /// Returns local-core event object factories. - pub fn events(&self) -> EventObjects<'_, Platform> { - EventObjects::new(&self.x.broker) + pub(crate) fn broker_control(&self) -> Option> { + self.x.broker.control() } } diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 7535c70ff..99bd3f2bc 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -47,12 +47,7 @@ impl EventFile { let mut status = OFlags::RDWR; status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); let local_core_event = if flags.contains(EfdFlags::NONBLOCK) { - Some( - litebox - .events() - .create_counter(count) - .map_err(Errno::from)?, - ) + Some(EventCounter::new(litebox, count).map_err(Errno::from)?) } else { None }; From e0641d443e3d60ccdad2c4ced18bdaf9d00d722e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 17:06:34 -0700 Subject: [PATCH 32/66] Simplify eventfd shim integration Keep eventfd creation behind GlobalState, rename the shim builder constructor, and remove the unrelated pipe refactor from this branch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/src/lib.rs | 15 +- litebox_shim_linux/src/lib.rs | 9 +- litebox_shim_linux/src/syscalls/epoll.rs | 16 +- litebox_shim_linux/src/syscalls/eventfd.rs | 281 +++++++++++++-------- litebox_shim_linux/src/syscalls/file.rs | 108 ++++++-- litebox_shim_linux/src/syscalls/mod.rs | 1 - litebox_shim_linux/src/syscalls/pipe.rs | 164 ------------ 7 files changed, 284 insertions(+), 310 deletions(-) delete mode 100644 litebox_shim_linux/src/syscalls/pipe.rs diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 427769d85..ab5182a4c 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -3,10 +3,7 @@ use anyhow::{Context as _, Result, anyhow}; use clap::Parser; -use litebox::{ - LiteBox, - fs::{FileSystem as _, Mode}, -}; +use litebox::fs::{FileSystem as _, Mode}; use litebox_platform_multiplex::Platform; use memmap2::Mmap; use std::os::linux::fs::MetadataExt as _; @@ -218,10 +215,12 @@ pub fn run(cli_args: CliArgs) -> Result<()> { 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::from_litebox(LiteBox::new_with_broker_control( - litebox_platform_multiplex::platform(), - broker_connection.control(), - )) + litebox_shim_linux::LinuxShimBuilder::new_with_litebox( + litebox::LiteBox::new_with_broker_control( + litebox_platform_multiplex::platform(), + broker_connection.control(), + ), + ) } else { litebox_shim_linux::LinuxShimBuilder::new() }; diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index 1bc89b16d..d030ac318 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -61,6 +61,7 @@ pub(crate) type LinuxFS = litebox::fs::layered::FileSystem< litebox::fs::tar_ro::FileSystem, >, >; + pub(crate) type FileFd = litebox::fd::TypedFd; /// A trait required for file systems to be used in the shim. @@ -164,11 +165,11 @@ impl LinuxShimBuilder { /// Returns a new shim builder. pub fn new() -> Self { let platform = litebox_platform_multiplex::platform(); - Self::from_litebox(LiteBox::new(platform)) + Self::new_with_litebox(LiteBox::new(platform)) } /// Returns a new shim builder using an already-created LiteBox instance. - pub fn from_litebox(litebox: LiteBox) -> Self { + pub fn new_with_litebox(litebox: LiteBox) -> Self { let platform = litebox_platform_multiplex::platform(); Self { platform, litebox } } @@ -354,6 +355,10 @@ fn default_fs( #[derive(Clone)] pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); +/// Status flags for pipes +#[derive(Clone)] +pub(crate) struct PipeStatusFlags(pub litebox::fs::OFlags); + impl syscalls::file::FilesState { fn initialize_stdio_in_shared_descriptors_table(&self, global: &GlobalState) { use litebox::fs::{Mode, OFlags}; diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index f7c838761..430ac2954 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -143,7 +143,7 @@ impl EpollDescriptor { }; Some(poll(&proxy)) } - EpollDescriptor::Pipe(fd) => global.with_linux_pipe_iopollable(fd, poll).ok(), + EpollDescriptor::Pipe(fd) => global.pipes.with_iopollable(fd, poll).ok(), EpollDescriptor::Unix(fd) => { let handle = global.litebox.descriptor_table().entry_handle(fd)?; Some(handle.with_entry(|entry| poll(entry))) @@ -635,9 +635,10 @@ mod test { #[test] fn test_epoll_with_eventfd() { let (task, epoll) = setup_epoll(); - let eventfd = - crate::syscalls::eventfd::EventFile::new(&task.global.litebox, 0, EfdFlags::CLOEXEC) - .unwrap(); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::CLOEXEC) + .unwrap(); let typed = task .global .litebox @@ -732,9 +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(&task.global.litebox, 0, EfdFlags::empty()) - .unwrap(); + 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 99bd3f2bc..72a1401f2 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -6,13 +6,11 @@ use core::sync::atomic::AtomicU32; use litebox::{ - LiteBox, event::{ Events, IOPollable, counter::{EventCounter, EventCounterReadMode}, observer::Observer, - polling::Pollee, - polling::TryOpError, + polling::{Pollee, TryOpError}, wait::WaitContext, }, fd::{FdEnabledSubsystem, FdEnabledSubsystemEntry}, @@ -21,7 +19,8 @@ use litebox::{ 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 { @@ -29,155 +28,219 @@ impl FdEnabledSubsystem for EventfdSubsystem { } impl FdEnabledSubsystemEntry for EventFile {} +/// Backing counter for a Linux eventfd file description. +/// +/// Blocking eventfd still uses the shim-local implementation because the +/// local-core event counter does not support blocking operations yet. Once it +/// does, this split can collapse to the local-core counter. +enum EventFileCounter { + ShimLocal { + count: Mutex, + pollee: Pollee, + }, + LocalCore(EventCounter), +} + pub(crate) struct EventFile { - local_counter: Mutex, - local_core_event: Option>, + counter: EventFileCounter, /// File status flags (see [`OFlags::STATUS_FLAGS_MASK`]) status: AtomicU32, semaphore: bool, - pollee: Pollee, } -impl EventFile { - pub(crate) fn new( - litebox: &LiteBox, - count: u64, - flags: EfdFlags, - ) -> Result { - let mut status = OFlags::RDWR; - status.set(OFlags::NONBLOCK, flags.contains(EfdFlags::NONBLOCK)); - let local_core_event = if flags.contains(EfdFlags::NONBLOCK) { - Some(EventCounter::new(litebox, count).map_err(Errno::from)?) - } else { - None - }; - - Ok(Self { - local_counter: Mutex::new(count), - local_core_event, - 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 consume_mode(&self) -> EventCounterReadMode { - if self.semaphore { - EventCounterReadMode::One - } else { - EventCounterReadMode::All - } + fn local_core(counter: EventCounter) -> Self { + Self::LocalCore(counter) } - fn try_read(&self) -> Result> { - let mut counter = self.local_counter.lock(); - if *counter == 0 { - return Err(TryOpError::TryAgain); + fn supports_blocking_operations(&self) -> bool { + match self { + Self::ShimLocal { .. } => true, + Self::LocalCore(counter) => counter.supports_blocking_operations(), } + } - let res = if self.semaphore { 1 } else { *counter }; - *counter -= res; + 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), + } + } - drop(counter); - self.pollee.notify_observers(Events::OUT); - Ok(res) + 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), + } } - pub(crate) fn read(&self, cx: &WaitContext<'_, Platform>) -> Result { - if let Some(event) = &self.local_core_event { - return event - .read( - cx, - self.get_status().contains(OFlags::NONBLOCK), - self.consume_mode(), - ) - .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); } - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::IN, - || self.try_read(), - ) - .map_err(Errno::from) + + let res = if semaphore { 1 } else { *count }; + *count -= res; + + drop(count); + pollee.notify_observers(Events::OUT); + Ok(res) } - fn try_write(&self, value: u64) -> Result> { + fn try_write_local( + count: &Mutex, + pollee: &Pollee, + value: u64, + ) -> Result> { if value == u64::MAX { return Err(TryOpError::Other(Errno::EINVAL)); } - let mut counter = self.local_counter.lock(); - if let Some(new_value) = (*counter).checked_add(value) + let mut count = count.lock(); + if let Some(new_value) = (*count).checked_add(value) && new_value != u64::MAX { - *counter = new_value; - drop(counter); - self.pollee.notify_observers(Events::IN); + *count = new_value; + drop(count); + pollee.notify_observers(Events::IN); return Ok(core::mem::size_of::()); } Err(TryOpError::TryAgain) } - pub(crate) fn write(&self, cx: &WaitContext<'_, Platform>, value: u64) -> Result { - if let Some(event) = &self.local_core_event { - return event - .write(cx, self.get_status().contains(OFlags::NONBLOCK), value) - .map_err(Errno::from); + 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), } - self.pollee - .wait( - cx, - self.get_status().contains(OFlags::NONBLOCK), - Events::OUT, - || self.try_write(value), - ) - .map_err(Errno::from) + } + + 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.counter.write(cx, self.is_nonblocking(), value) } super::common_functions_for_file_status!(); pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { let new_status = (self.get_status() & mask.complement()) | (requested & mask); - if !new_status.contains(OFlags::NONBLOCK) - && self - .local_core_event - .as_ref() - .is_some_and(|event| !event.supports_blocking_operations()) - { + if !new_status.contains(OFlags::NONBLOCK) && !self.counter.supports_blocking_operations() { return Err(Errno::EINVAL); } self.set_status(requested & mask, true); self.set_status(requested.complement() & mask, false); Ok(()) } + + fn is_nonblocking(&self) -> bool { + self.get_status().contains(OFlags::NONBLOCK) + } } impl IOPollable for EventFile { fn check_io_events(&self) -> Events { - if let Some(event) = &self.local_core_event { - return event.check_io_events(); - } - - let counter = self.local_counter.lock(); - let mut events = Events::empty(); - if *counter != 0 { - events |= Events::IN; - } - if *counter < u64::MAX - 1 { - events |= Events::OUT; - } - events + self.counter.check_io_events() } fn register_observer(&self, observer: alloc::sync::Weak>, mask: Events) { - if let Some(event) = &self.local_core_event { - event.register_observer(observer, mask); - } else { - self.pollee.register_observer(observer, mask); + 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); } + + let count = u64::from(initval); + let counter = if flags.contains(EfdFlags::NONBLOCK) { + EventFileCounter::local_core( + EventCounter::new(&self.litebox, count).map_err(Errno::from)?, + ) + } else { + EventFileCounter::shim_local(count) + }; + Ok(EventFile::new(counter, flags)) + } +} + +fn consume_mode(semaphore: bool) -> EventCounterReadMode { + if semaphore { + EventCounterReadMode::One + } else { + EventCounterReadMode::All } } @@ -194,7 +257,9 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); let eventfd = alloc::sync::Arc::new( - super::EventFile::new(&task.global.litebox, 0, EfdFlags::SEMAPHORE).unwrap(), + task.global + .create_linux_eventfd(0, EfdFlags::SEMAPHORE) + .unwrap(), ); let total = 8; for _ in 0..total { @@ -217,7 +282,9 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); let eventfd = alloc::sync::Arc::new( - super::EventFile::new(&task.global.litebox, 0, EfdFlags::empty()).unwrap(), + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { @@ -244,7 +311,9 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); let eventfd = alloc::sync::Arc::new( - super::EventFile::new(&task.global.litebox, 0, EfdFlags::empty()).unwrap(), + task.global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(), ); let copied_eventfd = eventfd.clone(); std::thread::spawn(move || { @@ -266,7 +335,9 @@ mod tests { let task = crate::syscalls::tests::init_platform(None); assert_eq!( - super::EventFile::new(&task.global.litebox, 0, EfdFlags::NONBLOCK).map(|_| ()), + task.global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .map(|_| ()), Err(Errno::EIO) ); } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 85940c226..261479494 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -408,7 +408,9 @@ impl Task { |fd| { espipe_for_non_seekable_offset(offset)?; self.global - .read_linux_pipe(&self.wait_cx(), fd, &mut buf.borrow_mut()) + .pipes + .read(&self.wait_cx(), fd, &mut buf.borrow_mut()) + .map_err(Errno::from) }, |fd| { let handle = self @@ -479,7 +481,10 @@ impl Task { }, |fd| { espipe_for_non_seekable_offset(offset)?; - self.global.write_linux_pipe(&self.wait_cx(), fd, buf) + self.global + .pipes + .write(&self.wait_cx(), fd, buf) + .map_err(Errno::from) }, |fd| { let handle = self @@ -806,7 +811,7 @@ impl Task { files.fs.close(&fd).map_err(Errno::from) } ConsumedFd::Network(fd) => self.global.close_socket(&self.wait_cx(), fd), - ConsumedFd::Pipes(fd) => self.global.close_linux_pipe(&fd), + ConsumedFd::Pipes(fd) => self.global.pipes.close(&fd).map_err(Errno::from), ConsumedFd::Eventfd(fd) => { let entry = { let mut dt = self.global.litebox.descriptor_table_mut(); @@ -1405,10 +1410,14 @@ where }, |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { - Ok(T::from(synthetic( - task.global.linux_pipe_mode_bits(fd)?, - 4096, - ))) + let half_pipe_type = task.global.pipes.half_pipe_type(fd)?; + let read_write_mode = match half_pipe_type { + litebox::pipes::HalfPipeType::SenderHalf => Mode::WUSR, + litebox::pipes::HalfPipeType::ReceiverHalf => Mode::RUSR, + }; + let pipe_mode = + read_write_mode.bits() | litebox_common_linux::InodeType::NamedPipe as u32; + Ok(T::from(synthetic(pipe_mode, 4096))) }, |_fd| Ok(T::from(synthetic(rw_user_mode, 4096))), |_fd| Ok(T::from(synthetic(rw_user_mode, 0))), @@ -1642,7 +1651,7 @@ impl Task { desc, |fd| getfl_from_metadata!(fd, crate::StdioStatusFlags), |fd| getfl_from_metadata!(fd, crate::syscalls::net::SocketOFlags), - |fd| self.global.linux_pipe_status_flags(fd), + |fd| getfl_from_metadata!(fd, crate::PipeStatusFlags), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), @@ -1716,8 +1725,22 @@ impl Task { ) }, |fd| { + // Update the actual pipe non-blocking behavior self.global - .set_linux_pipe_status_flags(fd, flags, setfl_mask) + .pipes + .update_flags( + fd, + litebox::pipes::Flags::NON_BLOCKING, + flags.intersects(OFlags::NONBLOCK), + ) + .map_err(Errno::from)?; + // Record all status flags in metadata for F_GETFL + setfl_in_metadata!( + fd, + crate::PipeStatusFlags, + unreachable!("all pipes have PipeStatusFlags when created"), + |_| {} + ) }, |fd| { let handle = self @@ -1868,38 +1891,77 @@ impl Task { } } +const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; + impl Task { /// Handle syscall `pipe2` pub fn sys_pipe2(&self, flags: OFlags) -> Result<(u32, u32), Errno> { - let pipe = self.global.create_linux_pipe(flags)?; + let (pipe_flags, cloexec) = { + use litebox::pipes::Flags; + let mut f = Flags::empty(); + if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) + { + return Err(Errno::EINVAL); + } + f.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); + if flags.contains(OFlags::DIRECT) { + todo!("O_DIRECT not supported"); + } + (f, flags.contains(OFlags::CLOEXEC)) + }; + + let (writer, reader) = self.global.pipes.create_pipe( + DEFAULT_PIPE_BUF_SIZE, + pipe_flags, + // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. + core::num::NonZero::new(4096), + ); + + { + let initial_status = OFlags::from(pipe_flags); + let mut dt = self.global.litebox.descriptor_table_mut(); + let old = dt.set_entry_metadata( + &writer, + crate::PipeStatusFlags(initial_status | OFlags::WRONLY), + ); + assert!(old.is_none()); + let old = dt.set_entry_metadata( + &reader, + crate::PipeStatusFlags(initial_status | OFlags::RDONLY), + ); + assert!(old.is_none()); + } + + if cloexec { + let mut dt = self.global.litebox.descriptor_table_mut(); + let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + } let files = self.files.borrow(); - let wr_raw_fd = files.insert_raw_fd(pipe.writer).map_err(|writer| { - self.global.close_linux_pipe(&writer).unwrap(); + let wr_raw_fd = files.insert_raw_fd(writer).map_err(|writer| { + self.global.pipes.close(&writer).unwrap(); Errno::EMFILE })?; - let rd_raw_fd = files.insert_raw_fd(pipe.reader).map_err(|reader| { + let rd_raw_fd = files.insert_raw_fd(reader).map_err(|reader| { let writer = files .raw_descriptor_store .write() .fd_consume_raw_integer(wr_raw_fd) .unwrap(); - self.global.close_linux_pipe(&writer).unwrap(); - self.global.close_linux_pipe(&reader).unwrap(); + self.global.pipes.close(&writer).unwrap(); + self.global.pipes.close(&reader).unwrap(); Errno::EMFILE })?; Ok((rd_raw_fd.try_into().unwrap(), wr_raw_fd.try_into().unwrap())) } pub fn sys_eventfd2(&self, initval: u32, flags: EfdFlags) -> Result { - if flags - .intersects((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) - { - return Err(Errno::EINVAL); - } - - let eventfd = - super::eventfd::EventFile::new(&self.global.litebox, 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) { diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index b5fa1a82f..a138e2160 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -9,7 +9,6 @@ pub mod file; pub(crate) mod misc; pub(crate) mod mm; pub(crate) mod net; -pub(crate) mod pipe; pub mod process; pub(crate) mod unix; diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs deleted file mode 100644 index 90273c164..000000000 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -//! Linux ABI glue for the generic LiteBox pipe subsystem. -//! -//! `litebox::pipes` owns the in-process pipe buffer, endpoint, and readiness -//! mechanics. This module owns Linux-specific presentation: `pipe2` flags, -//! raw-fd metadata, `fcntl` status flags, and errno mapping. - -use core::num::NonZero; - -use litebox::{ - event::{IOPollable, wait::WaitContext}, - fd::MetadataError, - fs::{Mode, OFlags}, - pipes::{Flags, HalfPipeType, PipeFd}, -}; -use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; - -use crate::{GlobalState, Platform, ShimFS}; - -const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; - -/// Status flags for Linux pipe file descriptions. -/// -/// Access mode and Linux status flags are shim ABI state. The generic pipe -/// backend only needs the subset that affects pipe behavior, such as -/// nonblocking mode. -#[derive(Clone)] -pub(crate) struct PipeStatusFlags(OFlags); - -/// Both ends of a freshly created Linux pipe. -/// -/// `PipeFd` does not release the pipe on `Drop`; ends must either be inserted -/// into the fd table or explicitly released via [`GlobalState::close_linux_pipe`]. -pub(crate) struct LinuxPipeEnds { - pub(crate) reader: PipeFd, - pub(crate) writer: PipeFd, -} - -impl GlobalState { - pub(crate) fn create_linux_pipe(&self, flags: OFlags) -> Result { - let (pipe_flags, cloexec) = { - let mut pipe_flags = Flags::empty(); - if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) - { - return Err(Errno::EINVAL); - } - pipe_flags.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); - if flags.contains(OFlags::DIRECT) { - todo!("O_DIRECT not supported"); - } - (pipe_flags, flags.contains(OFlags::CLOEXEC)) - }; - - let (writer, reader) = self.pipes.create_pipe( - DEFAULT_PIPE_BUF_SIZE, - pipe_flags, - // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. - NonZero::new(4096), - ); - - let initial_status = OFlags::from(pipe_flags); - { - let mut dt = self.litebox.descriptor_table_mut(); - let old = - dt.set_entry_metadata(&writer, PipeStatusFlags(initial_status | OFlags::WRONLY)); - assert!(old.is_none()); - let old = - dt.set_entry_metadata(&reader, PipeStatusFlags(initial_status | OFlags::RDONLY)); - assert!(old.is_none()); - } - - if cloexec { - let mut dt = self.litebox.descriptor_table_mut(); - let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - } - - Ok(LinuxPipeEnds { reader, writer }) - } - - pub(crate) fn close_linux_pipe(&self, fd: &PipeFd) -> Result<(), Errno> { - self.pipes.close(fd).map_err(Errno::from) - } - - pub(crate) fn read_linux_pipe( - &self, - cx: &WaitContext<'_, Platform>, - fd: &PipeFd, - buf: &mut [u8], - ) -> Result { - self.pipes.read(cx, fd, buf).map_err(Errno::from) - } - - pub(crate) fn write_linux_pipe( - &self, - cx: &WaitContext<'_, Platform>, - fd: &PipeFd, - buf: &[u8], - ) -> Result { - self.pipes.write(cx, fd, buf).map_err(Errno::from) - } - - pub(crate) fn linux_pipe_status_flags(&self, fd: &PipeFd) -> Result { - self.litebox - .descriptor_table() - .with_metadata(fd, |PipeStatusFlags(flags)| { - *flags & OFlags::STATUS_FLAGS_MASK - }) - .map_err(metadata_to_errno) - } - - pub(crate) fn set_linux_pipe_status_flags( - &self, - fd: &PipeFd, - flags: OFlags, - setfl_mask: OFlags, - ) -> Result<(), Errno> { - self.pipes - .update_flags(fd, Flags::NON_BLOCKING, flags.intersects(OFlags::NONBLOCK)) - .map_err(Errno::from)?; - - self.litebox - .descriptor_table_mut() - .with_metadata_mut(fd, |PipeStatusFlags(current)| { - let diff = (*current & setfl_mask) ^ flags; - if diff.intersects(OFlags::APPEND | OFlags::DIRECT | OFlags::NOATIME) { - log_unsupported!("unsupported flags"); - } - current.toggle(diff); - }) - .map_err(metadata_to_errno) - } - - pub(crate) fn linux_pipe_mode_bits(&self, fd: &PipeFd) -> Result { - let read_write_mode = match self.pipes.half_pipe_type(fd)? { - HalfPipeType::SenderHalf => Mode::WUSR, - HalfPipeType::ReceiverHalf => Mode::RUSR, - }; - Ok(read_write_mode.bits() | InodeType::NamedPipe as u32) - } - - pub(crate) fn with_linux_pipe_iopollable( - &self, - fd: &PipeFd, - f: impl FnOnce(&dyn IOPollable) -> R, - ) -> Result { - self.pipes.with_iopollable(fd, f).map_err(Errno::from) - } -} - -fn metadata_to_errno(err: MetadataError) -> Errno { - match err { - MetadataError::ClosedFd => Errno::EBADF, - MetadataError::NoSuchMetadata => { - unreachable!("Linux pipe descriptors always carry PipeStatusFlags") - } - } -} From 3d49ca30a36347eb43aa5153010cb7d79418a5ef Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 17:10:50 -0700 Subject: [PATCH 33/66] Restore shim pipe module Restore the pipe module and helper call sites from main after the eventfd cleanup accidentally removed them. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/lib.rs | 4 - litebox_shim_linux/src/syscalls/epoll.rs | 2 +- litebox_shim_linux/src/syscalls/file.rs | 99 +++----------- litebox_shim_linux/src/syscalls/mod.rs | 1 + litebox_shim_linux/src/syscalls/pipe.rs | 164 +++++++++++++++++++++++ 5 files changed, 181 insertions(+), 89 deletions(-) create mode 100644 litebox_shim_linux/src/syscalls/pipe.rs diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index d030ac318..9fa295a24 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -355,10 +355,6 @@ fn default_fs( #[derive(Clone)] pub(crate) struct StdioStatusFlags(litebox::fs::OFlags); -/// Status flags for pipes -#[derive(Clone)] -pub(crate) struct PipeStatusFlags(pub litebox::fs::OFlags); - impl syscalls::file::FilesState { fn initialize_stdio_in_shared_descriptors_table(&self, global: &GlobalState) { use litebox::fs::{Mode, OFlags}; diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 430ac2954..50acc45ae 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -143,7 +143,7 @@ impl EpollDescriptor { }; Some(poll(&proxy)) } - EpollDescriptor::Pipe(fd) => global.pipes.with_iopollable(fd, poll).ok(), + EpollDescriptor::Pipe(fd) => global.with_linux_pipe_iopollable(fd, poll).ok(), EpollDescriptor::Unix(fd) => { let handle = global.litebox.descriptor_table().entry_handle(fd)?; Some(handle.with_entry(|entry| poll(entry))) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 261479494..1454594a8 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -408,9 +408,7 @@ impl Task { |fd| { espipe_for_non_seekable_offset(offset)?; self.global - .pipes - .read(&self.wait_cx(), fd, &mut buf.borrow_mut()) - .map_err(Errno::from) + .read_linux_pipe(&self.wait_cx(), fd, &mut buf.borrow_mut()) }, |fd| { let handle = self @@ -481,10 +479,7 @@ impl Task { }, |fd| { espipe_for_non_seekable_offset(offset)?; - self.global - .pipes - .write(&self.wait_cx(), fd, buf) - .map_err(Errno::from) + self.global.write_linux_pipe(&self.wait_cx(), fd, buf) }, |fd| { let handle = self @@ -811,7 +806,7 @@ impl Task { files.fs.close(&fd).map_err(Errno::from) } ConsumedFd::Network(fd) => self.global.close_socket(&self.wait_cx(), fd), - ConsumedFd::Pipes(fd) => self.global.pipes.close(&fd).map_err(Errno::from), + ConsumedFd::Pipes(fd) => self.global.close_linux_pipe(&fd), ConsumedFd::Eventfd(fd) => { let entry = { let mut dt = self.global.litebox.descriptor_table_mut(); @@ -1410,14 +1405,10 @@ where }, |_fd| Ok(T::from(synthetic(socket_mode, 4096))), |fd| { - let half_pipe_type = task.global.pipes.half_pipe_type(fd)?; - let read_write_mode = match half_pipe_type { - litebox::pipes::HalfPipeType::SenderHalf => Mode::WUSR, - litebox::pipes::HalfPipeType::ReceiverHalf => Mode::RUSR, - }; - let pipe_mode = - read_write_mode.bits() | litebox_common_linux::InodeType::NamedPipe as u32; - Ok(T::from(synthetic(pipe_mode, 4096))) + Ok(T::from(synthetic( + task.global.linux_pipe_mode_bits(fd)?, + 4096, + ))) }, |_fd| Ok(T::from(synthetic(rw_user_mode, 4096))), |_fd| Ok(T::from(synthetic(rw_user_mode, 0))), @@ -1651,7 +1642,7 @@ impl Task { desc, |fd| getfl_from_metadata!(fd, crate::StdioStatusFlags), |fd| getfl_from_metadata!(fd, crate::syscalls::net::SocketOFlags), - |fd| getfl_from_metadata!(fd, crate::PipeStatusFlags), + |fd| self.global.linux_pipe_status_flags(fd), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), |fd| getfl_from_handle!(fd), @@ -1725,22 +1716,8 @@ impl Task { ) }, |fd| { - // Update the actual pipe non-blocking behavior self.global - .pipes - .update_flags( - fd, - litebox::pipes::Flags::NON_BLOCKING, - flags.intersects(OFlags::NONBLOCK), - ) - .map_err(Errno::from)?; - // Record all status flags in metadata for F_GETFL - setfl_in_metadata!( - fd, - crate::PipeStatusFlags, - unreachable!("all pipes have PipeStatusFlags when created"), - |_| {} - ) + .set_linux_pipe_status_flags(fd, flags, setfl_mask) }, |fd| { let handle = self @@ -1891,70 +1868,24 @@ impl Task { } } -const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; - impl Task { /// Handle syscall `pipe2` pub fn sys_pipe2(&self, flags: OFlags) -> Result<(u32, u32), Errno> { - let (pipe_flags, cloexec) = { - use litebox::pipes::Flags; - let mut f = Flags::empty(); - if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) - { - return Err(Errno::EINVAL); - } - f.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); - if flags.contains(OFlags::DIRECT) { - todo!("O_DIRECT not supported"); - } - (f, flags.contains(OFlags::CLOEXEC)) - }; - - let (writer, reader) = self.global.pipes.create_pipe( - DEFAULT_PIPE_BUF_SIZE, - pipe_flags, - // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. - core::num::NonZero::new(4096), - ); - - { - let initial_status = OFlags::from(pipe_flags); - let mut dt = self.global.litebox.descriptor_table_mut(); - let old = dt.set_entry_metadata( - &writer, - crate::PipeStatusFlags(initial_status | OFlags::WRONLY), - ); - assert!(old.is_none()); - let old = dt.set_entry_metadata( - &reader, - crate::PipeStatusFlags(initial_status | OFlags::RDONLY), - ); - assert!(old.is_none()); - } - - if cloexec { - let mut dt = self.global.litebox.descriptor_table_mut(); - let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { - unreachable!() - }; - } + let pipe = self.global.create_linux_pipe(flags)?; let files = self.files.borrow(); - let wr_raw_fd = files.insert_raw_fd(writer).map_err(|writer| { - self.global.pipes.close(&writer).unwrap(); + let wr_raw_fd = files.insert_raw_fd(pipe.writer).map_err(|writer| { + self.global.close_linux_pipe(&writer).unwrap(); Errno::EMFILE })?; - let rd_raw_fd = files.insert_raw_fd(reader).map_err(|reader| { + let rd_raw_fd = files.insert_raw_fd(pipe.reader).map_err(|reader| { let writer = files .raw_descriptor_store .write() .fd_consume_raw_integer(wr_raw_fd) .unwrap(); - self.global.pipes.close(&writer).unwrap(); - self.global.pipes.close(&reader).unwrap(); + self.global.close_linux_pipe(&writer).unwrap(); + self.global.close_linux_pipe(&reader).unwrap(); Errno::EMFILE })?; Ok((rd_raw_fd.try_into().unwrap(), wr_raw_fd.try_into().unwrap())) diff --git a/litebox_shim_linux/src/syscalls/mod.rs b/litebox_shim_linux/src/syscalls/mod.rs index a138e2160..b5fa1a82f 100644 --- a/litebox_shim_linux/src/syscalls/mod.rs +++ b/litebox_shim_linux/src/syscalls/mod.rs @@ -9,6 +9,7 @@ pub mod file; pub(crate) mod misc; pub(crate) mod mm; pub(crate) mod net; +pub(crate) mod pipe; pub mod process; pub(crate) mod unix; diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs new file mode 100644 index 000000000..90273c164 --- /dev/null +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux ABI glue for the generic LiteBox pipe subsystem. +//! +//! `litebox::pipes` owns the in-process pipe buffer, endpoint, and readiness +//! mechanics. This module owns Linux-specific presentation: `pipe2` flags, +//! raw-fd metadata, `fcntl` status flags, and errno mapping. + +use core::num::NonZero; + +use litebox::{ + event::{IOPollable, wait::WaitContext}, + fd::MetadataError, + fs::{Mode, OFlags}, + pipes::{Flags, HalfPipeType, PipeFd}, +}; +use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; + +use crate::{GlobalState, Platform, ShimFS}; + +const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; + +/// Status flags for Linux pipe file descriptions. +/// +/// Access mode and Linux status flags are shim ABI state. The generic pipe +/// backend only needs the subset that affects pipe behavior, such as +/// nonblocking mode. +#[derive(Clone)] +pub(crate) struct PipeStatusFlags(OFlags); + +/// Both ends of a freshly created Linux pipe. +/// +/// `PipeFd` does not release the pipe on `Drop`; ends must either be inserted +/// into the fd table or explicitly released via [`GlobalState::close_linux_pipe`]. +pub(crate) struct LinuxPipeEnds { + pub(crate) reader: PipeFd, + pub(crate) writer: PipeFd, +} + +impl GlobalState { + pub(crate) fn create_linux_pipe(&self, flags: OFlags) -> Result { + let (pipe_flags, cloexec) = { + let mut pipe_flags = Flags::empty(); + if flags.intersects((OFlags::CLOEXEC | OFlags::NONBLOCK | OFlags::DIRECT).complement()) + { + return Err(Errno::EINVAL); + } + pipe_flags.set(Flags::NON_BLOCKING, flags.contains(OFlags::NONBLOCK)); + if flags.contains(OFlags::DIRECT) { + todo!("O_DIRECT not supported"); + } + (pipe_flags, flags.contains(OFlags::CLOEXEC)) + }; + + let (writer, reader) = self.pipes.create_pipe( + DEFAULT_PIPE_BUF_SIZE, + pipe_flags, + // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. + NonZero::new(4096), + ); + + let initial_status = OFlags::from(pipe_flags); + { + let mut dt = self.litebox.descriptor_table_mut(); + let old = + dt.set_entry_metadata(&writer, PipeStatusFlags(initial_status | OFlags::WRONLY)); + assert!(old.is_none()); + let old = + dt.set_entry_metadata(&reader, PipeStatusFlags(initial_status | OFlags::RDONLY)); + assert!(old.is_none()); + } + + if cloexec { + let mut dt = self.litebox.descriptor_table_mut(); + let None = dt.set_fd_metadata(&writer, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + let None = dt.set_fd_metadata(&reader, FileDescriptorFlags::FD_CLOEXEC) else { + unreachable!() + }; + } + + Ok(LinuxPipeEnds { reader, writer }) + } + + pub(crate) fn close_linux_pipe(&self, fd: &PipeFd) -> Result<(), Errno> { + self.pipes.close(fd).map_err(Errno::from) + } + + pub(crate) fn read_linux_pipe( + &self, + cx: &WaitContext<'_, Platform>, + fd: &PipeFd, + buf: &mut [u8], + ) -> Result { + self.pipes.read(cx, fd, buf).map_err(Errno::from) + } + + pub(crate) fn write_linux_pipe( + &self, + cx: &WaitContext<'_, Platform>, + fd: &PipeFd, + buf: &[u8], + ) -> Result { + self.pipes.write(cx, fd, buf).map_err(Errno::from) + } + + pub(crate) fn linux_pipe_status_flags(&self, fd: &PipeFd) -> Result { + self.litebox + .descriptor_table() + .with_metadata(fd, |PipeStatusFlags(flags)| { + *flags & OFlags::STATUS_FLAGS_MASK + }) + .map_err(metadata_to_errno) + } + + pub(crate) fn set_linux_pipe_status_flags( + &self, + fd: &PipeFd, + flags: OFlags, + setfl_mask: OFlags, + ) -> Result<(), Errno> { + self.pipes + .update_flags(fd, Flags::NON_BLOCKING, flags.intersects(OFlags::NONBLOCK)) + .map_err(Errno::from)?; + + self.litebox + .descriptor_table_mut() + .with_metadata_mut(fd, |PipeStatusFlags(current)| { + let diff = (*current & setfl_mask) ^ flags; + if diff.intersects(OFlags::APPEND | OFlags::DIRECT | OFlags::NOATIME) { + log_unsupported!("unsupported flags"); + } + current.toggle(diff); + }) + .map_err(metadata_to_errno) + } + + pub(crate) fn linux_pipe_mode_bits(&self, fd: &PipeFd) -> Result { + let read_write_mode = match self.pipes.half_pipe_type(fd)? { + HalfPipeType::SenderHalf => Mode::WUSR, + HalfPipeType::ReceiverHalf => Mode::RUSR, + }; + Ok(read_write_mode.bits() | InodeType::NamedPipe as u32) + } + + pub(crate) fn with_linux_pipe_iopollable( + &self, + fd: &PipeFd, + f: impl FnOnce(&dyn IOPollable) -> R, + ) -> Result { + self.pipes.with_iopollable(fd, f).map_err(Errno::from) + } +} + +fn metadata_to_errno(err: MetadataError) -> Errno { + match err { + MetadataError::ClosedFd => Errno::EBADF, + MetadataError::NoSuchMetadata => { + unreachable!("Linux pipe descriptors always carry PipeStatusFlags") + } + } +} From d2ec34c395f5a43e3ae68348dd89a597857c4295 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Wed, 3 Jun 2026 17:27:45 -0700 Subject: [PATCH 34/66] Fix eventfd writev zero-length handling Match Linux behavior by treating all-zero eventfd writev calls as a no-op while rejecting leading zero-length iovecs before data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/file.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 1454594a8..f4a92e51c 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -2866,7 +2866,7 @@ mod tests { fn eventfd_writev_rejects_leading_zero_before_value() { let task = crate::syscalls::tests::init_platform(None); let fd = task - .sys_eventfd2(5, EfdFlags::NONBLOCK) + .sys_eventfd2(5, EfdFlags::empty()) .expect("eventfd2 failed"); let fd = i32::try_from(fd).unwrap(); let value = 7u64.to_ne_bytes(); @@ -2889,14 +2889,13 @@ mod tests { let mut output = [0u8; 8]; assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); assert_eq!(u64::from_ne_bytes(output), 5); - assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); } #[test] fn eventfd_writev_all_zero_iovecs_is_noop() { let task = crate::syscalls::tests::init_platform(None); let fd = task - .sys_eventfd2(5, EfdFlags::NONBLOCK) + .sys_eventfd2(5, EfdFlags::empty()) .expect("eventfd2 failed"); let fd = i32::try_from(fd).unwrap(); let value = 7u64.to_ne_bytes(); @@ -2919,7 +2918,6 @@ mod tests { let mut output = [0u8; 8]; assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); assert_eq!(u64::from_ne_bytes(output), 5); - assert_eq!(task.sys_read(fd, &mut output, None), Err(Errno::EAGAIN)); } #[test] From 7b8bd9e1cfd1d6979de7c1ed68707562630b4b37 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 10:17:37 -0700 Subject: [PATCH 35/66] Prune redundant broker tests Consolidate overlapping unit tests and remove duplicate runner broker protocol coverage while preserving distinct eventfd, broker, and runner contracts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 21 ++--- litebox_broker_core/src/event.rs | 18 +---- litebox_broker_core/src/identity.rs | 7 +- litebox_broker_server/src/server.rs | 91 ++-------------------- litebox_runner_linux_userland/tests/run.rs | 34 -------- 5 files changed, 19 insertions(+), 152 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 74c56e79c..edcbb66e6 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -299,8 +299,8 @@ mod tests { } #[test] - fn zero_write_does_not_notify_read_observers() { - let litebox = litebox([create_response(), add_response(false, true)]); + fn zero_write_does_not_notify_read_observers_even_when_broker_reports_ready() { + let litebox = litebox([create_response(), add_response(true, true)]); let event = EventCounter::new(&litebox, 0).unwrap(); let (observer, observer_dyn) = observer(); event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); @@ -325,8 +325,12 @@ mod tests { } #[test] - fn read_does_not_notify_write_observers_without_broker_write_readiness() { - let litebox = litebox([create_response(), consume_response(1, false, false)]); + fn read_notifies_write_observers_only_when_broker_reports_write_ready() { + let litebox = litebox([ + create_response(), + consume_response(1, false, false), + consume_response(1, false, true), + ]); let event = EventCounter::new(&litebox, 0).unwrap(); let (observer, observer_dyn) = observer(); event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); @@ -340,16 +344,7 @@ mod tests { ); assert_eq!(observer.notifications(), 0); - } - #[test] - fn read_notifies_write_observers_when_broker_reports_write_ready() { - let litebox = litebox([create_response(), consume_response(1, false, true)]); - let event = EventCounter::new(&litebox, 0).unwrap(); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); - - let wait = WaitState::new(MockPlatform::new()); assert_eq!( event .read(&wait.context(), true, EventCounterReadMode::All) diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index f69118d69..4ff9596aa 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -158,7 +158,7 @@ mod tests { }; #[test] - fn wait_rejects_reference_without_wait_right() { + fn wait_rejects_invalid_references_with_expected_errors() { let mut core = BrokerCore::new(EventOnlyPolicy); let association = core .create_association(CallerCredential::Unauthenticated) @@ -176,14 +176,7 @@ mod tests { core.wait_event(&association, handle), Err(BrokerError::InvalidRights) ); - } - #[test] - fn wait_rejects_stale_reference_generation() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); let mut handle = core.create_event(&association).unwrap(); handle.reference_generation = ObjectReferenceGeneration::new(handle.reference_generation.get() + 1); @@ -192,18 +185,11 @@ mod tests { core.wait_event(&association, handle), Err(BrokerError::StaleHandle) ); - } - #[test] - fn wait_hides_handle_owned_by_another_association() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let owner = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); let other = core .create_association(CallerCredential::Unauthenticated) .unwrap(); - let handle = core.create_event(&owner).unwrap(); + let handle = core.create_event(&association).unwrap(); assert_eq!( core.wait_event(&other, handle), diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index e18954934..77dd48969 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -122,7 +122,7 @@ mod tests { use crate::DefaultDenyPolicy; #[test] - fn create_association_uses_one_session_and_distinct_processes() { + fn create_association_assigns_distinct_identities_and_preserves_credential() { let mut core = BrokerCore::new(DefaultDenyPolicy); let first = core @@ -132,10 +132,7 @@ mod tests { .create_association(CallerCredential::Unauthenticated) .unwrap(); - assert_eq!(first.identity.session_id, SessionId::FIRST); - assert_eq!(second.identity.session_id, SessionId::FIRST); - assert_eq!(first.identity.process_id, ProcessId::new(1)); - assert_eq!(second.identity.process_id, ProcessId::new(2)); + assert_ne!(first.identity(), second.identity()); assert_eq!(first.caller_credential(), CallerCredential::Unauthenticated); assert_eq!( second.caller_credential(), diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 70432e059..0dadf8093 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -319,9 +319,7 @@ where mod tests { use super::*; use litebox_broker_core::EventOnlyPolicy; - use litebox_broker_protocol::{ - AddEventRequest, CreateEventRequest, ReadinessState, WaitEventRequest, WaitOutcome, - }; + use litebox_broker_protocol::CreateEventRequest; #[test] fn dispatch_enforces_negotiation_state() { @@ -398,64 +396,20 @@ mod tests { } #[test] - fn dispatch_negotiates_then_routes_event_requests() { + fn dispatch_negotiates_then_routes_event_create() { let (mut core, association, mut state) = new_association(); negotiate(&mut core, &association, &mut state); let dispatch = handle_request(&mut core, &association, &mut state, event_create_request(0)); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - let handle = match dispatch.response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { - response.handle - } + match dispatch.response { + BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(_))) => {} response => panic!("unexpected response: {response:?}"), - }; - - let dispatch = handle_request( - &mut core, - &association, - &mut state, - event_request(EventRequest::Wait(WaitEventRequest::new(handle))), - ); - assert_eq!( - dispatch.response, - event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) - ))) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - - let dispatch = handle_request( - &mut core, - &association, - &mut state, - event_request(EventRequest::Add(AddEventRequest::new(handle, 1))), - ); - assert_eq!( - dispatch.response, - event_response(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(true, true, 1) - ))) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - - let dispatch = handle_request( - &mut core, - &association, - &mut state, - event_request(EventRequest::Wait(WaitEventRequest::new(handle))), - ); - assert_eq!( - dispatch.response, - event_response(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, true, 1)) - ))) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + } } #[test] - fn serve_connection_negotiates_routes_event_and_returns_peer_closed() { + fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed() { let mut core = BrokerCore::new(EventOnlyPolicy); let mut channel = FakeServerChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( @@ -485,14 +439,7 @@ mod tests { } response => panic!("unexpected response: {response:?}"), }; - - let probe = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - assert_eq!( - core.close_object_reference(&probe, handle), - Err(BrokerError::UnknownObject) - ); + assert_eq!(handle.reference_id.get(), 1); } #[test] @@ -520,28 +467,6 @@ mod tests { assert_eq!(channel.requests.len(), 1); } - #[test] - fn serve_connection_returns_channel_error_after_cleanup_path() { - let mut core = BrokerCore::new(EventOnlyPolicy); - let mut channel = FakeServerChannel::new(std::vec::Vec::from([ - Ok(Some(ReceivedBrokerRequest::Request( - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ))), - Ok(Some(ReceivedBrokerRequest::Request(event_create_request( - 0, - )))), - Err(FakeChannelError::Recv), - ])); - - match serve_connection(&mut core, &mut channel) { - Err(BrokerServeError::Channel(FakeChannelError::Recv)) => {} - result => panic!("unexpected serve result: {result:?}"), - } - assert_eq!(channel.responses.len(), 2); - } - #[test] fn serve_connection_returns_channel_error_when_response_send_fails() { let mut core = BrokerCore::new(EventOnlyPolicy); @@ -619,14 +544,12 @@ mod tests { #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum FakeChannelError { - Recv, Send, } impl fmt::Display for FakeChannelError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Recv => f.write_str("fake receive error"), Self::Send => f.write_str("fake send error"), } } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 69dc09009..6cf1d3bf7 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -291,40 +291,6 @@ fn test_runner_connects_to_broker() { let _ = std::fs::remove_file(socket_path); } -#[cfg(all(target_arch = "x86_64", target_os = "linux"))] -#[test] -fn test_broker_event_path_over_unix_socket() { - use litebox_broker_protocol::{ReadinessState, WaitOutcome}; - - let socket_path = unique_test_socket_path("broker-event"); - let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::EventOnlyPolicy); - - let mut channel = - litebox_broker_unix_socket::UnixStreamClientControlChannel::connect(&socket_path) - .expect("failed to connect to broker test socket"); - channel - .set_io_timeout(Some(std::time::Duration::from_secs(5))) - .expect("failed to configure broker test timeout"); - let mut client = litebox_broker_client::BrokerClient::new(channel); - client.negotiate().expect("broker negotiation failed"); - - let handle = client.create_event().expect("event create failed"); - assert_eq!( - client.wait_event(handle).expect("event wait failed"), - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) - ); - let readiness = client.add_event(handle, 1).expect("event add failed"); - assert_eq!(readiness, ReadinessState::new(true, true, 1)); - assert_eq!( - client.wait_event(handle).expect("event ready-wait failed"), - WaitOutcome::Ready(readiness) - ); - - drop(client); - broker_thread.join().expect("broker test server panicked"); - let _ = std::fs::remove_file(socket_path); -} - #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] fn test_broker_backed_eventfd_with_rewriter() { From f2c615467ea123f502fbe1ddb046796d28a83102 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 10:33:44 -0700 Subject: [PATCH 36/66] Preserve generic writev handling Keep eventfd-specific writev behavior while routing other descriptor types through the existing sys_write path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/file.rs | 102 +++++++++++++++--------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index f4a92e51c..5fc95f5bc 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -1169,46 +1169,36 @@ impl Task { // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with // output from writes in other processes - let files = self.files.borrow(); - let res = files - .run_on_raw_fd( - raw_fd, - |fd| { - write_to_iovec(iovs, |buf, _total| { - files.fs.write(fd, buf, None).map_err(Errno::from) - }) - }, - |fd| { - write_to_iovec(iovs, |buf, _total| { - self.global.sendto( - &self.wait_cx(), - fd, - buf, - litebox_common_linux::SendFlags::empty(), - None, - ) - }) - }, - |_fd| todo!("pipes"), - |fd| { - let handle = self - .global - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(Errno::EBADF)?; - write_eventfd_iovec(iovs, |value| { - handle.with_entry(|file| file.write(&self.wait_cx(), value)) - }) - }, - |_fd| Err(Errno::EINVAL), - |_fd| todo!("unix"), - ) - .flatten(); - if let Err(Errno::EPIPE) = res { - self.send_signal(Signal::SIGPIPE, signal::siginfo_kill(Signal::SIGPIPE)); + { + let files = self.files.borrow(); + if let Some(size) = files + .run_on_raw_fd( + raw_fd, + |_fd| Ok(None), + |_fd| Ok(None), + |_fd| Ok(None), + |fd| { + let handle = self + .global + .litebox + .descriptor_table() + .entry_handle(fd) + .ok_or(Errno::EBADF)?; + write_eventfd_iovec(iovs, |value| { + handle.with_entry(|file| file.write(&self.wait_cx(), value)) + }) + .map(Some) + }, + |_fd| Ok(None), + |_fd| Ok(None), + ) + .flatten()? + { + return Ok(size); + } } - res + + write_to_iovec(iovs, |buf, _total| self.sys_write(fd, buf, None)) } fn validate_access_mode(mode: &AccessFlags) -> Result<(), Errno> { @@ -2740,6 +2730,40 @@ mod tests { assert_eq!(calls.get(), 2); } + #[test] + fn writev_uses_generic_write_path_for_pipes() { + let task = crate::syscalls::tests::init_platform(None); + let (read_fd, write_fd) = task.sys_pipe2(OFlags::empty()).unwrap(); + let first = b"hello "; + let second = b"pipe"; + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(first.as_ptr()), + iov_len: first.len(), + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(second.as_ptr()), + iov_len: second.len(), + }, + ]; + + assert_eq!( + task.sys_writev( + i32::try_from(write_fd).unwrap(), + ConstPtr::from_ptr(iovs.as_ptr()), + iovs.len() + ), + Ok(first.len() + second.len()) + ); + + let mut output = [0u8; 10]; + assert_eq!( + task.sys_read(i32::try_from(read_fd).unwrap(), &mut output, None), + Ok(output.len()) + ); + assert_eq!(&output, b"hello pipe"); + } + #[test] fn read_from_iovec_breaks_on_eof() { let mut first = [0u8; 4]; From 738c871ad7791055423e0ee67aa653c46360e710 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 10:41:05 -0700 Subject: [PATCH 37/66] Update broker design eventfd status Clarify that the Unix-socket PoC now includes broker-backed nonblocking eventfd object routing through the local-core event counter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index 2dd3850b1..f837ae93b 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -313,7 +313,7 @@ The durable-unicorn Linux experiment provides a future hosted-userland profile w - the broker binds the mapped ring set to the host-authenticated spawned runner identity; - the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. -This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, and then starts the guest without issuing broker object operations as a startup smoke test. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. +This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. The initial Linux ring set can use five unidirectional rings: @@ -366,7 +366,7 @@ The eventual deployment contract should fail closed: 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. -The current hosted PoC implements only the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and guest-visible broker object routing remain future work. +The current hosted PoC implements the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. It also routes the migrated nonblocking `eventfd` object family through broker-backed local-core event counters. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and broader guest-visible broker object routing remain future work. UserLiteBox should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. From 9fe28eac30c774a63082810fe38dedb6892c4520 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 12:34:45 -0700 Subject: [PATCH 38/66] Address broker review followups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/broker/error.rs | 5 +- litebox/src/event/counter.rs | 8 + litebox_broker_client/src/event.rs | 27 +- litebox_broker_client/src/lib.rs | 31 +- litebox_broker_client/src/negotiate.rs | 6 + litebox_broker_client/src/worker.rs | 377 ++++++++++++++++-- litebox_broker_core/src/event.rs | 103 ++++- litebox_broker_core/src/identity.rs | 49 ++- litebox_broker_core/src/lib.rs | 47 ++- litebox_broker_core/src/object.rs | 106 +++-- litebox_broker_protocol/src/channel.rs | 4 +- litebox_broker_protocol/src/lib.rs | 10 +- litebox_broker_protocol/src/message.rs | 12 +- litebox_broker_server/src/server.rs | 6 +- litebox_broker_unix_socket/src/lib.rs | 183 +++++++-- litebox_broker_userland/src/main.rs | 60 ++- .../tests/userland_broker.rs | 69 +++- litebox_broker_wire/src/lib.rs | 47 ++- litebox_runner_linux_userland/src/broker.rs | 18 +- litebox_runner_linux_userland/tests/run.rs | 102 ++++- litebox_shim_linux/src/syscalls/eventfd.rs | 32 +- litebox_shim_linux/src/syscalls/file.rs | 43 +- litebox_shim_linux/src/syscalls/tests.rs | 4 + 23 files changed, 1134 insertions(+), 215 deletions(-) diff --git a/litebox/src/broker/error.rs b/litebox/src/broker/error.rs index 53936a3b6..2ffff3289 100644 --- a/litebox/src/broker/error.rs +++ b/litebox/src/broker/error.rs @@ -74,11 +74,12 @@ pub(crate) fn map_broker_object_result( impl From for EventCounterError { fn from(error: BrokerObjectError) -> Self { match error { - BrokerObjectError::InvalidObject => Self::InvalidInput, BrokerObjectError::WouldBlock => Self::WouldBlock, BrokerObjectError::ResourceExhausted => Self::ResourceExhausted, BrokerObjectError::UnexpectedResponse => Self::UnexpectedResponse, - _ => Self::Io, + BrokerObjectError::Control + | BrokerObjectError::InvalidObject + | BrokerObjectError::Internal => Self::Io, } } } diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index edcbb66e6..62b1d8448 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -298,6 +298,14 @@ mod tests { assert!(matches!(result, Err(EventCounterError::UnexpectedResponse))); } + #[test] + fn cached_broker_object_errors_map_to_io() { + assert_eq!( + EventCounterError::from(BrokerObjectError::InvalidObject), + EventCounterError::Io + ); + } + #[test] fn zero_write_does_not_notify_read_observers_even_when_broker_reports_ready() { let litebox = litebox([create_response(), add_response(true, true)]); diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_client/src/event.rs index 5a4b56279..c490937e2 100644 --- a/litebox_broker_client/src/event.rs +++ b/litebox_broker_client/src/event.rs @@ -4,11 +4,14 @@ use litebox_broker_protocol::{ AddEventRequest, BrokerRequest, BrokerResponse, ClientControlChannel, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, - EventRequest, EventResponse, ObjectHandle, ReadinessState, WaitEventRequest, WaitOutcome, + EventRequest, EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, + WaitOutcome, }; use crate::{BrokerClient, ClientError, Result}; +const EVENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); + impl BrokerClient { /// Creates a broker-owned event object. pub fn create_event(&mut self) -> Result { @@ -20,7 +23,7 @@ impl BrokerClient { &mut self, initial_count: u64, ) -> Result { - self.ensure_negotiated()?; + self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Create( CreateEventRequest::new(initial_count), )))? { @@ -33,7 +36,7 @@ impl BrokerClient { /// Checks whether an event wait would complete now. pub fn wait_event(&mut self, handle: ObjectHandle) -> Result { - self.ensure_negotiated()?; + self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Wait(WaitEventRequest::new( handle, ))))? { @@ -50,7 +53,7 @@ impl BrokerClient { handle: ObjectHandle, value: u64, ) -> Result { - self.ensure_negotiated()?; + self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Add(AddEventRequest::new( handle, value, ))))? { @@ -67,7 +70,7 @@ impl BrokerClient { handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - self.ensure_negotiated()?; + self.ensure_event_protocol()?; match self.request(event_request(EventRequest::Consume( ConsumeEventRequest::new(handle, mode), )))? { @@ -82,3 +85,17 @@ impl BrokerClient { const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } + +impl BrokerClient { + fn ensure_event_protocol(&self) -> Result<(), T::Error> { + let negotiated = self.ensure_negotiated()?; + if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { + Ok(()) + } else { + Err(ClientError::UnsupportedVersion { + requested: EVENT_PROTOCOL_VERSION, + broker_protocol_version: negotiated, + }) + } + } +} diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_client/src/lib.rs index 5bd4b3bdb..476705a71 100644 --- a/litebox_broker_client/src/lib.rs +++ b/litebox_broker_client/src/lib.rs @@ -132,6 +132,26 @@ mod tests { assert_eq!(client.channel.sent_request, None); } + #[test] + fn event_operations_require_event_protocol_version_without_sending() { + let channel = + FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::UnsupportedOperation))); + let mut client = BrokerClient::new(channel); + client.state = ConnectionState::Active { + negotiated_protocol_version: ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1), + }; + + assert!(matches!( + client.create_event(), + Err(ClientError::UnsupportedVersion { + requested, + broker_protocol_version + }) if requested == CLIENT_PROTOCOL_VERSION + && broker_protocol_version == ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1) + )); + assert_eq!(client.channel.sent_request, None); + } + #[test] fn negotiate_version_sends_requested_version_and_activates_client() { let requested = ProtocolVersion::new( @@ -155,9 +175,10 @@ mod tests { #[test] fn negotiate_version_rejects_incompatible_broker_response() { - let requested = ProtocolVersion::new(1, 1); + let requested = CLIENT_PROTOCOL_VERSION; + let broker_version = ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1); let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version: ProtocolVersion::new(1, 0), + broker_protocol_version: broker_version, })); let mut client = BrokerClient::new(channel); @@ -166,8 +187,7 @@ mod tests { Err(ClientError::IncompatibleNegotiation { requested: actual_requested, broker_protocol_version - }) if actual_requested == requested - && broker_protocol_version == ProtocolVersion::new(1, 0) + }) if actual_requested == requested && broker_protocol_version == broker_version )); assert_eq!(client.negotiated_protocol_version(), None); } @@ -192,6 +212,7 @@ mod tests { }) if requested == too_new && broker_protocol_version == fallback )); assert_eq!(client.negotiated_protocol_version(), None); + assert_eq!(client.channel.sent_request, None); client.channel.response = Some(BrokerResponse::Negotiated { broker_protocol_version: fallback, @@ -222,7 +243,7 @@ mod tests { &mut self, request: &BrokerRequest, ) -> core::result::Result<(), Self::Error> { - self.sent_request = Some(*request); + self.sent_request = Some(request.clone()); Ok(()) } diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_client/src/negotiate.rs index ef42c7481..00d11fa7a 100644 --- a/litebox_broker_client/src/negotiate.rs +++ b/litebox_broker_client/src/negotiate.rs @@ -27,6 +27,12 @@ impl BrokerClient { if self.state != crate::ConnectionState::AwaitingNegotiation { return Err(ClientError::AlreadyNegotiated); } + if !protocol_version.is_supported_by(CLIENT_PROTOCOL_VERSION) { + return Err(ClientError::UnsupportedVersion { + requested: protocol_version, + broker_protocol_version: CLIENT_PROTOCOL_VERSION, + }); + } let response = self.request(BrokerRequest::Negotiate { protocol_version })?; match response { diff --git a/litebox_broker_client/src/worker.rs b/litebox_broker_client/src/worker.rs index 02e4aac1b..88e72d414 100644 --- a/litebox_broker_client/src/worker.rs +++ b/litebox_broker_client/src/worker.rs @@ -3,11 +3,14 @@ use core::fmt; use std::{ + boxed::Box, + sync::mpsc::{self, Receiver, RecvTimeoutError}, sync::{ Arc, Condvar, Mutex, atomic::{AtomicBool, AtomicU8, Ordering}, }, thread::{self, JoinHandle, Thread}, + time::Duration, }; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ClientControlChannel}; @@ -19,6 +22,7 @@ const PHASE_RESERVED: u8 = 1; const PHASE_REQUEST_READY: u8 = 2; const PHASE_RESPONSE_READY: u8 = 3; const PHASE_SHUTDOWN: u8 = 4; +const DEFAULT_SHUTDOWN_WAIT: Duration = Duration::from_millis(100); /// Error returned by [`BrokerClientWorker`]. #[derive(Debug)] @@ -52,6 +56,7 @@ where } type WorkerResult = core::result::Result>; +type ShutdownHook = Box; /// Dedicated worker for broker clients that must not run channel I/O on the caller thread. /// @@ -68,6 +73,7 @@ where { state: Arc>, worker: Mutex>>, + finished: Mutex>, } struct BrokerClientWorkerState { @@ -78,6 +84,7 @@ struct BrokerClientWorkerState { requester_wait: Mutex<()>, requester_wakeup: Condvar, worker_thread: Mutex>, + shutdown_hook: Mutex>, } impl BrokerClientWorker @@ -92,6 +99,19 @@ where /// Panics if the worker-thread bookkeeping mutex is poisoned while the /// worker is being started. pub fn new(client: BrokerClient) -> Self { + Self::new_with_shutdown_hook(client, || {}) + } + + /// Starts a worker and registers a hook used to interrupt blocking channel I/O during shutdown. + /// + /// # Panics + /// + /// Panics if the worker-thread bookkeeping mutex is poisoned while the + /// worker is being started. + pub fn new_with_shutdown_hook(client: BrokerClient, shutdown_hook: F) -> Self + where + F: FnOnce() + Send + 'static, + { let state = Arc::new(BrokerClientWorkerState { shutdown_requested: AtomicBool::new(false), phase: AtomicU8::new(PHASE_IDLE), @@ -100,9 +120,14 @@ where requester_wait: Mutex::new(()), requester_wakeup: Condvar::new(), worker_thread: Mutex::new(None), + shutdown_hook: Mutex::new(Some(Box::new(shutdown_hook))), }); let worker_state = state.clone(); - let worker = thread::spawn(move || run_broker_client_worker(client, worker_state)); + let (finished_tx, finished_rx) = mpsc::channel(); + let worker = thread::spawn(move || { + run_broker_client_worker(client, worker_state); + let _ = finished_tx.send(()); + }); *state .worker_thread .lock() @@ -111,6 +136,7 @@ where Self { state, worker: Mutex::new(Some(worker)), + finished: Mutex::new(finished_rx), } } @@ -122,7 +148,12 @@ where self.state.submit(request) } - /// Shuts down the worker and waits for its thread to exit. + /// Shuts down the worker. + /// + /// If an in-flight blocking channel operation does not exit promptly, the + /// worker thread is detached after a short grace period so shutdown callers + /// are not blocked indefinitely. Use [`Self::new_with_shutdown_hook`] for + /// blocking channels that can be explicitly interrupted. /// /// # Panics /// @@ -135,7 +166,20 @@ where .expect("broker client worker mutex poisoned"); if let Some(worker) = worker.take() { self.state.request_shutdown(); - worker.join().expect("broker client worker panicked"); + match self + .finished + .lock() + .expect("broker client worker-finished mutex poisoned") + .recv_timeout(DEFAULT_SHUTDOWN_WAIT) + { + Ok(()) => worker.join().expect("broker client worker panicked"), + Err(RecvTimeoutError::Disconnected) => { + worker.join().expect("broker client worker panicked"); + } + Err(RecvTimeoutError::Timeout) => { + drop(worker); + } + } } } } @@ -169,7 +213,7 @@ impl BrokerClientWorkerState { .is_ok() { if self.shutdown_requested.load(Ordering::Acquire) { - self.store_phase_and_notify(PHASE_IDLE); + self.cancel_reserved_request_on_shutdown(); return Err(BrokerClientWorkerError::Shutdown); } break; @@ -188,10 +232,13 @@ impl BrokerClientWorkerState { self.wake_worker(); loop { + if self.shutdown_requested.load(Ordering::Acquire) { + return Err(BrokerClientWorkerError::Shutdown); + } match self.phase.load(Ordering::Acquire) { PHASE_RESPONSE_READY => break, PHASE_SHUTDOWN => return Err(BrokerClientWorkerError::Shutdown), - phase => self.wait_for_phase_change(phase, false), + phase => self.wait_for_phase_change(phase, true), } } @@ -205,6 +252,103 @@ impl BrokerClientWorkerState { response } + fn request_aborted_before_send(&self) -> bool { + if self.shutdown_requested.load(Ordering::Acquire) { + self.store_phase_and_notify(PHASE_SHUTDOWN); + self.wake_worker(); + true + } else { + false + } + } + + fn publish_worker_response(&self, response: WorkerResult) -> bool { + if self.shutdown_requested.load(Ordering::Acquire) { + self.store_phase_and_notify(PHASE_SHUTDOWN); + false + } else { + *self + .response + .lock() + .expect("broker client worker response mutex poisoned") = Some(response); + self.store_phase_and_notify(PHASE_RESPONSE_READY); + true + } + } + + fn take_request(&self) -> Option { + if self.request_aborted_before_send() { + return None; + } + let request = self + .request + .lock() + .expect("broker client worker request mutex poisoned") + .take() + .expect("broker client worker request missing"); + if self.shutdown_requested.load(Ordering::Acquire) { + self.store_phase_and_notify(PHASE_SHUTDOWN); + None + } else { + Some(request) + } + } + + fn cancel_reserved_request_on_shutdown(&self) { + if self + .phase + .compare_exchange( + PHASE_RESERVED, + PHASE_SHUTDOWN, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + { + self.wake_worker(); + self.requester_wakeup.notify_all(); + } + } + + fn worker_should_shutdown(&self) -> bool { + self.shutdown_requested.load(Ordering::Acquire) + || self.phase.load(Ordering::Acquire) == PHASE_SHUTDOWN + } + + fn wait_for_work_or_shutdown(&self) -> bool { + if self.worker_should_shutdown() { + return false; + } + thread::park(); + !self.worker_should_shutdown() + } + + fn transition_idle_to_shutdown(&self) -> bool { + self.phase + .compare_exchange( + PHASE_IDLE, + PHASE_SHUTDOWN, + Ordering::AcqRel, + Ordering::Acquire, + ) + .is_ok() + } + + fn cancel_in_flight_request(&self) { + self.store_phase_and_notify(PHASE_SHUTDOWN); + self.wake_worker(); + } + + fn request_shutdown_without_waiting(&self) { + if self.transition_idle_to_shutdown() { + self.wake_worker(); + } else if self.phase.load(Ordering::Acquire) == PHASE_RESERVED { + self.cancel_reserved_request_on_shutdown(); + } else { + self.cancel_in_flight_request(); + } + } + fn request_shutdown(&self) { { let _guard = self @@ -214,27 +358,8 @@ impl BrokerClientWorkerState { self.shutdown_requested.store(true, Ordering::Release); self.requester_wakeup.notify_all(); } - loop { - match self.phase.load(Ordering::Acquire) { - PHASE_IDLE => { - if self - .phase - .compare_exchange( - PHASE_IDLE, - PHASE_SHUTDOWN, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - self.wake_worker(); - return; - } - } - PHASE_SHUTDOWN => return, - phase => self.wait_for_phase_change(phase, false), - } - } + self.run_shutdown_hook(); + self.request_shutdown_without_waiting(); } fn store_phase_and_notify(&self, phase: u8) { @@ -271,6 +396,17 @@ impl BrokerClientWorkerState { worker.unpark(); } } + + fn run_shutdown_hook(&self) { + if let Some(hook) = self + .shutdown_hook + .lock() + .expect("broker client worker shutdown-hook mutex poisoned") + .take() + { + hook(); + } + } } fn run_broker_client_worker( @@ -282,23 +418,22 @@ fn run_broker_client_worker( loop { match state.phase.load(Ordering::Acquire) { PHASE_REQUEST_READY => { - let request = state - .request - .lock() - .expect("broker client worker request mutex poisoned") - .take() - .expect("broker client worker request missing"); + let Some(request) = state.take_request() else { + break; + }; let response = client .active_raw_request(request) .map_err(BrokerClientWorkerError::Client); - *state - .response - .lock() - .expect("broker client worker response mutex poisoned") = Some(response); - state.store_phase_and_notify(PHASE_RESPONSE_READY); + if !state.publish_worker_response(response) { + break; + } } PHASE_SHUTDOWN => break, - _ => thread::park(), + _ => { + if !state.wait_for_work_or_shutdown() { + break; + } + } } } } @@ -308,7 +443,9 @@ mod tests { use core::convert::Infallible; use std::{ collections::VecDeque, - sync::{Arc, Mutex}, + io, + sync::{Arc, Condvar, Mutex}, + time::{Duration, Instant}, vec::Vec, }; @@ -369,6 +506,117 @@ mod tests { )); } + #[test] + fn worker_shutdown_interrupts_in_flight_channel_io() { + let sent = Arc::new(Mutex::new(Vec::new())); + let interrupted = Arc::new((Mutex::new(false), Condvar::new())); + let channel = BlockingControlChannel::new(sent, interrupted.clone()); + let mut client = BrokerClient::new(channel); + client.negotiate().unwrap(); + let worker = Arc::new(BrokerClientWorker::new_with_shutdown_hook(client, { + let interrupted = interrupted.clone(); + move || { + let (lock, wakeup) = &*interrupted; + *lock.lock().unwrap() = true; + wakeup.notify_all(); + } + })); + + let requester = { + let worker = worker.clone(); + thread::spawn(move || { + worker.active_raw_request(BrokerRequest::Negotiate { + protocol_version: CLIENT_PROTOCOL_VERSION, + }) + }) + }; + + while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { + thread::yield_now(); + } + worker.shutdown(); + + assert!(matches!( + requester.join().unwrap(), + Err(BrokerClientWorkerError::Shutdown + | BrokerClientWorkerError::Client(ClientError::Channel(_))) + )); + } + + #[test] + fn worker_default_shutdown_does_not_wait_forever_for_in_flight_channel_io() { + let sent = Arc::new(Mutex::new(Vec::new())); + let interrupted = Arc::new((Mutex::new(false), Condvar::new())); + let channel = BlockingControlChannel::new(sent, interrupted); + let mut client = BrokerClient::new(channel); + client.negotiate().unwrap(); + let worker = Arc::new(BrokerClientWorker::new(client)); + + let requester = { + let worker = worker.clone(); + thread::spawn(move || { + worker.active_raw_request(BrokerRequest::Negotiate { + protocol_version: CLIENT_PROTOCOL_VERSION, + }) + }) + }; + + while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { + thread::yield_now(); + } + let started = Instant::now(); + worker.shutdown(); + + assert!( + started.elapsed() < Duration::from_secs(1), + "default worker shutdown waited too long" + ); + assert!(matches!( + requester.join().unwrap(), + Err(BrokerClientWorkerError::Shutdown) + )); + } + + #[test] + fn worker_shutdown_hook_interrupts_worker_thread() { + let sent = Arc::new(Mutex::new(Vec::new())); + let interrupted = Arc::new((Mutex::new(false), Condvar::new())); + let channel = BlockingControlChannel::new(sent, interrupted.clone()); + let mut client = BrokerClient::new(channel); + client.negotiate().unwrap(); + let worker = Arc::new(BrokerClientWorker::new_with_shutdown_hook(client, { + let interrupted = interrupted.clone(); + move || { + let (lock, wakeup) = &*interrupted; + *lock.lock().unwrap() = true; + wakeup.notify_all(); + } + })); + + let requester = { + let worker = worker.clone(); + thread::spawn(move || { + worker.active_raw_request(BrokerRequest::Negotiate { + protocol_version: CLIENT_PROTOCOL_VERSION, + }) + }) + }; + + while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { + thread::yield_now(); + } + worker.shutdown(); + + let (lock, _) = &*interrupted; + assert!(*lock.lock().unwrap(), "shutdown hook was not invoked"); + match requester.join().unwrap() { + Err(BrokerClientWorkerError::Shutdown) => {} + Err(BrokerClientWorkerError::Client(ClientError::Channel(error))) + if error.kind() == io::ErrorKind::Interrupted => {} + result => panic!("unexpected requester result: {result:?}"), + } + } + struct FakeControlChannel { sent: Arc>>, responses: VecDeque, @@ -390,7 +638,7 @@ mod tests { type Error = Infallible; fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error> { - self.sent.lock().unwrap().push(*request); + self.sent.lock().unwrap().push(request.clone()); Ok(()) } @@ -401,4 +649,53 @@ mod tests { .map(ReceivedBrokerResponse::Response)) } } + + struct BlockingControlChannel { + sent: Arc>>, + interrupted: Arc<(Mutex, Condvar)>, + negotiated: bool, + } + + impl BlockingControlChannel { + fn new( + sent: Arc>>, + interrupted: Arc<(Mutex, Condvar)>, + ) -> Self { + Self { + sent, + interrupted, + negotiated: false, + } + } + } + + impl ClientControlChannel for BlockingControlChannel { + type Error = io::Error; + + fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error> { + self.sent.lock().unwrap().push(request.clone()); + Ok(()) + } + + fn recv_response(&mut self) -> Result, Self::Error> { + if !self.negotiated { + self.negotiated = true; + return Ok(Some(ReceivedBrokerResponse::Response( + BrokerResponse::Negotiated { + broker_protocol_version: CLIENT_PROTOCOL_VERSION, + }, + ))); + } + + let (lock, wakeup) = &*self.interrupted; + let mut interrupted = lock.lock().unwrap(); + while !*interrupted { + interrupted = wakeup.wait(interrupted).unwrap(); + } + Err(io::Error::new( + io::ErrorKind::Interrupted, + "interrupted by shutdown", + )) + } + } } diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 4ff9596aa..a4110dbc6 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -3,11 +3,10 @@ use crate::object::{ObjectId, ObjectKind}; use crate::{ - BrokerAssociation, BrokerCore, BrokerError, ObjectHandle, ObjectRights, ObjectType, - PolicyEngine, Result, + BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, Result, }; use litebox_broker_protocol::{ - ConsumeEventResponse, EventConsumeMode, ReadinessState, WaitOutcome, + ConsumeEventResponse, EventConsumeMode, ObjectHandle, ReadinessState, WaitOutcome, }; const MAX_EVENT_COUNT: u64 = u64::MAX - 1; @@ -47,9 +46,12 @@ impl BrokerCore

{ association: &BrokerAssociation, handle: ObjectHandle, ) -> Result { - let object_id = + let authorized = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; - let state = self.event_state(object_id)?; + let state = Self::filter_readiness_for_rights( + self.event_state(authorized.object_id)?, + authorized.rights, + ); Ok(if state.read_ready { WaitOutcome::Ready(state) } else { @@ -64,10 +66,12 @@ impl BrokerCore

{ handle: ObjectHandle, value: u64, ) -> Result { - let object_id = + let authorized = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WRITE)?; - match &mut self.object_mut(object_id)?.kind { - ObjectKind::Event(event) => event.add(value), + 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)), } } @@ -78,13 +82,26 @@ impl BrokerCore

{ handle: ObjectHandle, mode: EventConsumeMode, ) -> Result { - let object_id = + let authorized = self.authorize_use_object(association, handle, ObjectType::Event, ObjectRights::WAIT)?; - match &mut self.object_mut(object_id)?.kind { - ObjectKind::Event(event) => event.consume(mode), + match &mut self.object_mut(authorized.object_id)?.kind { + ObjectKind::Event(event) => event.consume(mode).map(|response| { + ConsumeEventResponse::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()), @@ -120,8 +137,9 @@ impl EventObject { .checked_add(value) .filter(|count| *count <= MAX_EVENT_COUNT) .ok_or(BrokerError::WouldBlock)?; + let next_generation = self.next_generation()?; self.count = new_count; - self.bump_generation()?; + self.readiness_generation = next_generation; Ok(self.readiness_state()) } @@ -135,17 +153,16 @@ impl EventObject { EventConsumeMode::One => 1, _ => return Err(BrokerError::UnsupportedOperation), }; + let next_generation = self.next_generation()?; self.count -= value; - self.bump_generation()?; + self.readiness_generation = next_generation; Ok(ConsumeEventResponse::new(value, self.readiness_state())) } - fn bump_generation(&mut self) -> Result<()> { - self.readiness_generation = self - .readiness_generation + fn next_generation(&self) -> Result { + self.readiness_generation .checked_add(1) - .ok_or(BrokerError::ResourceExhausted)?; - Ok(()) + .ok_or(BrokerError::ResourceExhausted) } } @@ -153,9 +170,10 @@ impl EventObject { mod tests { use super::*; use crate::{ - BrokerCore, CallerCredential, EventOnlyPolicy, ObjectOperation, ObjectReferenceGeneration, - PolicyDecision, PolicyEngine, PolicyOperation, + BrokerCore, CallerCredential, EventOnlyPolicy, ObjectOperation, PolicyDecision, + PolicyEngine, PolicyOperation, }; + use litebox_broker_protocol::ObjectReferenceGeneration; #[test] fn wait_rejects_invalid_references_with_expected_errors() { @@ -216,6 +234,51 @@ mod tests { ); } + #[test] + fn event_readiness_state_only_reports_authorized_directions() { + let mut core = BrokerCore::new(WaitOnlyCreatePolicy); + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let handle = core.create_event_with_count(&association, 1).unwrap(); + + assert!(matches!( + core.wait_event(&association, handle), + Ok(WaitOutcome::Ready(ReadinessState { + read_ready: true, + write_ready: false, + .. + })) + )); + } + + #[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); + } + struct WaitOnlyCreatePolicy; impl PolicyEngine for WaitOnlyCreatePolicy { diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 77dd48969..6b8e08e6f 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +use core::sync::atomic::{AtomicU64, Ordering}; + use crate::{BrokerCore, Result, allocate_id}; /// Caller identity information supplied by the broker entry layer. @@ -30,6 +32,17 @@ macro_rules! id_type { }; } +id_type! { + /// Process-local broker core identity. + BrokerCoreId +} + +static NEXT_CORE_ID: AtomicU64 = AtomicU64::new(1); + +pub(crate) fn allocate_core_id() -> BrokerCoreId { + BrokerCoreId::new(NEXT_CORE_ID.fetch_add(1, Ordering::Relaxed)) +} + id_type! { /// Broker-assigned sandbox session identity. SessionId @@ -47,15 +60,17 @@ id_type! { /// Broker-assigned identity for one caller association. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) struct AssociationIdentity { - session_id: SessionId, - process_id: ProcessId, + core: BrokerCoreId, + session: SessionId, + process: ProcessId, } impl AssociationIdentity { - const fn new(session_id: SessionId, process_id: ProcessId) -> Self { + const fn new(core_id: BrokerCoreId, session_id: SessionId, process_id: ProcessId) -> Self { Self { - session_id, - process_id, + core: core_id, + session: session_id, + process: process_id, } } } @@ -64,9 +79,8 @@ impl AssociationIdentity { /// /// 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. The current architecture expects one BrokerCore per -/// broker, so this token is scoped by broker-assigned session and process -/// identity rather than by a separate core identifier. +/// on that association. The token is scoped by a process-local BrokerCore +/// identity so handles cannot be replayed across distinct core instances. #[derive(Debug, PartialEq, Eq)] pub struct BrokerAssociation { /// Broker-assigned sandbox session and guest process identity. @@ -78,12 +92,13 @@ pub struct BrokerAssociation { impl BrokerAssociation { /// Creates an authenticated association identity. pub(crate) const fn new( + core_id: BrokerCoreId, session_id: SessionId, process_id: ProcessId, caller_credential: CallerCredential, ) -> Self { Self { - identity: AssociationIdentity::new(session_id, process_id), + identity: AssociationIdentity::new(core_id, session_id, process_id), caller_credential, } } @@ -108,6 +123,7 @@ impl

BrokerCore

{ // The POC models one sandbox session per BrokerCore; multi-session // allocation belongs with the future deployment/session manager. let association = BrokerAssociation::new( + self.core_id, SessionId::FIRST, ProcessId::new(process_id), caller_credential, @@ -139,4 +155,19 @@ mod tests { CallerCredential::Unauthenticated ); } + + #[test] + fn create_association_scopes_identity_to_core_instance() { + let mut first_core = BrokerCore::new(DefaultDenyPolicy); + let mut second_core = BrokerCore::new(DefaultDenyPolicy); + + let first = first_core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let second = second_core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + assert_ne!(first.identity(), second.identity()); + } } diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 739a5b873..3c1811a4b 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -23,11 +23,9 @@ mod policy; use alloc::collections::BTreeMap; pub use error::BrokerError; +use identity::BrokerCoreId; pub use identity::{BrokerAssociation, CallerCredential}; -pub use litebox_broker_protocol::{ - ConsumeEventResponse, EventConsumeMode, ObjectHandle, ObjectReferenceGeneration, - ObjectReferenceId, ReadinessState, WaitOutcome, -}; +use litebox_broker_protocol::ObjectReferenceId; use object::{ObjectEntry, ObjectId, ObjectReference}; pub use object::{ObjectRights, ObjectType}; pub use policy::{ @@ -38,9 +36,43 @@ pub use policy::{ /// 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 the first broker proof of concept. + 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. pub struct BrokerCore

{ + core_id: BrokerCoreId, policy: P, + limits: BrokerCoreLimits, next_process_id: u64, next_object_id: u64, next_reference_id: u64, @@ -51,8 +83,15 @@ pub struct BrokerCore

{ impl

BrokerCore

{ /// Creates a broker core with the provided policy engine. pub fn new(policy: P) -> Self { + Self::new_with_limits(policy, BrokerCoreLimits::DEFAULT) + } + + /// Creates a broker core with explicit authority-state limits. + pub fn new_with_limits(policy: P, limits: BrokerCoreLimits) -> Self { Self { + core_id: identity::allocate_core_id(), policy, + limits, next_process_id: 1, next_object_id: 1, next_reference_id: 1, diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 18dde901b..408796bf3 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use alloc::vec::Vec; use core::ops::BitOr; use crate::event::EventObject; @@ -112,6 +111,12 @@ impl BrokerCore

{ 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; @@ -151,14 +156,19 @@ impl BrokerCore

{ handle: ObjectHandle, object_type: ObjectType, rights: ObjectRights, - ) -> Result { - let object_id = self.validate_handle(association, handle, object_type, rights)?; + ) -> 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(object_id), + PolicyDecision::Authorized => Ok(AuthorizedObject { + object_id, + rights: reference_rights, + }), _ => Err(BrokerError::InvalidPolicyDecision), } } @@ -181,7 +191,7 @@ impl BrokerCore

{ handle: ObjectHandle, expected_type: ObjectType, required_rights: ObjectRights, - ) -> Result { + ) -> Result { let reference = self.reference_for_handle(association, handle)?; if reference.object_type != expected_type { return Err(BrokerError::WrongObjectType); @@ -198,7 +208,7 @@ impl BrokerCore

{ return Err(BrokerError::WrongObjectType); } - Ok(reference.object_id) + Ok(*reference) } fn allocate_object_id(&mut self) -> Result { @@ -232,24 +242,14 @@ impl

BrokerCore

{ /// Closes a broker association and releases references owned by it. pub fn close_association(&mut self, association: BrokerAssociation) { let identity = association.identity(); - let reference_ids = self - .references - .iter() - .filter_map(|(reference_id, reference)| { - (reference.owner == identity).then_some(*reference_id) - }) - .collect::>(); - - let mut object_ids = Vec::new(); - for reference_id in reference_ids { - if let Some(reference) = self.references.remove(&reference_id) { - object_ids.push(reference.object_id); - } - } - - for object_id in object_ids { - self.drop_object_if_unreferenced(object_id); - } + self.references + .retain(|_, reference| reference.owner != identity); + let references = &self.references; + self.objects.retain(|object_id, _| { + references + .values() + .any(|reference| reference.object_id == *object_id) + }); } fn reference_for_handle( @@ -281,10 +281,18 @@ impl

BrokerCore

{ } } +#[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, DefaultDenyPolicy, EventOnlyPolicy}; + use crate::{ + BrokerCoreLimits, BrokerError, CallerCredential, DefaultDenyPolicy, EventOnlyPolicy, + }; #[test] fn object_and_reference_allocators_issue_max_id_then_exhaust() { @@ -318,6 +326,31 @@ mod tests { ); } + #[test] + fn insert_object_with_reference_enforces_object_and_reference_limits() { + let mut object_limited = + BrokerCore::new_with_limits(EventOnlyPolicy, BrokerCoreLimits::new(0, 1)); + let association = object_limited + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + assert_eq!( + object_limited.create_event(&association), + Err(BrokerError::ResourceExhausted) + ); + + let mut reference_limited = + BrokerCore::new_with_limits(EventOnlyPolicy, BrokerCoreLimits::new(1, 0)); + let association = reference_limited + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + assert_eq!( + reference_limited.create_event(&association), + Err(BrokerError::ResourceExhausted) + ); + } + #[test] fn close_association_releases_owned_references_and_orphaned_objects() { let mut core = BrokerCore::new(EventOnlyPolicy); @@ -335,6 +368,27 @@ mod tests { assert!(core.objects.is_empty()); } + #[test] + fn foreign_core_association_cannot_authorize_matching_handle_values() { + let mut owner_core = BrokerCore::new(EventOnlyPolicy); + let owner = owner_core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let handle = owner_core.create_event(&owner).unwrap(); + + let mut other_core = BrokerCore::new(EventOnlyPolicy); + let other = other_core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + let other_handle = other_core.create_event(&other).unwrap(); + assert_eq!(handle, other_handle); + + assert_eq!( + other_core.wait_event(&owner, handle), + Err(BrokerError::UnknownObject) + ); + } + #[test] fn close_object_reference_releases_reference_and_orphaned_object() { let mut core = BrokerCore::new(EventOnlyPolicy); @@ -378,7 +432,7 @@ mod tests { ); assert!(matches!( core.wait_event(&owner, handle), - Ok(crate::WaitOutcome::WouldBlock(_)) + Ok(litebox_broker_protocol::WaitOutcome::WouldBlock(_)) )); } } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index 5b92f16f9..a3486881d 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -21,7 +21,7 @@ pub enum PeerCredential { } /// Broker authority request received from a control channel. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ReceivedBrokerRequest { /// A request understood by the current protocol crate. @@ -37,7 +37,7 @@ impl From for ReceivedBrokerRequest { } /// Broker authority response received from a control channel. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum ReceivedBrokerResponse { /// A response understood by the current protocol crate. diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 3260a284a..a67a8dcd3 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -10,11 +10,11 @@ #![no_std] -mod channel; -mod error; -mod event; -mod message; -mod object; +pub mod channel; +pub mod error; +pub mod event; +pub mod message; +pub mod object; pub use channel::{ ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index f1b97f4c8..126ae6e0c 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -12,7 +12,7 @@ use crate::{ /// 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, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum BrokerRequest { /// Protocol negotiation request. @@ -25,7 +25,7 @@ pub enum BrokerRequest { } /// Request adapted by the broker server into a BrokerCore domain call. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CoreRequest { /// Event object request family. @@ -33,7 +33,7 @@ pub enum CoreRequest { } /// Broker-owned event object request. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum EventRequest { /// Create a broker-owned event object. @@ -51,7 +51,7 @@ pub enum EventRequest { /// 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, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum BrokerResponse { /// Negotiation result. @@ -79,7 +79,7 @@ pub enum BrokerResponse { } /// Response returned by a BrokerCore domain request. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CoreResponse { /// Event object response family. @@ -87,7 +87,7 @@ pub enum CoreResponse { } /// Broker-owned event object response. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum EventResponse { /// Create operation response. diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_server/src/server.rs index 0dadf8093..f573481d4 100644 --- a/litebox_broker_server/src/server.rs +++ b/litebox_broker_server/src/server.rs @@ -230,7 +230,7 @@ enum ConnectionState { }, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] struct BrokerDispatch { response: BrokerResponse, outcome: DispatchOutcome, @@ -433,7 +433,7 @@ mod tests { broker_protocol_version: SUPPORTED_PROTOCOL_VERSION } ); - let handle = match channel.responses[1] { + let handle = match &channel.responses[1] { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { response.handle } @@ -602,7 +602,7 @@ mod tests { if let Some(error) = self.send_error { return Err(error); } - self.responses.push(*response); + self.responses.push(response.clone()); Ok(()) } } diff --git a/litebox_broker_unix_socket/src/lib.rs b/litebox_broker_unix_socket/src/lib.rs index 8742eeb20..7b302af9a 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_unix_socket/src/lib.rs @@ -24,7 +24,9 @@ const MAX_FRAME_LEN: usize = 64 * 1024; /// Client-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamClientControlChannel { stream: UnixStream, + io_timeout: Option, io_deadline: Option, + active_request_deadline: Option, } impl UnixStreamClientControlChannel { @@ -32,7 +34,9 @@ impl UnixStreamClientControlChannel { pub const fn from_connected(stream: UnixStream) -> Self { Self { stream, + io_timeout: None, io_deadline: None, + active_request_deadline: None, } } @@ -43,34 +47,82 @@ impl UnixStreamClientControlChannel { /// 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(None), + None => self.set_stream_io_timeout(self.io_timeout), } } + /// Clones the underlying stream so another owner can interrupt blocking I/O. + pub fn try_clone_stream(&self) -> io::Result { + self.stream.try_clone() + } + 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(()) + } } /// Server-side Unix-domain-socket control channel for the hosted userland POC. pub struct UnixStreamServerControlChannel { stream: UnixStream, + io_deadline: Option, } impl UnixStreamServerControlChannel { /// Creates a server control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { - Self { stream } + 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) + } } } @@ -78,18 +130,23 @@ impl ClientControlChannel for UnixStreamClientControlChannel { type Error = io::Error; fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { - write_frame_with_deadline( - &mut self.stream, - &encode_request(*request).map_err(wire_error)?, - self.io_deadline, - ) + let frame = encode_request(request.clone()).map_err(wire_error)?; + 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 Some(frame) = read_frame_with_deadline(&mut self.stream, self.io_deadline)? else { - return Ok(None); + 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), }; - decode_response(&frame).map(Some).map_err(wire_error) + self.clear_active_request_deadline()?; + result } } @@ -103,24 +160,21 @@ impl ServerControlChannel for UnixStreamServerControlChannel { } fn recv_request(&mut self) -> io::Result> { - let Some(frame) = read_frame(&mut self.stream)? else { + 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( + write_frame_with_deadline( &mut self.stream, - &encode_response(*response).map_err(wire_error)?, + &encode_response(response.clone()).map_err(wire_error)?, + self.io_deadline, ) } } -fn read_frame(stream: &mut UnixStream) -> io::Result>> { - read_frame_with_deadline(stream, None) -} - fn read_frame_with_deadline( stream: &mut UnixStream, deadline: Option, @@ -157,10 +211,6 @@ fn read_frame_with_deadline( Ok(Some(frame)) } -fn write_frame(stream: &mut UnixStream, frame: &[u8]) -> io::Result<()> { - write_frame_with_deadline(stream, frame, None) -} - fn write_frame_with_deadline( stream: &mut UnixStream, frame: &[u8], @@ -213,6 +263,12 @@ fn io_timeout_for_deadline(deadline: Instant) -> io::Result { 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) } @@ -231,9 +287,14 @@ mod tests { #[test] fn frame_round_trip() { let (mut writer, mut reader) = UnixStream::pair().unwrap(); - write_frame(&mut writer, &[1, 2, 3]).unwrap(); + write_frame_with_deadline(&mut writer, &[1, 2, 3], None).unwrap(); - assert_eq!(read_frame(&mut reader).unwrap().unwrap(), [1, 2, 3]); + assert_eq!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .unwrap(), + [1, 2, 3] + ); } #[test] @@ -241,7 +302,11 @@ mod tests { let (writer, mut reader) = UnixStream::pair().unwrap(); drop(writer); - assert!(read_frame(&mut reader).unwrap().is_none()); + assert!( + read_frame_with_deadline(&mut reader, None) + .unwrap() + .is_none() + ); } #[test] @@ -250,14 +315,18 @@ mod tests { writer.write_all(&[1, 0]).unwrap(); drop(writer); assert_eq!( - read_frame(&mut reader).unwrap_err().kind(), + 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(&mut reader).unwrap_err().kind(), + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), io::ErrorKind::InvalidData ); @@ -266,7 +335,9 @@ mod tests { .write_all(&u32::try_from(MAX_FRAME_LEN + 1).unwrap().to_le_bytes()) .unwrap(); assert_eq!( - read_frame(&mut reader).unwrap_err().kind(), + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), io::ErrorKind::InvalidData ); @@ -275,7 +346,9 @@ mod tests { writer.write_all(&[1, 2]).unwrap(); drop(writer); assert_eq!( - read_frame(&mut reader).unwrap_err().kind(), + read_frame_with_deadline(&mut reader, None) + .unwrap_err() + .kind(), io::ErrorKind::InvalidData ); } @@ -298,6 +371,33 @@ mod tests { ); } + #[test] + fn client_response_read_io_timeout_is_wall_clock() { + let (mut server, client) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamClientControlChannel::from_connected(client); + channel + .set_io_timeout(Some(Duration::from_millis(50))) + .unwrap(); + + let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); + server.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + std::thread::sleep(Duration::from_millis(20)); + if server.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:?}" + ); + } + #[test] fn client_response_read_honors_io_deadline() { let (mut server, client) = UnixStream::pair().unwrap(); @@ -318,4 +418,31 @@ mod tests { "unexpected deadline error kind: {error:?}" ); } + + #[test] + fn server_request_read_io_deadline_is_wall_clock() { + let (mut client, server) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamServerControlChannel::from_accepted(server); + channel + .set_io_deadline(Some(Instant::now() + Duration::from_millis(50))) + .unwrap(); + + let reader = std::thread::spawn(move || channel.recv_request().unwrap_err()); + client.write_all(&8u32.to_le_bytes()).unwrap(); + for _ in 0..8 { + std::thread::sleep(Duration::from_millis(20)); + if client.write_all(&[0]).is_err() { + break; + } + } + + let error = reader.join().expect("deadline reader panicked"); + assert!( + matches!( + error.kind(), + io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut + ), + "unexpected deadline error kind: {error:?}" + ); + } } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index c963f4db8..f98693d11 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -2,25 +2,81 @@ // Licensed under the MIT license. use std::env; +use std::fs; use std::io; +use std::os::unix::fs::FileTypeExt; use std::os::unix::net::UnixListener; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; use litebox_broker_core::{BrokerCore, EventOnlyPolicy}; use litebox_broker_server::{BrokerServeError, serve_connection}; use litebox_broker_unix_socket::UnixStreamServerControlChannel; +const SESSION_TIMEOUT: Duration = Duration::from_secs(5); + fn main() -> io::Result<()> { let args = Args::parse(env::args().skip(1))?; - let listener = UnixListener::bind(&args.socket_path)?; + let listener = bind_listener(&args.socket_path)?; + let _socket_cleanup = SocketPathCleanup::new(args.socket_path.clone()); let (stream, _) = listener.accept()?; let mut channel = UnixStreamServerControlChannel::from_accepted(stream); + channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; let mut broker = BrokerCore::new(EventOnlyPolicy); serve_connection(&mut broker, &mut channel) .map(|_| ()) .map_err(broker_error) } +fn bind_listener(socket_path: &Path) -> io::Result { + match UnixListener::bind(socket_path) { + Ok(listener) => Ok(listener), + Err(error) if error.kind() == io::ErrorKind::AddrInUse => { + remove_stale_socket(socket_path)?; + UnixListener::bind(socket_path) + } + Err(error) => Err(error), + } +} + +fn remove_stale_socket(socket_path: &Path) -> io::Result<()> { + let metadata = fs::symlink_metadata(socket_path)?; + if !metadata.file_type().is_socket() { + return Err(io::Error::new( + io::ErrorKind::AlreadyExists, + "broker socket path exists and is not a socket", + )); + } + + match std::os::unix::net::UnixStream::connect(socket_path) { + Ok(_stream) => Err(io::Error::new( + io::ErrorKind::AddrInUse, + "broker socket path is already accepting connections", + )), + Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => { + fs::remove_file(socket_path) + } + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error), + } +} + +struct SocketPathCleanup { + socket_path: PathBuf, +} + +impl SocketPathCleanup { + fn new(socket_path: PathBuf) -> Self { + Self { socket_path } + } +} + +impl Drop for SocketPathCleanup { + fn drop(&mut self) { + let _ = fs::remove_file(&self.socket_path); + } +} + fn broker_error(error: BrokerServeError) -> io::Error { match error { BrokerServeError::AssociationSetup => io::Error::other("broker association setup failed"), diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index d68da00fa..d4e17bee8 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -5,7 +5,7 @@ use std::env; use std::fs; use std::io; use std::path::{Path, PathBuf}; -use std::process::{Child, Command}; +use std::process::{Child, Command, ExitStatus}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -16,9 +16,12 @@ use litebox_broker_unix_socket::UnixStreamClientControlChannel; #[test] fn separate_process_broker_serves_event_object_requests() { - let socket_path = unique_socket_path(); - let mut child = spawn_broker(&socket_path); - let channel = connect_with_retry(&socket_path).unwrap(); + 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 client = BrokerClient::new(channel); assert_eq!(client.negotiate().unwrap(), SUPPORTED_PROTOCOL_VERSION); @@ -38,10 +41,8 @@ fn separate_process_broker_serves_event_object_requests() { client.wait_event(handle).unwrap(), WaitOutcome::Ready(ReadinessState::new(true, true, 1)) ); - drop(client); assert!(child.wait().unwrap().success()); - let _ = fs::remove_file(socket_path); } fn spawn_broker(socket_path: &Path) -> Child { @@ -52,6 +53,62 @@ fn spawn_broker(socket_path: &Path) -> Child { .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 { diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_wire/src/lib.rs index c8da62581..c25a1e775 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_wire/src/lib.rs @@ -190,7 +190,10 @@ fn decode_event_request(decoder: &mut Decoder<'_>) -> Result EventRequest::Consume(ConsumeEventRequest::new( decoder.handle()?, - decode_event_consume_mode(decoder)?, + match decode_event_consume_mode(decoder)? { + Some(mode) => mode, + None => return Ok(None), + }, )), _ => return Ok(None), }; @@ -324,9 +327,12 @@ fn decode_event_response(decoder: &mut Decoder<'_>) -> Result { EventResponse::Create(CreateEventResponse::new(decoder.handle()?)) } - EVENT_RESPONSE_TAG_WAITED => { - EventResponse::Wait(WaitEventResponse::new(decode_wait_outcome(decoder)?)) - } + 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(decoder.readiness()?)), EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( decoder.u64()?, @@ -338,11 +344,11 @@ fn decode_event_response(decoder: &mut Decoder<'_>) -> Result) -> Result { +fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, WireError> { match decoder.u8()? { - WAIT_OUTCOME_TAG_READY => Ok(WaitOutcome::Ready(decoder.readiness()?)), - WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(WaitOutcome::WouldBlock(decoder.readiness()?)), - _ => Err(WireError::UnknownWaitOutcome), + WAIT_OUTCOME_TAG_READY => Ok(Some(WaitOutcome::Ready(decoder.readiness()?))), + WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(Some(WaitOutcome::WouldBlock(decoder.readiness()?))), + _ => Ok(None), } } @@ -363,11 +369,13 @@ fn encode_event_consume_mode( } } -fn decode_event_consume_mode(decoder: &mut Decoder<'_>) -> Result { +fn decode_event_consume_mode( + decoder: &mut Decoder<'_>, +) -> Result, WireError> { match decoder.u8()? { - EVENT_CONSUME_MODE_TAG_ALL => Ok(EventConsumeMode::All), - EVENT_CONSUME_MODE_TAG_ONE => Ok(EventConsumeMode::One), - _ => Err(WireError::UnknownEventConsumeMode), + EVENT_CONSUME_MODE_TAG_ALL => Ok(Some(EventConsumeMode::All)), + EVENT_CONSUME_MODE_TAG_ONE => Ok(Some(EventConsumeMode::One)), + _ => Ok(None), } } @@ -504,7 +512,7 @@ mod tests { for request in requests { assert_eq!( - decode_request(&encode_request(request).unwrap()).unwrap(), + decode_request(&encode_request(request.clone()).unwrap()).unwrap(), ReceivedBrokerRequest::Request(request) ); } @@ -541,7 +549,7 @@ mod tests { for response in responses { assert_eq!( - decode_response(&encode_response(response).unwrap()).unwrap(), + decode_response(&encode_response(response.clone()).unwrap()).unwrap(), ReceivedBrokerResponse::Response(response) ); } @@ -553,6 +561,15 @@ mod tests { 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), + ))) + .unwrap(); + *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), @@ -570,7 +587,7 @@ mod tests { ); assert_eq!( decode_response(&[1, 0, 1, 0xff]), - Err(WireError::UnknownWaitOutcome) + Ok(ReceivedBrokerResponse::Unknown) ); assert_eq!( decode_response(&[2, 0xff, 0xff]), diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index e1479e8f5..77d54dd7b 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -2,6 +2,7 @@ // Licensed under the MIT license. use std::{ + net::Shutdown, path::Path, thread, time::{Duration, Instant}, @@ -15,6 +16,7 @@ use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreRe use litebox_broker_unix_socket::UnixStreamClientControlChannel; 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 Client = BrokerClient; @@ -52,9 +54,11 @@ impl Drop for BrokerConnection { } impl BrokerControlClient { - fn new(client: Client) -> Self { + fn new(client: Client, shutdown_stream: std::os::unix::net::UnixStream) -> Self { Self { - worker: BrokerClientWorker::new(client), + worker: BrokerClientWorker::new_with_shutdown_hook(client, move || { + let _ = shutdown_stream.shutdown(Shutdown::Both); + }), } } @@ -84,12 +88,16 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let mut client = connect_with_retry(socket_path, setup_deadline) .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; + let shutdown_stream = client + .control_channel_mut() + .try_clone_stream() + .context("failed to clone broker control channel for shutdown")?; client .control_channel_mut() - .set_io_deadline(None) - .context("failed to clear broker setup deadline")?; + .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) + .context("failed to configure broker active request timeout")?; Ok(BrokerConnection { - control: Arc::new(BrokerControlClient::new(client)), + control: Arc::new(BrokerControlClient::new(client, shutdown_stream)), }) } diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 6cf1d3bf7..293403ada 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, @@ -192,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()) @@ -208,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()) @@ -245,36 +260,83 @@ fn unique_test_socket_path(name: &str) -> PathBuf { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -fn spawn_test_broker

(socket_path: &Path, policy: P) -> std::thread::JoinHandle<()> +struct TestBroker { + thread: Option>, + done_rx: std::sync::mpsc::Receiver<()>, + socket_path: PathBuf, +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux"))] +impl TestBroker { + fn join(mut self) { + self.done_rx + .recv_timeout(BROKER_HELPER_TIMEOUT) + .expect("broker test server did not finish"); + self.thread + .take() + .expect("broker test server thread missing") + .join() + .expect("broker test server 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: P) -> TestBroker where P: litebox_broker_core::PolicyEngine + Send + 'static, { 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 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 listener = std::os::unix::net::UnixListener::bind(&server_socket_path) - .expect("failed to bind broker test socket"); - ready_tx.send(()).expect("failed to report broker ready"); - - let (stream, _) = listener.accept().expect("failed to accept broker client"); - let mut channel = - litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); - let mut core = litebox_broker_core::BrokerCore::new(policy); - let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) - .expect("broker server failed"); - assert_eq!( - termination, - litebox_broker_server::ConnectionTermination::PeerClosed - ); - let _ = std::fs::remove_file(server_socket_path); + 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"); + ready_tx.send(()).expect("failed to report broker ready"); + + let (stream, _) = listener.accept().expect("failed to accept broker client"); + 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 = + litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); + let mut core = litebox_broker_core::BrokerCore::new(policy); + let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) + .expect("broker server failed"); + assert_eq!( + termination, + litebox_broker_server::ConnectionTermination::PeerClosed + ); + })); + 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 server did not start"); - broker_thread + TestBroker { + thread: Some(broker_thread), + done_rx, + socket_path: cleanup_socket_path, + } } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] @@ -287,8 +349,7 @@ fn test_runner_connects_to_broker() { Runner::new(&true_path, "broker_true_rewriter") .broker_socket(&socket_path) .run(); - broker_thread.join().expect("broker test server panicked"); - let _ = std::fs::remove_file(socket_path); + broker_thread.join(); } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] @@ -302,8 +363,7 @@ fn test_broker_backed_eventfd_with_rewriter() { .broker_socket(&socket_path) .run(); - broker_thread.join().expect("broker test server panicked"); - let _ = std::fs::remove_file(socket_path); + broker_thread.join(); } #[cfg(target_arch = "x86_64")] diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 72a1401f2..f3dacaf38 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -67,6 +67,10 @@ impl EventFileCounter bool { + matches!(self, Self::LocalCore(_)) + } + fn read( &self, cx: &WaitContext<'_, Platform>, @@ -188,7 +192,14 @@ impl EventFile { super::common_functions_for_file_status!(); pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { + let current_status = self.get_status(); let new_status = (self.get_status() & mask.complement()) | (requested & mask); + if !current_status.contains(OFlags::NONBLOCK) + && new_status.contains(OFlags::NONBLOCK) + && !self.counter.is_local_core() + { + return Err(Errno::EINVAL); + } if !new_status.contains(OFlags::NONBLOCK) && !self.counter.supports_blocking_operations() { return Err(Errno::EINVAL); } @@ -262,19 +273,24 @@ mod tests { .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] diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 5fc95f5bc..f8346cef5 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -904,7 +904,12 @@ impl Task { |_fd| Ok(None), |_fd| Ok(None), |fd| { - validate_eventfd_iovec_len(iovs.iter().map(|iov| iov.iov_len))?; + let total_len = + eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; + if total_len == 0 { + return Ok(Some(0)); + } + validate_eventfd_iovec_len(total_len)?; let handle = self .global .litebox @@ -977,7 +982,7 @@ fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno Ok(()) } -fn validate_eventfd_iovec_len(iov_lens: impl IntoIterator) -> Result<(), Errno> { +fn eventfd_iovec_total_len(iov_lens: impl IntoIterator) -> Result { let mut total_len = 0usize; for iov_len in iov_lens { total_len = total_len.checked_add(iov_len).ok_or(Errno::EINVAL)?; @@ -985,6 +990,10 @@ fn validate_eventfd_iovec_len(iov_lens: impl IntoIterator) -> Resu return Err(Errno::EINVAL); } } + Ok(total_len) +} + +fn validate_eventfd_iovec_len(total_len: usize) -> Result<(), Errno> { if total_len < size_of::() { return Err(Errno::EINVAL); } @@ -992,7 +1001,7 @@ fn validate_eventfd_iovec_len(iov_lens: impl IntoIterator) -> Resu } fn copy_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Result<(), Errno> { - validate_eventfd_iovec_len(iovs.iter().map(|iov| iov.iov_len))?; + validate_eventfd_iovec_len(eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?)?; let bytes = value.to_ne_bytes(); let mut copied = 0; @@ -2886,6 +2895,34 @@ mod tests { ); } + #[test] + fn eventfd_readv_all_zero_iovecs_is_noop() { + let task = crate::syscalls::tests::init_platform(None); + let fd = task + .sys_eventfd2(5, EfdFlags::empty()) + .expect("eventfd2 failed"); + let fd = i32::try_from(fd).unwrap(); + let mut output = [0u8; 8]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(output.as_mut_ptr().expose_provenance()), + iov_len: 0, + }, + IoReadVec { + iov_base: MutPtr::from_usize(output.as_mut_ptr().expose_provenance()), + iov_len: 0, + }, + ]; + + assert_eq!( + task.sys_readv(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Ok(0) + ); + + assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); + assert_eq!(u64::from_ne_bytes(output), 5); + } + #[test] fn eventfd_writev_rejects_leading_zero_before_value() { let task = crate::syscalls::tests::init_platform(None); diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 8fe5854f0..66d031449 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -92,6 +92,10 @@ fn test_fcntl() { .expect("Failed to create eventfd"); let eventfd = i32::try_from(eventfd).unwrap(); check(eventfd, OFlags::RDWR, OFlags::RDWR); + assert_eq!( + task.sys_fcntl(eventfd, FcntlArg::SETFL(OFlags::RDWR | OFlags::NONBLOCK)), + Err(Errno::EINVAL) + ); // Test fcntl with DUPFD let fd = task From 5c03a0d3bc03faab36259b6f60d5e340771bbf6b Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 13:00:09 -0700 Subject: [PATCH 39/66] Rename broker adapter crates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 4 +-- Cargo.lock | 30 +++++++++---------- Cargo.toml | 12 ++++---- docs/broker-design.md | 26 ++++++++-------- docs/impl-plan.md | 2 +- .../Cargo.toml | 2 +- .../src/lib.rs | 2 +- .../src/server.rs | 0 .../Cargo.toml | 2 +- .../src/error.rs | 0 .../src/event.rs | 0 .../src/lib.rs | 0 .../src/negotiate.rs | 0 .../src/worker.rs | 0 .../Cargo.toml | 2 +- litebox_broker_transport/src/lib.rs | 10 +++++++ .../src/unix_socket.rs | 4 +-- litebox_broker_userland/Cargo.toml | 6 ++-- litebox_broker_userland/src/main.rs | 4 +-- .../tests/userland_broker.rs | 6 ++-- litebox_runner_linux_userland/Cargo.toml | 6 ++-- litebox_runner_linux_userland/src/broker.rs | 4 +-- litebox_runner_linux_userland/tests/run.rs | 6 ++-- 23 files changed, 69 insertions(+), 59 deletions(-) rename {litebox_broker_server => litebox_broker_host}/Cargo.toml (88%) rename {litebox_broker_server => litebox_broker_host}/src/lib.rs (93%) rename {litebox_broker_server => litebox_broker_host}/src/server.rs (100%) rename {litebox_broker_client => litebox_broker_local}/Cargo.toml (86%) rename {litebox_broker_client => litebox_broker_local}/src/error.rs (100%) rename {litebox_broker_client => litebox_broker_local}/src/event.rs (100%) rename {litebox_broker_client => litebox_broker_local}/src/lib.rs (100%) rename {litebox_broker_client => litebox_broker_local}/src/negotiate.rs (100%) rename {litebox_broker_client => litebox_broker_local}/src/worker.rs (100%) rename {litebox_broker_unix_socket => litebox_broker_transport}/Cargo.toml (87%) create mode 100644 litebox_broker_transport/src/lib.rs rename litebox_broker_unix_socket/src/lib.rs => litebox_broker_transport/src/unix_socket.rs (99%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bcf7de954..27225490f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,7 +230,7 @@ jobs: # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # - # - `litebox_broker_unix_socket` is allowed to have `std` access, + # - `litebox_broker_transport` is allowed to have `std` access, # since it is the concrete Unix-domain-socket channel for the # userland broker proof of concept. # @@ -293,7 +293,7 @@ jobs: # can safely use std. find . -type f -name 'Cargo.toml' \ -not -path './Cargo.toml' \ - -not -path './litebox_broker_unix_socket/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' \ diff --git a/Cargo.lock b/Cargo.lock index 2ef44865a..f3316f37c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1476,33 +1476,33 @@ dependencies = [ ] [[package]] -name = "litebox_broker_client" +name = "litebox_broker_core" version = "0.1.0" dependencies = [ "litebox_broker_protocol", ] [[package]] -name = "litebox_broker_core" +name = "litebox_broker_host" version = "0.1.0" dependencies = [ + "litebox_broker_core", "litebox_broker_protocol", ] [[package]] -name = "litebox_broker_protocol" -version = "0.1.0" - -[[package]] -name = "litebox_broker_server" +name = "litebox_broker_local" version = "0.1.0" dependencies = [ - "litebox_broker_core", "litebox_broker_protocol", ] [[package]] -name = "litebox_broker_unix_socket" +name = "litebox_broker_protocol" +version = "0.1.0" + +[[package]] +name = "litebox_broker_transport" version = "0.1.0" dependencies = [ "litebox_broker_protocol", @@ -1513,11 +1513,11 @@ dependencies = [ name = "litebox_broker_userland" version = "0.1.0" dependencies = [ - "litebox_broker_client", "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", "litebox_broker_protocol", - "litebox_broker_server", - "litebox_broker_unix_socket", + "litebox_broker_transport", ] [[package]] @@ -1700,11 +1700,11 @@ dependencies = [ "glob", "libc", "litebox", - "litebox_broker_client", "litebox_broker_core", + "litebox_broker_host", + "litebox_broker_local", "litebox_broker_protocol", - "litebox_broker_server", - "litebox_broker_unix_socket", + "litebox_broker_transport", "litebox_common_linux", "litebox_platform_linux_userland", "litebox_platform_multiplex", diff --git a/Cargo.toml b/Cargo.toml index a78b9da8b..08525a3bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,11 @@ resolver = "2" members = [ "litebox", - "litebox_broker_client", + "litebox_broker_local", "litebox_broker_core", "litebox_broker_protocol", - "litebox_broker_server", - "litebox_broker_unix_socket", + "litebox_broker_host", + "litebox_broker_transport", "litebox_broker_userland", "litebox_broker_wire", "litebox_common_linux", @@ -36,11 +36,11 @@ members = [ ] default-members = [ "litebox", - "litebox_broker_client", + "litebox_broker_local", "litebox_broker_core", "litebox_broker_protocol", - "litebox_broker_server", - "litebox_broker_unix_socket", + "litebox_broker_host", + "litebox_broker_transport", "litebox_broker_userland", "litebox_broker_wire", "litebox_common_linux", diff --git a/docs/broker-design.md b/docs/broker-design.md index f837ae93b..455ec2a50 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -136,19 +136,19 @@ The broker architecture should use crate names that make the authority boundary | `litebox_broker_protocol` | Shared `no_std` protocol crate for broker-visible DTOs and transport-neutral control-channel contracts: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and client/server control-channel traits. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | | `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream channel implementations reuse it rather than duplicating protocol encoding. | -| `litebox_broker_server` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | -| `litebox_broker_unix_socket` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | +| `litebox_broker_host` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | +| `litebox_broker_transport` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | | `litebox_broker_userland` | Hosted `std` broker executable used by the userland POC. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket channel implementation together. | -| `litebox_broker_client` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | +| `litebox_broker_local` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | | `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | -`litebox_broker_client` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. +`litebox_broker_local` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. -Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_unix_socket` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_transport` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. -Shared broker DTOs live in `litebox_broker_protocol` to avoid repeating request/response and handle/readiness shapes across protocol, core, client, wire, and server code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_server` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. +Shared broker DTOs live in `litebox_broker_protocol` to avoid repeating request/response and handle/readiness shapes across protocol, core, client, wire, and server code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_host` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. @@ -162,7 +162,7 @@ Kernel/trusted deployments will likely link the host-support and broker-authorit | Future crate/layer | Role | |---|---| -| `litebox_broker_host` | BrokerHost abstractions for user-mode execution support, trap/upcall/channel delivery, process/thread setup, and the UserLiteBox host ABI. | +| BrokerHost support layer (name TBD) | User-mode execution support, trap/upcall/channel delivery, process/thread setup, and the UserLiteBox host ABI. This is distinct from the current `litebox_broker_host` crate, which owns broker-side protocol/channel adaptation into BrokerCore. | | `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, BrokerServices, PolicyEngine, BrokerPlatform, and BrokerHost. | These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, broker server/entry code, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, broker server/entry code decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. @@ -313,7 +313,7 @@ The durable-unicorn Linux experiment provides a future hosted-userland profile w - the broker binds the mapped ring set to the host-authenticated spawned runner identity; - the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. -This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_client`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. +This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_local`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. The initial Linux ring set can use five unidirectional rings: @@ -795,10 +795,10 @@ The first proof of concept should use: litebox_broker_protocol litebox_broker_core litebox_broker_wire -litebox_broker_unix_socket -litebox_broker_server +litebox_broker_transport +litebox_broker_host litebox_broker_userland -litebox_broker_client +litebox_broker_local separate userland broker process Unix-domain-socket channel implementing neutral control-channel traits control channel only @@ -813,10 +813,10 @@ Then proceed incrementally: 1. Define the component boundary: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Create `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_unix_socket` with the concrete Unix-domain-socket implementation, `litebox_broker_server` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. +4. Create `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_transport` with the concrete Unix-domain-socket implementation, `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. 5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. -7. Use `litebox_broker_client` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. +7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. 10. Make UserLiteBox handle tables broker-backed views for migrated object families. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 4f1459893..c01eeb9ff 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -95,7 +95,7 @@ Initial scope: Exit criteria: -- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_client` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. +- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. diff --git a/litebox_broker_server/Cargo.toml b/litebox_broker_host/Cargo.toml similarity index 88% rename from litebox_broker_server/Cargo.toml rename to litebox_broker_host/Cargo.toml index 74444bdb7..fdeb2b609 100644 --- a/litebox_broker_server/Cargo.toml +++ b/litebox_broker_host/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "litebox_broker_server" +name = "litebox_broker_host" version = "0.1.0" edition = "2024" diff --git a/litebox_broker_server/src/lib.rs b/litebox_broker_host/src/lib.rs similarity index 93% rename from litebox_broker_server/src/lib.rs rename to litebox_broker_host/src/lib.rs index 0d7906a7b..dd1245f18 100644 --- a/litebox_broker_server/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -5,7 +5,7 @@ //! //! This crate wires `litebox_broker_core` to any implementation of the neutral //! server control-channel trait. Concrete channels live in separate crates such as -//! `litebox_broker_unix_socket`. +//! `litebox_broker_transport`. #![no_std] diff --git a/litebox_broker_server/src/server.rs b/litebox_broker_host/src/server.rs similarity index 100% rename from litebox_broker_server/src/server.rs rename to litebox_broker_host/src/server.rs diff --git a/litebox_broker_client/Cargo.toml b/litebox_broker_local/Cargo.toml similarity index 86% rename from litebox_broker_client/Cargo.toml rename to litebox_broker_local/Cargo.toml index 9d7a90a0b..142555c93 100644 --- a/litebox_broker_client/Cargo.toml +++ b/litebox_broker_local/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "litebox_broker_client" +name = "litebox_broker_local" version = "0.1.0" edition = "2024" diff --git a/litebox_broker_client/src/error.rs b/litebox_broker_local/src/error.rs similarity index 100% rename from litebox_broker_client/src/error.rs rename to litebox_broker_local/src/error.rs diff --git a/litebox_broker_client/src/event.rs b/litebox_broker_local/src/event.rs similarity index 100% rename from litebox_broker_client/src/event.rs rename to litebox_broker_local/src/event.rs diff --git a/litebox_broker_client/src/lib.rs b/litebox_broker_local/src/lib.rs similarity index 100% rename from litebox_broker_client/src/lib.rs rename to litebox_broker_local/src/lib.rs diff --git a/litebox_broker_client/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs similarity index 100% rename from litebox_broker_client/src/negotiate.rs rename to litebox_broker_local/src/negotiate.rs diff --git a/litebox_broker_client/src/worker.rs b/litebox_broker_local/src/worker.rs similarity index 100% rename from litebox_broker_client/src/worker.rs rename to litebox_broker_local/src/worker.rs diff --git a/litebox_broker_unix_socket/Cargo.toml b/litebox_broker_transport/Cargo.toml similarity index 87% rename from litebox_broker_unix_socket/Cargo.toml rename to litebox_broker_transport/Cargo.toml index 1ef949412..2866696a8 100644 --- a/litebox_broker_unix_socket/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "litebox_broker_unix_socket" +name = "litebox_broker_transport" version = "0.1.0" edition = "2024" 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_unix_socket/src/lib.rs b/litebox_broker_transport/src/unix_socket.rs similarity index 99% rename from litebox_broker_unix_socket/src/lib.rs rename to litebox_broker_transport/src/unix_socket.rs index 7b302af9a..866e1ab08 100644 --- a/litebox_broker_unix_socket/src/lib.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -3,9 +3,9 @@ //! Unix-domain-socket broker channel for hosted userland deployments. //! -//! This crate deliberately uses `std` because Unix-domain sockets and `std::io` +//! 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, wire, client, core, and server crates. +//! no_std protocol, wire, local, core, and host crates. use std::io::{self, Read, Write}; use std::os::unix::net::UnixStream; diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index b16fe1d41..cad04fa92 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -5,15 +5,15 @@ edition = "2024" [dependencies] litebox_broker_core = { path = "../litebox_broker_core", version = "0.1.0" } -litebox_broker_server = { path = "../litebox_broker_server", version = "0.1.0" } -litebox_broker_unix_socket = { path = "../litebox_broker_unix_socket", 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_client = { path = "../litebox_broker_client", version = "0.1.0" } +litebox_broker_local = { path = "../litebox_broker_local", version = "0.1.0" } litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } [lints] diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index f98693d11..e9ddf6af3 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -10,8 +10,8 @@ use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use litebox_broker_core::{BrokerCore, EventOnlyPolicy}; -use litebox_broker_server::{BrokerServeError, serve_connection}; -use litebox_broker_unix_socket::UnixStreamServerControlChannel; +use litebox_broker_host::{BrokerServeError, serve_connection}; +use litebox_broker_transport::unix_socket::UnixStreamServerControlChannel; const SESSION_TIMEOUT: Duration = Duration::from_secs(5); diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index d4e17bee8..b93b64057 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,10 +9,10 @@ use std::process::{Child, Command, ExitStatus}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use litebox_broker_client::BrokerClient; +use litebox_broker_host::SUPPORTED_PROTOCOL_VERSION; +use litebox_broker_local::BrokerClient; use litebox_broker_protocol::{ReadinessState, WaitOutcome}; -use litebox_broker_server::SUPPORTED_PROTOCOL_VERSION; -use litebox_broker_unix_socket::UnixStreamClientControlChannel; +use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; #[test] fn separate_process_broker_serves_event_object_requests() { diff --git a/litebox_runner_linux_userland/Cargo.toml b/litebox_runner_linux_userland/Cargo.toml index 7a81c1afc..1a8b603e6 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -8,9 +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_client = { version = "0.1.0", path = "../litebox_broker_client", features = ["std"] } +litebox_broker_local = { version = "0.1.0", path = "../litebox_broker_local", features = ["std"] } litebox_broker_protocol = { version = "0.1.0", path = "../litebox_broker_protocol" } -litebox_broker_unix_socket = { version = "0.1.0", path = "../litebox_broker_unix_socket" } +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"] } @@ -25,7 +25,7 @@ sha2 = "0.10" walkdir = "2.0" glob = "0.3" litebox_broker_core = { version = "0.1.0", path = "../litebox_broker_core" } -litebox_broker_server = { version = "0.1.0", path = "../litebox_broker_server" } +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 index 77d54dd7b..8224d5177 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -11,9 +11,9 @@ use std::{ use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox::{BrokerControl, BrokerControlError}; -use litebox_broker_client::{BrokerClient, BrokerClientWorker}; +use litebox_broker_local::{BrokerClient, BrokerClientWorker}; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; -use litebox_broker_unix_socket::UnixStreamClientControlChannel; +use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); const ACTIVE_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 293403ada..2fc3ea305 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -313,13 +313,13 @@ where .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); let mut channel = - litebox_broker_unix_socket::UnixStreamServerControlChannel::from_accepted(stream); + litebox_broker_transport::unix_socket::UnixStreamServerControlChannel::from_accepted(stream); let mut core = litebox_broker_core::BrokerCore::new(policy); - let termination = litebox_broker_server::serve_connection(&mut core, &mut channel) + let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) .expect("broker server failed"); assert_eq!( termination, - litebox_broker_server::ConnectionTermination::PeerClosed + litebox_broker_host::ConnectionTermination::PeerClosed ); })); let _ = std::fs::remove_file(&server_socket_path); From 10f15eef011ec01a9ffec418333ee84c81b5666e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 13:15:14 -0700 Subject: [PATCH 40/66] Merge broker wire into protocol Move the reusable control-message codec into litebox_broker_protocol::wire and remove the standalone wire crate from the workspace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 8 -- Cargo.toml | 2 - docs/broker-design.md | 8 +- litebox_broker_protocol/src/lib.rs | 3 + .../src/wire.rs | 99 +++++-------------- litebox_broker_transport/Cargo.toml | 1 - litebox_broker_transport/src/unix_socket.rs | 12 ++- litebox_broker_wire/Cargo.toml | 10 -- 8 files changed, 36 insertions(+), 107 deletions(-) rename litebox_broker_wire/src/lib.rs => litebox_broker_protocol/src/wire.rs (86%) delete mode 100644 litebox_broker_wire/Cargo.toml diff --git a/Cargo.lock b/Cargo.lock index f3316f37c..5333ec06c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1506,7 +1506,6 @@ name = "litebox_broker_transport" version = "0.1.0" dependencies = [ "litebox_broker_protocol", - "litebox_broker_wire", ] [[package]] @@ -1520,13 +1519,6 @@ dependencies = [ "litebox_broker_transport", ] -[[package]] -name = "litebox_broker_wire" -version = "0.1.0" -dependencies = [ - "litebox_broker_protocol", -] - [[package]] name = "litebox_common_linux" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 08525a3bc..6456081c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ members = [ "litebox_broker_host", "litebox_broker_transport", "litebox_broker_userland", - "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", @@ -42,7 +41,6 @@ default-members = [ "litebox_broker_host", "litebox_broker_transport", "litebox_broker_userland", - "litebox_broker_wire", "litebox_common_linux", "litebox_common_windows", "litebox_common_optee", diff --git a/docs/broker-design.md b/docs/broker-design.md index 455ec2a50..0c62876d3 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -133,9 +133,8 @@ The broker architecture should use crate names that make the authority boundary | Crate | Initial role | |---|---| -| `litebox_broker_protocol` | Shared `no_std` protocol crate for broker-visible DTOs and transport-neutral control-channel contracts: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and client/server control-channel traits. | +| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, client/server control-channel traits, and request/response message-body encoding. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_wire` | Reusable `no_std + alloc` byte codec for broker request/response message bodies. Byte-stream channel implementations reuse it rather than duplicating protocol encoding. | | `litebox_broker_host` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_transport` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | | `litebox_broker_userland` | Hosted `std` broker executable used by the userland POC. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket channel implementation together. | @@ -148,7 +147,7 @@ Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. -Shared broker DTOs live in `litebox_broker_protocol` to avoid repeating request/response and handle/readiness shapes across protocol, core, client, wire, and server code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_host` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. +Shared broker DTOs and the current wire codec live in `litebox_broker_protocol` to avoid repeating request/response, handle/readiness, and message-body encoding shapes across protocol, core, local, transport, and host code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_host` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. @@ -794,7 +793,6 @@ The first proof of concept should use: ```text litebox_broker_protocol litebox_broker_core -litebox_broker_wire litebox_broker_transport litebox_broker_host litebox_broker_userland @@ -813,7 +811,7 @@ Then proceed incrementally: 1. Define the component boundary: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. 2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Create `litebox_broker_wire` with reusable message-body encoding, `litebox_broker_transport` with the concrete Unix-domain-socket implementation, `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. +4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. 5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index a67a8dcd3..221505d30 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -10,11 +10,14 @@ #![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::{ ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, diff --git a/litebox_broker_wire/src/lib.rs b/litebox_broker_protocol/src/wire.rs similarity index 86% rename from litebox_broker_wire/src/lib.rs rename to litebox_broker_protocol/src/wire.rs index c25a1e775..df47ac34d 100644 --- a/litebox_broker_wire/src/lib.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -3,14 +3,11 @@ //! Reusable byte codec for broker request/response control-channel messages. -#![no_std] - -extern crate alloc; - use core::fmt; use alloc::vec::Vec; -use litebox_broker_protocol::{ + +use crate::{ AddEventRequest, AddEventResponse, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, CreateEventResponse, ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, @@ -46,24 +43,12 @@ const EVENT_CONSUME_MODE_TAG_ONE: u8 = 2; #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum WireError { - /// The encoder was asked to emit a request tag this codec does not own. - EncodeUnknownRequestTag, - /// The encoder was asked to emit a response tag this codec does not own. - EncodeUnknownResponseTag, - /// The encoder was asked to emit a wait-outcome tag this codec does not own. - EncodeUnknownWaitOutcome, - /// The encoder was asked to emit an event consume mode this codec does not own. - EncodeUnknownEventConsumeMode, /// 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, - /// The wait-outcome tag is unknown. - UnknownWaitOutcome, - /// The event consume mode tag is unknown. - UnknownEventConsumeMode, /// A decoder offset overflowed. OffsetOverflow, } @@ -72,22 +57,8 @@ 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::EncodeUnknownRequestTag => { - f.write_str("cannot encode unknown broker request tag") - } - Self::EncodeUnknownResponseTag => { - f.write_str("cannot encode unknown broker response tag") - } - Self::EncodeUnknownWaitOutcome => { - f.write_str("cannot encode unknown broker wait outcome tag") - } - Self::EncodeUnknownEventConsumeMode => { - f.write_str("cannot encode unknown broker event consume mode") - } Self::TrailingBytes => f.write_str("trailing broker wire bytes"), Self::InvalidBoolean => f.write_str("invalid broker wire boolean"), - Self::UnknownWaitOutcome => f.write_str("unknown broker wait outcome"), - Self::UnknownEventConsumeMode => f.write_str("unknown broker event consume mode"), Self::OffsetOverflow => f.write_str("broker wire offset overflow"), } } @@ -99,7 +70,7 @@ impl core::error::Error for WireError {} /// /// Successful encodings are always non-empty because the first byte is the /// message tag. -pub fn encode_request(request: BrokerRequest) -> Result, WireError> { +pub fn encode_request(request: BrokerRequest) -> Vec { let mut encoder = Encoder::default(); match request { BrokerRequest::Negotiate { protocol_version } => { @@ -107,47 +78,41 @@ pub fn encode_request(request: BrokerRequest) -> Result, WireError> { encoder.u16(protocol_version.major); encoder.u16(protocol_version.minor); } - BrokerRequest::Core(request) => encode_core_request(&mut encoder, request)?, - _ => return Err(WireError::EncodeUnknownRequestTag), + BrokerRequest::Core(request) => encode_core_request(&mut encoder, request), } - Ok(encoder.finish()) + encoder.finish() } -fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) -> Result<(), WireError> { +fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { encoder.u8(REQUEST_TAG_CORE); match request { CoreRequest::Event(request) => { encoder.u8(CORE_REQUEST_TAG_EVENT); - encode_event_request(encoder, request) + encode_event_request(encoder, request); } - _ => Err(WireError::EncodeUnknownRequestTag), } } -fn encode_event_request(encoder: &mut Encoder, request: EventRequest) -> Result<(), WireError> { +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); - Ok(()) } EventRequest::Wait(request) => { encoder.u8(EVENT_REQUEST_TAG_WAIT); encoder.handle(request.handle); - Ok(()) } EventRequest::Add(request) => { encoder.u8(EVENT_REQUEST_TAG_ADD); encoder.handle(request.handle); encoder.u64(request.value); - Ok(()) } EventRequest::Consume(request) => { encoder.u8(EVENT_REQUEST_TAG_CONSUME); encoder.handle(request.handle); - encode_event_consume_mode(encoder, request.mode) + encode_event_consume_mode(encoder, request.mode); } - _ => Err(WireError::EncodeUnknownRequestTag), } } @@ -205,7 +170,7 @@ fn decode_event_request(decoder: &mut Decoder<'_>) -> Result Result, WireError> { +pub fn encode_response(response: BrokerResponse) -> Vec { let mut encoder = Encoder::default(); match response { BrokerResponse::Negotiated { @@ -222,66 +187,57 @@ pub fn encode_response(response: BrokerResponse) -> Result, WireError> { encoder.u16(broker_protocol_version.major); encoder.u16(broker_protocol_version.minor); } - BrokerResponse::Core(response) => encode_core_response(&mut encoder, response)?, + BrokerResponse::Core(response) => encode_core_response(&mut encoder, response), BrokerResponse::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.u16(error.as_raw()); } - _ => return Err(WireError::EncodeUnknownResponseTag), } - Ok(encoder.finish()) + encoder.finish() } -fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) -> Result<(), WireError> { +fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { encoder.u8(RESPONSE_TAG_CORE); match response { CoreResponse::Event(response) => { encoder.u8(CORE_RESPONSE_TAG_EVENT); - encode_event_response(encoder, response) + encode_event_response(encoder, response); } - _ => Err(WireError::EncodeUnknownResponseTag), } } -fn encode_event_response(encoder: &mut Encoder, response: EventResponse) -> Result<(), WireError> { +fn encode_event_response(encoder: &mut Encoder, response: EventResponse) { match response { EventResponse::Create(response) => { encoder.u8(EVENT_RESPONSE_TAG_CREATED); encoder.handle(response.handle); - Ok(()) } EventResponse::Wait(response) => { encoder.u8(EVENT_RESPONSE_TAG_WAITED); - encode_wait_outcome(encoder, response.outcome) + encode_wait_outcome(encoder, response.outcome); } EventResponse::Add(response) => { encoder.u8(EVENT_RESPONSE_TAG_ADDED); encoder.readiness(response.readiness); - Ok(()) } EventResponse::Consume(response) => { encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); encoder.u64(response.value); encoder.readiness(response.readiness); - Ok(()) } - _ => Err(WireError::EncodeUnknownResponseTag), } } -fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) -> Result<(), WireError> { +fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { match outcome { WaitOutcome::Ready(readiness) => { encoder.u8(WAIT_OUTCOME_TAG_READY); encoder.readiness(readiness); - Ok(()) } WaitOutcome::WouldBlock(readiness) => { encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); encoder.readiness(readiness); - Ok(()) } - _ => Err(WireError::EncodeUnknownWaitOutcome), } } @@ -352,20 +308,14 @@ fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, } } -fn encode_event_consume_mode( - encoder: &mut Encoder, - mode: EventConsumeMode, -) -> Result<(), WireError> { +fn encode_event_consume_mode(encoder: &mut Encoder, mode: EventConsumeMode) { match mode { EventConsumeMode::All => { encoder.u8(EVENT_CONSUME_MODE_TAG_ALL); - Ok(()) } EventConsumeMode::One => { encoder.u8(EVENT_CONSUME_MODE_TAG_ONE); - Ok(()) } - _ => Err(WireError::EncodeUnknownEventConsumeMode), } } @@ -512,7 +462,7 @@ mod tests { for request in requests { assert_eq!( - decode_request(&encode_request(request.clone()).unwrap()).unwrap(), + decode_request(&encode_request(request.clone())).unwrap(), ReceivedBrokerRequest::Request(request) ); } @@ -549,7 +499,7 @@ mod tests { for response in responses { assert_eq!( - decode_response(&encode_response(response.clone()).unwrap()).unwrap(), + decode_response(&encode_response(response.clone())).unwrap(), ReceivedBrokerResponse::Response(response) ); } @@ -563,8 +513,7 @@ mod tests { ); let mut unknown_consume_mode = encode_request(event_request(EventRequest::Consume( ConsumeEventRequest::new(sample_handle(), EventConsumeMode::All), - ))) - .unwrap(); + ))); *unknown_consume_mode.last_mut().unwrap() = 0xff; assert_eq!( decode_request(&unknown_consume_mode), @@ -573,8 +522,7 @@ mod tests { assert_eq!(decode_request(&[0, 1]), Err(WireError::TruncatedFrame)); let mut frame = encode_request(event_request(EventRequest::Create( CreateEventRequest::new(0), - ))) - .unwrap(); + ))); frame.push(0xff); assert_eq!(decode_request(&frame), Err(WireError::TrailingBytes)); } @@ -615,8 +563,7 @@ mod tests { assert_eq!( encode_response(event_response(EventResponse::Add(AddEventResponse::new( ReadinessState::new(true, false, 0x0102_0304_0506_0708) - )))) - .unwrap(), + )))), [1, 0, 2, 1, 0, 8, 7, 6, 5, 4, 3, 2, 1] ); } diff --git a/litebox_broker_transport/Cargo.toml b/litebox_broker_transport/Cargo.toml index 2866696a8..da5909d56 100644 --- a/litebox_broker_transport/Cargo.toml +++ b/litebox_broker_transport/Cargo.toml @@ -5,7 +5,6 @@ edition = "2024" [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } -litebox_broker_wire = { path = "../litebox_broker_wire", version = "0.1.0" } [lints] workspace = true diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 866e1ab08..dc5ad5fc4 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -5,19 +5,21 @@ //! //! 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, wire, local, core, and host crates. +//! 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}; use litebox_broker_protocol::{ ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, ServerControlChannel, }; -use litebox_broker_wire::{decode_request, decode_response, encode_request, encode_response}; const MAX_FRAME_LEN: usize = 64 * 1024; @@ -130,7 +132,7 @@ impl ClientControlChannel for UnixStreamClientControlChannel { type Error = io::Error; fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { - let frame = encode_request(request.clone()).map_err(wire_error)?; + 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() { @@ -169,7 +171,7 @@ impl ServerControlChannel for UnixStreamServerControlChannel { fn send_response(&mut self, response: &BrokerResponse) -> io::Result<()> { write_frame_with_deadline( &mut self.stream, - &encode_response(response.clone()).map_err(wire_error)?, + &encode_response(response.clone()), self.io_deadline, ) } @@ -273,7 +275,7 @@ fn invalid_data(message: &'static str) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, message) } -fn wire_error(error: litebox_broker_wire::WireError) -> io::Error { +fn wire_error(error: WireError) -> io::Error { io::Error::new( io::ErrorKind::InvalidData, format!("invalid broker wire message: {error}"), diff --git a/litebox_broker_wire/Cargo.toml b/litebox_broker_wire/Cargo.toml deleted file mode 100644 index 38443e090..000000000 --- a/litebox_broker_wire/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "litebox_broker_wire" -version = "0.1.0" -edition = "2024" - -[dependencies] -litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } - -[lints] -workspace = true From 09dbbed20b4c8898ac83b8c2d2c6b41e9bd33461 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 13:32:37 -0700 Subject: [PATCH 41/66] Address broker interface review feedback Clarify BrokerCore layering docs, return a core-domain event consumption value, and distinguish local, negotiated-session, and broker-reported unsupported protocol versions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/event.rs | 28 +++++++++++---- litebox_broker_core/src/lib.rs | 12 ++++--- litebox_broker_host/src/server.rs | 13 ++++--- litebox_broker_local/src/error.rs | 30 ++++++++++++++++ litebox_broker_local/src/event.rs | 6 ++-- litebox_broker_local/src/lib.rs | 49 +++++++++++++++++++++------ litebox_broker_local/src/negotiate.rs | 4 +-- 7 files changed, 111 insertions(+), 31 deletions(-) diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index a4110dbc6..bc4f1bd0d 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -5,12 +5,26 @@ use crate::object::{ObjectId, ObjectKind}; use crate::{ BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, Result, }; -use litebox_broker_protocol::{ - ConsumeEventResponse, EventConsumeMode, ObjectHandle, ReadinessState, WaitOutcome, -}; +use litebox_broker_protocol::{EventConsumeMode, ObjectHandle, ReadinessState, WaitOutcome}; const MAX_EVENT_COUNT: u64 = u64::MAX - 1; +/// 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 } + } +} + impl BrokerCore

{ /// Creates a broker-owned event object. pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { @@ -81,12 +95,12 @@ impl BrokerCore

{ association: &BrokerAssociation, handle: ObjectHandle, mode: EventConsumeMode, - ) -> Result { + ) -> 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| { - ConsumeEventResponse::new( + EventConsumption::new( response.value, Self::filter_readiness_for_rights(response.readiness, authorized.rights), ) @@ -143,7 +157,7 @@ impl EventObject { Ok(self.readiness_state()) } - fn consume(&mut self, mode: EventConsumeMode) -> Result { + fn consume(&mut self, mode: EventConsumeMode) -> Result { if self.count == 0 { return Err(BrokerError::WouldBlock); } @@ -156,7 +170,7 @@ impl EventObject { let next_generation = self.next_generation()?; self.count -= value; self.readiness_generation = next_generation; - Ok(ConsumeEventResponse::new(value, self.readiness_state())) + Ok(EventConsumption::new(value, self.readiness_state())) } fn next_generation(&self) -> Result { diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 3c1811a4b..e94fa5a08 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -1,12 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Protocol- and channel-independent broker authority core. +//! 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 deliberately has no -//! dependency on protocol envelopes, Unix sockets, shared-memory rings, kernel -//! traps, or any other channel implementation. +//! 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] @@ -23,6 +26,7 @@ mod policy; use alloc::collections::BTreeMap; pub use error::BrokerError; +pub use event::EventConsumption; use identity::BrokerCoreId; pub use identity::{BrokerAssociation, CallerCredential}; use litebox_broker_protocol::ObjectReferenceId; diff --git a/litebox_broker_host/src/server.rs b/litebox_broker_host/src/server.rs index f573481d4..2e268c82f 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/server.rs @@ -7,9 +7,9 @@ use litebox_broker_core::{ BrokerAssociation, BrokerCore, BrokerError, CallerCredential, PolicyEngine, }; use litebox_broker_protocol::{ - AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, - CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, ProtocolVersion, - ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, + AddEventResponse, BrokerRequest, BrokerResponse, ConsumeEventResponse, CoreRequest, + CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, + ProtocolVersion, ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, }; /// Protocol version this broker server implementation supports. @@ -159,7 +159,12 @@ fn handle_event_request( ), EventRequest::Consume(request) => handle_core_result( core.consume_event(association, request.handle, request.mode), - |response| event_response(EventResponse::Consume(response)), + |consumption| { + event_response(EventResponse::Consume(ConsumeEventResponse::new( + consumption.value, + consumption.readiness, + ))) + }, ), _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 16c8408e2..5cd847b0e 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -26,6 +26,20 @@ pub enum ClientError { /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, }, + /// This client cannot speak the requested protocol version. + UnsupportedClientVersion { + /// Protocol version requested by the caller. + requested: ProtocolVersion, + /// Protocol version supported by this client implementation. + client_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 client. @@ -54,6 +68,20 @@ impl fmt::Display for ClientError { f, "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" ), + Self::UnsupportedClientVersion { + requested, + client_protocol_version, + } => write!( + f, + "broker client cannot request protocol version {requested:?}; client supports {client_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, @@ -82,6 +110,8 @@ where | Self::ChannelClosed | Self::UnknownResponse | Self::IncompatibleNegotiation { .. } + | Self::UnsupportedClientVersion { .. } + | Self::UnsupportedNegotiatedVersion { .. } | Self::UnsupportedVersion { .. } | Self::UnexpectedResponse(_) => None, } diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index c490937e2..3173555be 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -92,9 +92,9 @@ impl BrokerClient { if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { Ok(()) } else { - Err(ClientError::UnsupportedVersion { - requested: EVENT_PROTOCOL_VERSION, - broker_protocol_version: negotiated, + Err(ClientError::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 index 476705a71..a22402258 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -143,11 +143,11 @@ mod tests { assert!(matches!( client.create_event(), - Err(ClientError::UnsupportedVersion { - requested, - broker_protocol_version - }) if requested == CLIENT_PROTOCOL_VERSION - && broker_protocol_version == ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1) + Err(ClientError::UnsupportedNegotiatedVersion { + required, + negotiated_protocol_version + }) if required == CLIENT_PROTOCOL_VERSION + && negotiated_protocol_version == ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1) )); assert_eq!(client.channel.sent_request, None); } @@ -193,26 +193,53 @@ mod tests { } #[test] - fn negotiate_version_reports_supported_version_and_allows_retry() { + fn negotiate_version_rejects_locally_unsupported_version_without_sending() { let too_new = ProtocolVersion::new( CLIENT_PROTOCOL_VERSION.major, CLIENT_PROTOCOL_VERSION.minor + 1, ); - let fallback = CLIENT_PROTOCOL_VERSION; let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { - broker_protocol_version: fallback, + broker_protocol_version: CLIENT_PROTOCOL_VERSION, })); let mut client = BrokerClient::new(channel); assert!(matches!( client.negotiate_version(too_new), - Err(ClientError::UnsupportedVersion { + Err(ClientError::UnsupportedClientVersion { requested, - broker_protocol_version - }) if requested == too_new && broker_protocol_version == fallback + client_protocol_version + }) if requested == too_new && client_protocol_version == CLIENT_PROTOCOL_VERSION )); assert_eq!(client.negotiated_protocol_version(), None); assert_eq!(client.channel.sent_request, None); + } + + #[test] + fn negotiate_version_reports_broker_supported_version_and_allows_retry() { + let requested = CLIENT_PROTOCOL_VERSION; + let fallback = ProtocolVersion::new( + CLIENT_PROTOCOL_VERSION.major, + CLIENT_PROTOCOL_VERSION.minor - 1, + ); + let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { + broker_protocol_version: fallback, + })); + let mut client = BrokerClient::new(channel); + + assert!(matches!( + client.negotiate_version(requested), + Err(ClientError::UnsupportedVersion { + requested: actual_requested, + broker_protocol_version + }) if actual_requested == requested && broker_protocol_version == fallback + )); + assert_eq!(client.negotiated_protocol_version(), None); + assert_eq!( + client.channel.sent_request, + Some(BrokerRequest::Negotiate { + protocol_version: requested + }) + ); client.channel.response = Some(BrokerResponse::Negotiated { broker_protocol_version: fallback, diff --git a/litebox_broker_local/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs index 00d11fa7a..4a61ec211 100644 --- a/litebox_broker_local/src/negotiate.rs +++ b/litebox_broker_local/src/negotiate.rs @@ -28,9 +28,9 @@ impl BrokerClient { return Err(ClientError::AlreadyNegotiated); } if !protocol_version.is_supported_by(CLIENT_PROTOCOL_VERSION) { - return Err(ClientError::UnsupportedVersion { + return Err(ClientError::UnsupportedClientVersion { requested: protocol_version, - broker_protocol_version: CLIENT_PROTOCOL_VERSION, + client_protocol_version: CLIENT_PROTOCOL_VERSION, }); } From 663b7f6e48e27efa907f1cb88c358e89e8dc7a95 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 14:49:16 -0700 Subject: [PATCH 42/66] Align broker terminology and control client API Rename the broker-local public client API to ControlClient/ControlClientWorker, move the shared event consumption value into the protocol crate, and tighten split-broker design terminology around litebox_broker_host and broker-kernel user-mode support. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 7 +- docs/broker-design.md | 344 +++++++++--------- docs/impl-plan.md | 77 ++-- litebox_broker_core/src/event.rs | 20 +- litebox_broker_core/src/lib.rs | 3 +- litebox_broker_host/src/lib.rs | 2 +- litebox_broker_host/src/server.rs | 13 +- litebox_broker_local/src/error.rs | 10 +- litebox_broker_local/src/event.rs | 6 +- litebox_broker_local/src/lib.rs | 28 +- litebox_broker_local/src/negotiate.rs | 4 +- litebox_broker_local/src/worker.rs | 134 +++---- litebox_broker_protocol/src/event.rs | 13 +- litebox_broker_protocol/src/lib.rs | 4 +- .../tests/userland_broker.rs | 4 +- litebox_runner_linux_userland/src/broker.rs | 10 +- litebox_runner_linux_userland/tests/run.rs | 4 +- 17 files changed, 332 insertions(+), 351 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27225490f..5ee5fcc70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -231,12 +231,11 @@ jobs: # since it is a purely-userland implementation. # # - `litebox_broker_transport` is allowed to have `std` access, - # since it is the concrete Unix-domain-socket channel for the - # userland broker proof of concept. + # 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 broker executable for the userland - # broker proof of concept. + # 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. diff --git a/docs/broker-design.md b/docs/broker-design.md index 0c62876d3..c3e98c5aa 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -6,27 +6,24 @@ Enable true multi-process and multi-session LiteBox support while preserving por This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, a Windows-like ABI, or another guest ABI. The broker architecture should not assume any one shim's syscall set, process model, or resource vocabulary. -The design separates LiteBox into: +The design has two trust domains: ```text -Always user mode: - Shim + UserLiteBox - optional shim-specific user clients - -Host support for UserLiteBox: - hosted userland: existing host OS/user ABI - broker kernel: BrokerHost +User mode: + Shim + local core + optional BrokerService clients Authority domain: - broker entry/server + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker entry/server + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker-kernel user-mode support, in kernel-backed deployments ``` -The authority domain differs by deployment: +The local core reaches local mechanics and broker channels through deployment support selected by the deployment profile: -| Deployment | Broker location | UserLiteBox host support | +| Deployment | Broker location | Deployment support used by local core | |---|---|---| -| **Userland broker** | privileged broker process | existing host OS/user ABI | -| **Kernel broker** | kernel or equivalent trusted domain | `BrokerHost`, which can carry broker entry channel traffic without decoding broker requests | +| **Userland broker** | privileged broker process | host OS user-mode ABI plus a broker transport endpoint | +| **Kernel broker** | kernel or equivalent trusted domain | broker-kernel user-mode support plus broker-channel delivery without decoding broker requests | ## Component model @@ -38,19 +35,15 @@ User mode: Shim | v - UserLiteBox + optional shim-specific user clients - -- via BrokerClient adapter over UserLiteBox channel --> + local core + optional BrokerService clients + -- via litebox_broker_local over the selected broker channel --> broker authority interface -Hosted userland: - UserLiteBox <-> host OS/user ABI - -Broker-kernel deployment: - UserLiteBox <-> BrokerHost - Authority domain: - broker authority interface -> broker entry/server - | + broker-kernel user-mode support (kernel-backed deployments) + -- carries/classifies channel traffic --> + broker authority interface -> litebox_broker_host + | v BrokerCore + optional BrokerServices | | @@ -59,51 +52,53 @@ Authority domain: PolicyEngine BrokerPlatform ``` +Deployment support supplies two things to user mode: local mechanics that do not create LiteBox authority, and a broker channel. In hosted userland, those are the host OS user-mode ABI plus a broker transport endpoint. In a broker-kernel deployment, they are calls into broker-kernel user-mode support plus broker-channel delivery. + ### Shim Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap handling, argument decoding, return-value conventions, exception delivery, frame construction, and guest-visible ABI details. -### UserLiteBox +### Local core -Runs in user mode. It is the combined user-mode LiteBox component that replaces the earlier top-level separation between user-core logic and user-platform mechanics. +Runs in user mode. It is the LiteBox runtime below the shim: the component that presents local APIs to shims, manages non-authoritative local state, and turns broker-backed resources into local objects. -UserLiteBox contains: +The local core contains: - user-facing core APIs used by shims; - guest pointer and guest memory marshalling helpers; - local caches and non-authoritative views of broker state; - private synchronization and wait helpers; - broker-owned control/event/data channel wrappers; -- a thin BrokerClient adapter; -- an internal user-platform layer that talks to host support. +- the `litebox_broker_local` adapter; +- internal deployment-support calls for local mechanics. -UserLiteBox is **not trusted** for security. It executes in the same user-mode context as the guest and shim. It may request operations and cache derived state, but it must never create authority. +The local core is **not trusted** for security. It executes in the same user-mode context as the guest and shim. It may request operations and cache derived state, but it must never create authority. The old distinction between user core and user platform can remain as an internal implementation structure if it is useful for code organization. It is not a security boundary and does not need to be reflected as a top-level architectural component. -Because UserLiteBox always runs in user mode, it can use Rust `std` heavily in deployments that provide a normal user-mode runtime: allocation, collections, threads, TLS, synchronization, IPC clients, async runtimes, richer errors, and broker-channel abstractions. +The local core should keep its contracts portable instead of assuming `std` everywhere. Hosted adapters may use `std` for threads, synchronization, IPC clients, async runtimes, richer errors, and broker-channel plumbing when the selected deployment permits those facilities. -### BrokerClient adapter +### Broker-local adapter -Thin in-process adapter used by UserLiteBox/local-core code to call the broker authority interface. +Thin in-process adapter used by local-core code to call the broker authority interface. -BrokerClient is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the control channel supplied by UserLiteBox's host-support layer. +This adapter is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the selected broker channel. -### BrokerHost +### Broker-kernel user-mode support -Kernel-side host support for UserLiteBox in broker-kernel deployments. +Future kernel-side support for running the user-mode local core in broker-kernel deployments. -BrokerHost provides the user-mode execution substrate that UserLiteBox expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker channel endpoints, and possibly a compatibility ABI subset. +This code runs in the kernel or equivalent trusted domain. It is part of the broker-kernel deployment, not part of the local core. It provides the execution support that the user-mode local core expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker channel endpoints, and any compatibility ABI that the selected local-core profile requires. -BrokerHost is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is the host-side component that lets user-mode LiteBox processes run and reach the broker. It may be trusted kernel code, but it should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. +Broker-kernel user-mode support is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is trusted-domain support that lets user-mode LiteBox processes run and reach the broker. It should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. -BrokerHost may carry broker authority traffic, but decoding, validation, and dispatch of `BrokerRequest` belong to the broker entry/server layer, not BrokerHost or BrokerCore. +Broker-kernel user-mode support may carry broker authority traffic, but decoding, validation, and dispatch of `BrokerRequest` belong to `litebox_broker_host`, not this support code or BrokerCore. -In broker-kernel deployments, BrokerHost shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; BrokerHost code is in the TCB and must be audited accordingly. +In broker-kernel deployments, this support code shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; this code is in the TCB and must be audited accordingly. ### BrokerCore -Required, shim-neutral trusted substrate. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, and readiness. +Required, shim-neutral trusted core. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, and readiness. BrokerCore should not bake in every shim's ABI semantics. It provides the authority primitives that shims, broker services, and PolicyEngine build on. BrokerCore enforces structural invariants, such as capability validity and object lifetime, and supplies state/context to PolicyEngine for authorization. @@ -129,17 +124,17 @@ Trusted backend for privileged operations: address-space control, host I/O, file ## Crate layout and naming -The broker architecture should use crate names that make the authority boundary visible. The first proof of concept should start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. +The broker architecture should use crate names that make the authority boundary visible. Start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. | Crate | Initial role | |---|---| | `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, client/server control-channel traits, and request/response message-body encoding. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_host` | Protocol- and channel-adapter `no_std` broker server library: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. The server owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | -| `litebox_broker_transport` | Concrete `std` Unix-domain-socket control-channel implementation for hosted userland testing. It owns only Unix stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/server crates. | -| `litebox_broker_userland` | Hosted `std` broker executable used by the userland POC. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket channel implementation together. | -| `litebox_broker_local` | `no_std` channel-neutral user-side adapter linked into UserLiteBox/local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | -| `litebox_user` | Untrusted UserLiteBox facade: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | +| `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | +| `litebox_broker_transport` | Hosted concrete broker transport implementations. The current implementation is a Unix-domain-socket control channel under `unix_socket`. The crate owns stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/host crates. | +| `litebox_broker_userland` | Hosted `std` broker executable. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket transport together. | +| `litebox_broker_local` | `no_std` channel-neutral user-side adapter linked into local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | +| `litebox` | Local core crate: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | `litebox_broker_local` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. @@ -157,38 +152,37 @@ Negotiation separates the broker's max-supported protocol version from the effec The known broker protocol keeps the outer envelope intentionally small. Connection-level messages such as negotiation and common errors stay at the broker layer; BrokerCore/object operations are grouped below that layer by authority domain and object family, for example `BrokerRequest::Core(CoreRequest::Event(EventRequest::Wait { .. }))`. New object families should add a nested request/response family instead of growing a flat top-level `BrokerRequest`/`BrokerResponse` operation list. The wire codec may encode those nested families as layered tags, but tag widths and unknown-tag handling remain private to the codec. -Kernel/trusted deployments will likely link the host-support and broker-authority pieces into one binary or image, but the code should still preserve their logical separation: +Kernel/trusted deployments will likely link broker-kernel user-mode support and broker-authority pieces into one binary or image, but the code should still preserve their logical separation: | Future crate/layer | Role | |---|---| -| BrokerHost support layer (name TBD) | User-mode execution support, trap/upcall/channel delivery, process/thread setup, and the UserLiteBox host ABI. This is distinct from the current `litebox_broker_host` crate, which owns broker-side protocol/channel adaptation into BrokerCore. | -| `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, BrokerServices, PolicyEngine, BrokerPlatform, and BrokerHost. | +| broker-kernel user-mode support | Trusted-domain support for user-mode execution, trap/upcall/channel delivery, process/thread setup, broker-channel endpoints, and the kernel support ABI used by local core. It supplies or adapts a server control channel, but `litebox_broker_host` still owns broker-side protocol/channel adaptation into BrokerCore. | +| `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, `litebox_broker_host`, BrokerServices, PolicyEngine, BrokerPlatform, and broker-kernel user-mode support. | -These names do not require separate runtime processes. In a kernel-broker deployment, BrokerHost, broker server/entry code, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: BrokerHost carries or classifies traffic, broker server/entry code decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. +These names do not require separate runtime processes. In a kernel-broker deployment, broker-kernel user-mode support, `litebox_broker_host`, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: broker-kernel user-mode support carries or classifies traffic, `litebox_broker_host` decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. -## Three interfaces +## Runtime interfaces -There are three logical interfaces. In a broker-kernel deployment, the UserLiteBox/host interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, BrokerHost should only classify and deliver traffic; BrokerRequest decoding and sequencing remain broker server/entry responsibilities, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. +There are three logical interfaces. In a broker-kernel deployment, the local-core deployment-support interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, broker-kernel user-mode support should only classify and deliver traffic to the server control channel; `litebox_broker_host` decodes and sequences `BrokerRequest` values, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. | Interface | Userland deployment | Kernel deployment | Purpose | |---|---|---|---| -| **Shim <-> UserLiteBox** | same address space | same address space | ergonomic user-mode guest ABI implementation | -| **UserLiteBox <-> host support layer** | host OS/user ABI | BrokerHost ABI | local non-authoritative mechanics | -| **UserLiteBox / shim-specific user client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | +| **Shim <-> local core** | same address space | same address space | ergonomic user-mode guest ABI implementation | +| **local core <-> deployment support** | host OS user-mode ABI | broker-kernel support ABI | local non-authoritative mechanics | +| **local core / BrokerService client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | The interfaces have different stability and trust requirements: | Interface | Shape | Authority | |---|---|---| -| `Shim <-> UserLiteBox` | ergonomic in-process API | no authority; user-mode compatibility layer | -| `UserLiteBox <-> host support layer` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | -| `UserLiteBox / shim-specific user client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | +| `Shim <-> local core` | ergonomic in-process API | no authority; user-mode compatibility layer | +| `local core <-> deployment support` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | +| `local core / BrokerService client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | -The broker authority interface should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService: +The broker authority interface should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService. Caller identity is bound to the authenticated channel by broker entry code; it is not supplied as a user-controlled request field. ```text BrokerRequest { - caller_identity, target: BrokerCore | BrokerService(service_id), operation, handles, @@ -205,8 +199,8 @@ External broker authority APIs: | API | Shape | |---|---| -| `UserLiteBox -> broker server/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | -| `shim-specific user client -> BrokerService` | shim/domain-specific protocol | +| `local core -> broker server/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | +| `BrokerService client -> BrokerService` | service-specific protocol for an optional BrokerService | Internal broker-authority APIs: @@ -216,7 +210,7 @@ Internal broker-authority APIs: | `BrokerService -> BrokerCore` | trusted in-domain API | | `BrokerCore / BrokerService -> BrokerPlatform` | backend execution after PolicyEngine authorization | -The "shim-specific user client" is not a new authority layer. It is the user-mode, typed client-side half of an optional BrokerService. +A BrokerService client is not a new authority layer. It is optional user-mode typed client code for a matching BrokerService, used only when a service-specific protocol is clearer than routing through generic local-core APIs. ## Broker protocol and channels @@ -230,11 +224,11 @@ Each sandboxed process has exactly one authenticated broker association. That as | event | broker to process | lifecycle, readiness, interrupt-like events, broker/session failure | | data | bidirectional | bulk payload bytes associated with authorized object operations | -The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. UserLiteBox does not prove identity by filling in request fields. +The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. The local core does not prove identity by filling in request fields. -The protocol exposes broker-owned objects through opaque per-association reference handles: a reference identifier plus a reference generation. Object identifiers stay broker-internal. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, reference generations, rights, and policy. Object types and rights are broker-internal for authorization; UserLiteBox cannot amplify authority by editing request fields. +The protocol exposes broker-owned objects through opaque per-association reference handles: a reference identifier plus a reference generation. Object identifiers stay broker-internal. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, reference generations, rights, and policy. Object types and rights are broker-internal for authorization; the local core cannot amplify authority by editing request fields. -The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. UserLiteBox receives only broker reference handles, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. +The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. The local core receives only broker reference handles, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. The control channel authorizes an operation and binds it to an object and request identifier; the data channel carries bytes for that authorized operation. Shared memory is an optimization, not an authority transfer, and all shared-memory contents remain untrusted. @@ -244,37 +238,37 @@ The new architecture should not force one Rust runtime model everywhere. | Component | Recommended baseline | |---|---| -| `UserLiteBox` | `std`, because it always runs in user mode | +| local core | portable core APIs; `std` adapters only where the selected deployment permits them | | userland broker | `std` | -| broker-kernel deployment | start with `no_std + alloc + BrokerHost/BrokerPlatform traits`; consider a custom `std` target only if the host grows rich enough | +| broker-kernel deployment | start with `no_std + alloc + broker-kernel support/BrokerPlatform traits`; consider a custom `std` target only if broker-kernel support grows rich enough | | shared protocol/types | `no_std + alloc` where feasible, so they can cross user/kernel and userland/kernel-broker deployments | -Using `std` in UserLiteBox is a simplification, not a security decision. UserLiteBox is still untrusted. `std` merely makes the user-mode implementation easier: normal collections, `std::sync`, broker object wrappers, IPC clients, threads, and async/data-channel libraries can be used when the deployment supports them. +Using `std` in hosted local-core adapters is a deployment convenience, not a security decision. The local core is still untrusted. `std` can be used for normal collections, `std::sync`, broker object wrappers, IPC clients, threads, and async/data-channel libraries when the deployment supports those APIs, but cross-deployment contracts should not require a normal host OS runtime. -Strict host-syscall profiles constrain how much of `std` can be used after lockdown. Any `std` functionality that may issue disallowed host syscalls must either run only during bootstrap, be avoided in strict mode, or be implemented on top of the approved broker/host-support ABI. +Strict host-syscall profiles constrain how much hosted `std` functionality can be used after lockdown. Any `std` functionality that may issue disallowed host syscalls must either run only during bootstrap, be avoided in strict mode, or be implemented on top of the approved deployment-support ABI. -For a broker kernel or trusted host, a custom Rust `std` target may eventually be useful if BrokerHost can provide enough primitives: allocator, blocking/scheduling, synchronization, time, I/O, panic policy, and TLS. It should not be the first requirement. A staged design is safer: +For a broker kernel or trusted host, a custom Rust `std` target may eventually be useful if broker-kernel user-mode support can provide enough primitives: allocator, blocking/scheduling, synchronization, time, I/O, panic policy, and TLS. It should not be the first requirement. A staged design is safer: -1. Define a small BrokerHost/BrokerPlatform trait surface. +1. Define a small broker-kernel support/BrokerPlatform trait surface. 2. Implement it for a `std` userland broker. 3. Implement it for LVBS/SNP/kernel-backed brokers with `no_std + alloc`. 4. Only introduce a custom `std` target if the trait surface naturally becomes "basically std." -## UserLiteBox and host calls +## Local core and deployment-support calls -UserLiteBox may use local host/kernel calls, but only for local mechanics that do not create LiteBox authority. +The local core may use deployment-provided host/kernel calls, but only for local mechanics that do not create LiteBox authority. -| UserLiteBox operation | Allowed? | Requirement | +| local-core operation | Allowed? | Requirement | |---|---|---| | private memory allocation, TLS, logging, local scratch mappings | yes | must not grant guest-visible authority | | private locks/futex-like synchronization | yes | must not represent broker-owned shared state | | broker notification channel | yes | broker validates every request | | broker-owned shared-ring data movement | yes | ring ownership, cursor movement, and frames are validated by the broker | | direct host file, network, or device access for guest-visible resources | no | must be mediated by broker-owned objects and PolicyEngine-authorized broker policy | -| guest-visible mappings or executable/shared memory | only through broker/UserLiteBox mediation | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform/BrokerHost applies | +| guest-visible mappings or executable/shared memory | only through broker/local-core mediation | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform or broker-kernel user-mode support applies | | trusted randomness, secrets, or security-sensitive time | no | must come from broker authority | -If UserLiteBox uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through BrokerHost or the runner must select a different UserLiteBox build/profile. The stable portability target is the shim/UserLiteBox/broker contract, not a single universal UserLiteBox binary. +If the local core uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through broker-kernel user-mode support or the runner must select a different local-core build/profile. The stable portability target is the shim/local-core/broker contract, not a single universal local-core binary. ### Host syscall profiles @@ -287,9 +281,9 @@ The strict design still needs a small host-kernel interface for local mechanics, | strict mode | post-lockdown calls only; on Linux this can target `SECCOMP_MODE_STRICT`-like behavior where only `read`, `write`, `_exit`, and `sigreturn` remain available | | arbitrated mode | selected non-delegable syscalls are trapped/validated before execution in the user process context | -Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, `fcntl`, and similar authority-bearing or mapping-changing calls must not reach the host unrestricted after lockdown. Guest-visible mapping operations must enter the shim/UserLiteBox path and then either be emulated from pre-reserved local memory or mediated by the broker. +Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, `fcntl`, and similar authority-bearing or mapping-changing calls must not reach the host unrestricted after lockdown. Guest-visible mapping operations must enter the shim/local-core path and then either be emulated from pre-reserved local memory or mediated by the broker. -If unrestricted `mmap`/`mprotect` remain available to the sandbox, broker mapping policy is bypassable. The design must either block those syscalls after bootstrap, constrain them to anonymous private local mechanics with a host-enforced profile, or route them through UserLiteBox/BrokerHost mediation. +If unrestricted `mmap`/`mprotect` remain available to the sandbox, broker mapping policy is bypassable. The design must either block those syscalls after bootstrap, constrain them to anonymous private local mechanics with a host-enforced profile, or route them through local-core/broker-kernel support mediation. LITESHIELD's useful distinction is between delegable and non-delegable syscalls: @@ -297,10 +291,10 @@ LITESHIELD's useful distinction is between delegable and non-delegable syscalls: |---|---| | delegable | translate into BrokerCore/BrokerService operations | | local-private | allow directly under the host syscall profile because no LiteBox authority is created | -| non-delegable/arbitrated | must execute in the process context, but only after trap/validation by UserLiteBox/BrokerHost policy machinery | +| non-delegable/arbitrated | must execute in the process context, but only after trap/validation by the local-core/broker-kernel support arbitration path | | blocked | never allowed after lockdown | -Memory-management and process-management calls are the hard cases. If they cannot be safely delegated to the broker, the host support layer needs an arbitration mechanism that can validate address ranges, mapping types, permissions, and target objects before allowing the host syscall to complete. +Memory-management and process-management calls are the hard cases. If they cannot be safely delegated to the broker, deployment support needs an arbitration mechanism that can validate address ranges, mapping types, permissions, and target objects before allowing the host syscall to complete. ### Linux userland bootstrap profile @@ -310,9 +304,9 @@ The durable-unicorn Linux experiment provides a future hosted-userland profile w - the `memfd` is inherited by the runner and identified by an environment/argument convention; - the `memfd` contains shared metadata and broker-created rings; - the broker binds the mapped ring set to the host-authenticated spawned runner identity; -- the runner maps the `memfd`, initializes UserLiteBox/shim state, installs sandbox restrictions, then enters guest code. +- the runner maps the `memfd`, initializes shim/local-core state, installs sandbox restrictions, then enters guest code. -This is intentionally different from the current Unix-socket PoC. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_local`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. +This is intentionally different from the current Unix-socket path. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_local`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. The initial Linux ring set can use five unidirectional rings: @@ -324,7 +318,7 @@ The initial Linux ring set can use five unidirectional rings: | data | broker to runner | bulk response/event payload bytes | | data | runner to broker | bulk request payload bytes | -Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a UserLiteBox scheduler, but keeping the MPSC layout preserves one ring format. +Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a local core scheduler, but keeping the MPSC layout preserves one ring format. Shared-memory rings are not trusted. The broker validates header magic/version, ring offsets/capacities, producer/consumer roles, cursor movement, frame bounds, and frame contents before acting. Impossible cursor movement, malformed frames, or writes inconsistent with ring ownership are protocol failures. @@ -335,7 +329,7 @@ Linux can expose two protection modes: | fast-futex mode | allows a small syscall allowlist, including futex wait/wake on ring cursors or private locks | | strict-seccomp mode | installs mappings, fds, signal handlers, and trampoline state before lockdown; after lockdown, only a strict syscall set remains available | -In strict-seccomp mode, guest host-thread parallelism may need to become a shim/UserLiteBox scheduling illusion rather than real host threads. This is a compatibility/performance tradeoff, not a broker policy bypass. +In strict-seccomp mode, guest host-thread parallelism may need to become a shim/local-core scheduling illusion rather than real host threads. This is a compatibility/performance tradeoff, not a broker policy bypass. ## Deployment contract and negotiation @@ -349,31 +343,31 @@ Shared spec crates should define: - BrokerService IDs, protocol versions, request/response types, and feature requirements; - PolicyEngine policy versions, policy profile IDs, and audit requirements; - broker capability names and profiles; -- UserLiteBox/BrokerHost ABI names and versions; +- deployment-support ABI names and versions; - control/event/data channel formats; - shared-memory/ring layout versions and validation rules; - host syscall profiles for bootstrap, fast local mode, and strict mode; -- deployment profiles that bind a shim, UserLiteBox profile, broker channel, required services, and required broker features. +- deployment profiles that bind a shim, local-core profile, broker channel, required services, and required broker features. The eventual deployment contract should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. -2. The runner selects a UserLiteBox profile that matches the deployment's host ABI. -3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The first POC's Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `ServerControlChannel` API that later authenticated channels will implement. +2. The runner selects a local-core profile that matches the deployment's host ABI. +3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The current Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `ServerControlChannel` API that later authenticated channels will implement. 4. The broker binds the caller identity used for dispatch to the authenticated peer credential. User mode does not choose its own authority identity. -5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, UserLiteBox, BrokerHost, channel/ring, and host-syscall-profile versions. +5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, local-core profile, deployment-support, channel/ring, and host-syscall-profile versions. 6. The broker replies with supported services and capabilities. 7. The user side starts only if the required versions and features match. -The current hosted PoC implements the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. It also routes the migrated nonblocking `eventfd` object family through broker-backed local-core event counters. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and broader guest-visible broker object routing remain future work. +The current hosted userland path implements the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. It also routes the migrated nonblocking `eventfd` object family through broker-backed local-core event counters. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and broader guest-visible broker object routing remain future work. -UserLiteBox should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. +The local core should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. ## Security invariant The core security rule is: -> User-mode Shim and UserLiteBox may request operations and cache derived state, but they must never create authority. +> User-mode Shim and local core may request operations and cache derived state, but they must never create authority. Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be authoritative for: @@ -389,34 +383,34 @@ Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be a - network and application-level firewall policy; - shim/domain-specific trusted semantics when a BrokerService is present. -A compromised user-mode shim/UserLiteBox should not be able to escape the broker-granted authority. +A compromised user-mode shim/local-core path should not be able to escape the broker-granted authority. All authority-changing or host-effecting operations must be authorized by PolicyEngine before BrokerCore state mutation, BrokerService authority grants, or BrokerPlatform backend execution. BrokerCore remains responsible for structural capability/lifecycle validity; BrokerServices provide domain-specific context; PolicyEngine makes the policy decision. -Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. UserLiteBox must be assumed hostile for the duration of an in-flight request. +Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. The local core must be assumed hostile for the duration of an in-flight request. ## State ownership | State | Owner | |---|---| | guest ABI decoding state | Shim | -| per-workload cache/view | UserLiteBox | -| shim-specific user client state | optional shim-specific user client | -| guest memory marshalling | UserLiteBox + broker revalidation | -| private user-mode mechanics | UserLiteBox via host support layer | -| private synchronization fast paths | UserLiteBox | +| per-workload cache/view | local core | +| BrokerService client state | optional BrokerService client | +| guest memory marshalling | local core + broker revalidation | +| private user-mode mechanics | local core via deployment support | +| private synchronization fast paths | local core | | shared synchronization | BrokerCore | -| guest-visible handle numbers | UserLiteBox view, BrokerCore authority | +| guest-visible handle numbers | local core view, BrokerCore authority | | open/shared resource descriptions | BrokerCore | | shim/domain-specific authoritative state | optional BrokerService backed by BrokerCore | | policy decisions, constraints, and audit records | PolicyEngine | | IPC/event/queue/socket-like resources | BrokerCore-owned resources | | guest-visible/security-sensitive address-space mappings | BrokerCore + PolicyEngine + BrokerPlatform | -| user-only scratch mappings | UserLiteBox via host support layer | +| user-only scratch mappings | local core via deployment support | | host-visible I/O | BrokerPlatform executes PolicyEngine-authorized policy | | process/session/workload lifecycle | BrokerCore | -UserLiteBox-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. +local-core-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. ## Process and session model @@ -426,20 +420,20 @@ The stricter baseline uses one broker per sandbox session and one sandboxed host |---|---| | sandbox session | broker | | guest process identity | BrokerCore | -| authenticated host process association | broker channel / BrokerHost or host OS | -| guest-visible process semantics | shim + UserLiteBox, backed by BrokerCore identity | +| authenticated host process association | broker channel plus host OS or broker-kernel user-mode support identity | +| guest-visible process semantics | shim + local core, backed by BrokerCore identity | | process creation | broker-mediated | -| `exec`-like ABI behavior | shim/UserLiteBox within an existing broker association unless policy requires otherwise | +| `exec`-like ABI behavior | shim/local-core within an existing broker association unless policy requires otherwise | All guest processes in one sandbox session share one broker. The broker assigns guest process identity, creates or authorizes the private channel set for each process, and binds the channel set to the host-authenticated peer identity. A guest process cannot claim another process's identity by choosing request fields. -POSIX-like `fork` is broker-mediated at the identity/channel/resource level, while ABI-specific memory and descriptor inheritance semantics remain shim/UserLiteBox work. POSIX-like `exec` is preferably a shim/UserLiteBox replacement of guest memory and ABI state inside the existing sandboxed host process, so BrokerCore does not become ABI-specific. +POSIX-like `fork` is broker-mediated at the identity/channel/resource level, while ABI-specific memory and descriptor inheritance semantics remain shim/local-core work. POSIX-like `exec` is preferably a shim/local-core replacement of guest memory and ABI state inside the existing sandboxed host process, so BrokerCore does not become ABI-specific. -## UserLiteBox vs BrokerCore in current `litebox` +## Local core vs BrokerCore in current `litebox` The current `litebox` crate should not be migrated by whole module. Most modules mix ergonomic user-facing logic with authority-bearing state. The useful boundary is by responsibility. -| Current area | Keep in UserLiteBox | Move to BrokerCore / broker side | +| Current area | Keep in local core | Move to BrokerCore / broker side | |---|---|---| | `LiteBox` object | user-mode facade, std-backed helpers, broker connection/session object | broker session/workload identity | | `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, reference IDs, reference generations, rights, dup/pass/close/refcounts | @@ -455,14 +449,14 @@ The current `litebox` crate should not be migrated by whole module. Most modules | `mm::PageManager` | loader helpers, guest pointer handling, cached VMA view | authoritative mappings, permissions, memory grants, shared mappings, page-fault decisions | | `tls.rs` | shim/user-local TLS | none | | `utils::id_pool` and similar utilities | reusable helper where local-only | broker-owned ID allocation when IDs carry authority | -| current `platform` traits | internal UserLiteBox host-support layer where local-only | BrokerHost/BrokerPlatform traits where trusted-domain or backend effects are required | +| current `platform` traits | internal local-core deployment-support adapter where local-only | broker-kernel support/BrokerPlatform traits where trusted-domain or backend effects are required | Concrete examples: -- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. UserLiteBox can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, reference generations, refcount, passing, duplication, close, and revoke semantics. -- `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. UserLiteBox should only marshal paths/buffers and use broker-owned data channels or rings for payloads. -- `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. UserLiteBox should keep the socket facade and use broker-owned rings for data movement. -- `mm::PageManager` currently owns VMA state and calls platform page-management operations. UserLiteBox can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. +- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. The local core can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, reference generations, refcount, passing, duplication, close, and revoke semantics. +- `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. The local core should only marshal paths/buffers and use broker-owned data channels or rings for payloads. +- `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. The local core should keep the socket facade and use broker-owned rings for data movement. +- `mm::PageManager` currently owns VMA state and calls platform page-management operations. The local core can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. ## Control path and data path separation @@ -470,13 +464,13 @@ The broker should be on the control path for authority, but it does not need to ```text Control path: - UserLiteBox -> BrokerCore/BrokerService -> PolicyEngine -> BrokerPlatform + local core -> BrokerCore/BrokerService -> PolicyEngine -> BrokerPlatform Data path: - UserLiteBox moves bytes through broker-owned data channels or rings + local core moves bytes through broker-owned data channels or rings ``` -The broker still owns setup, rights, revocation model, object identity, and audit boundaries. Once it creates a constrained data channel or shared ring, UserLiteBox can move bytes without a control RPC per byte, but the broker still owns the object and validates frames/cursors before acting. +The broker still owns setup, rights, revocation model, object identity, and audit boundaries. Once it creates a constrained data channel or shared ring, the local core can move bytes without a control RPC per byte, but the broker still owns the object and validates frames/cursors before acting. Good candidates: @@ -505,26 +499,26 @@ The durable-unicorn experiment chose the stricter rule that the broker never pas Host resources stay broker-owned: ```text -UserLiteBox asks broker: open(path, flags) +local core asks broker: open(path, flags) BrokerCore resolves object/capability PolicyEngine authorizes path/flags/caller BrokerPlatform opens host object and stores host handle privately Broker returns broker reference handle, not host fd/HANDLE -UserLiteBox uses control/data channels for operations +local core uses control/data channels for operations ``` -This avoids moving enforcement into UserLiteBox or the runner, avoids cross-OS handle-rights mismatches, and makes revocation/audit simpler. The cost is higher broker involvement on data operations. Performance should first be recovered through broker-owned data rings, batching, and object-specific data channels rather than raw host-handle delegation. +This avoids moving enforcement into local core or the runner, avoids cross-OS handle-rights mismatches, and makes revocation/audit simpler. The cost is higher broker involvement on data operations. Performance should first be recovered through broker-owned data rings, batching, and object-specific data channels rather than raw host-handle delegation. Reintroducing host-handle delegation would require a separate future design note with object-specific proof obligations. It is not assumed by this architecture. ## Trusted data-plane services -The stricter baseline keeps host resources broker-owned and avoids raw host-handle delegation to UserLiteBox. SKernel suggests a future performance direction that preserves this rule: introduce trusted data-plane services inside the broker authority domain. +The stricter baseline keeps host resources broker-owned and avoids raw host-handle delegation to local core. SKernel suggests a future performance direction that preserves this rule: introduce trusted data-plane services inside the broker authority domain. In that model: ```text -UserLiteBox: +local core: untrusted ABI compatibility, marshalling, local caches BrokerCore / PolicyEngine: @@ -537,23 +531,23 @@ BrokerPlatform: authorized host/device/backend effects ``` -A trusted data-plane service is not UserLiteBox. It is part of the TCB, like a BrokerService or BrokerPlatform-adjacent component, and can hold backend authority that UserLiteBox must not receive. +A trusted data-plane service is not local core. It is part of the TCB, like a BrokerService or BrokerPlatform-adjacent component, and can hold backend authority that local core must not receive. Potential examples: | Service | Inspired by | LiteBox interpretation | |---|---|---| | filesystem data-plane service | SKernel-D FD/image-based filesystem, EROFS/TMPFS | broker-owned filesystem cache/ring service that reduces per-operation broker IPC without exposing host fds | -| network data-plane service | SKernel-D high-performance network stack with device passthrough | trusted broker-side network service; UserLiteBox talks via rings while PolicyEngine keeps firewall authority | -| memory/resource coordination service | SKernel-R/SKernel-V resource calls | BrokerHost/BrokerPlatform resource-call path for memory, CPU, and device-resource elasticity | +| network data-plane service | SKernel-D high-performance network stack with device passthrough | trusted broker-side network service; local core talks via rings while PolicyEngine keeps firewall authority | +| memory/resource coordination service | SKernel-R/SKernel-V resource calls | broker-kernel support/BrokerPlatform resource-call path for memory, CPU, and device-resource elasticity | -This is an optimization path, not the first milestone. The initial implementation should still start with simple broker-owned objects and mediated control/data channels. If performance demands it, move hot object-family data paths into trusted broker-side services rather than expanding untrusted UserLiteBox authority. +This is an optimization path, not the first milestone. The initial implementation should still start with simple broker-owned objects and mediated control/data channels. If performance demands it, move hot object-family data paths into trusted broker-side services rather than expanding untrusted local-core authority. ## Network and firewall enforcement The design can support a network or application-level firewall, but only if the broker is the authoritative network path. -In that configuration, UserLiteBox must not send or receive guest-visible traffic directly through a platform network device. It may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. +In that configuration, local core must not send or receive guest-visible traffic directly through a platform network device. It may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. Possible datapath shapes: @@ -578,12 +572,12 @@ Use: - local guest ABI decoding; - local cache for non-authoritative handle/process/session views; -- direct UserLiteBox fast paths for private state; +- direct local-core fast paths for private state; - batched broker calls where possible; - shared-memory rings for bulk IPC, pipe, queue, and network data; - broker-mediated setup with local data-plane access where security allows; - cached PolicyEngine decisions when the cache key includes all security-relevant context and supports revocation; -- explicit invalidation/revocation for stale UserLiteBox caches. +- explicit invalidation/revocation for stale local-core caches. The broker path is required for authority changes, cross-workload operations, shared resources, host-visible effects, and firewall-enforced traffic. @@ -605,22 +599,22 @@ Authority domain: | Risk | Mitigation | |---|---| -| UserLiteBox cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | +| local-core cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | | user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | | ABI becomes too chatty | batching, shared memory data planes, control/event/data channel separation, local private fast paths | -| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in UserLiteBox | +| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in local core | | handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | | broker bottleneck from no host-handle delegation | use broker-owned rings, batching, object-specific data channels, and policy caching | | address-space lifecycle complexity | broker-authoritative mappings, shared object IDs, careful copy-on-write/shared-memory design | | firewall datapath bottleneck | shared rings, batching, quotas, policy caching, and broker-side flow control | | encrypted traffic hides application data | enforce metadata/connection policy unless using broker proxying or broker-managed keys/endpoints | | BrokerServices become a second monolithic core | make them optional, small, versioned, and backed by BrokerCore primitives | -| UserLiteBox depends on unavailable host ABI | select UserLiteBox profile by deployment, or provide the needed ABI through BrokerHost | -| UserLiteBox depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | +| local core depends on unavailable host ABI | select local-core profile by deployment, or provide the needed ABI through deployment support | +| local core depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | | shared-memory TOCTOU or double-fetch bugs | validate broker requests against private snapshots or revalidate every use of user-controlled fields | -| unauthenticated broker channel | authenticate peers before negotiation and bind `caller_identity` to the authenticated channel endpoint | +| unauthenticated broker channel | authenticate peers before negotiation and bind broker-assigned caller identity to the authenticated channel endpoint | | PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | -| custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the host support surface justifies it | +| custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the broker-kernel support surface justifies it | | non-delegable syscalls bypass broker mapping/resource policy | block, constrain, or trap/arbitrate them before host execution | | trusted data-plane services grow too powerful | keep them broker-side, object-family-specific, and PolicyEngine-authorized | @@ -630,16 +624,16 @@ LiteBox's proposed design combines ideas from several systems rather than copyin | System | Relevant idea | LiteBox lesson | |---|---|---| -| **Drawbridge** | application + LibOS in a picoprocess over a narrow Host ABI | keep UserLiteBox user-mode and keep the broker ABI narrow | +| **Drawbridge** | application + LibOS in a picoprocess over a narrow Host ABI | keep the local core user-mode and keep the broker ABI narrow | | **Haven** | Drawbridge-style LibOS inside shielded execution | a narrow host interface composes with stronger isolation domains, but LiteBox's broker is trusted TCB rather than untrusted host | | **Graphene / Gramine** | LibOS plus PAL/host ABI, including SGX deployments | separate compatibility logic from host adaptation; avoid baking one host ABI into core | | **gVisor** | userspace kernel plus brokered filesystem/gofer model | rich domains like filesystems and sockets need real broker services, not just generic RPC | | **Chromium sandbox** | sandboxed target, broker/browser process, Mojo IPC, delegated handles | useful contrast: delegated handles can work, but LiteBox's stricter baseline keeps host handles broker-owned | | **Capsicum** | capability mode, broker opens resources and passes restricted fds | useful contrast: capability fd passing is powerful, but requires OS support for precise rights | -| **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | UserLiteBox should not be a generic syscall escape hatch | -| **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while UserLiteBox uses broker-owned rings/data channels for safe data paths | +| **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | local core should not be a generic syscall escape hatch | +| **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while local core uses broker-owned rings/data channels for safe data paths | | **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | -| **SKernel** | separates guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted UserLiteBox authority expansion | +| **SKernel** | separates guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted local-core authority expansion | | **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore should keep object identity internal and expose reference capabilities with generation checks while storing authoritative rights | | **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | | **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | @@ -654,7 +648,7 @@ kernel broker while keeping: ```text -Shim + UserLiteBox +Shim + local core ``` always in user mode. @@ -674,14 +668,14 @@ Future mapping: | Current responsibility | Target component | |---|---| | OP-TEE entry/request decoding, return conventions, TA ABI details | Shim | -| TA-local syscall helpers, guest buffer marshalling, local loader helpers | UserLiteBox, with broker revalidation for broker-visible data | -| non-authoritative TA/session/object caches | UserLiteBox | +| TA-local syscall helpers, guest buffer marshalling, local loader helpers | local core, with broker revalidation for broker-visible data | +| non-authoritative TA/session/object caches | local core | | authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access-control context | OP-TEE BrokerService backed by BrokerCore | | OP-TEE policy decisions and audit, including PTA and secure-storage authorization | PolicyEngine | | generic identities, capabilities, memory grants, lifecycle, wait/notify, accounting | BrokerCore | | trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform after PolicyEngine authorization | -This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable UserLiteBox and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. +This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable local core and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. ### `litebox` @@ -691,17 +685,17 @@ Future mapping: | Current responsibility | Target component | |---|---| -| ergonomic in-process helpers used by shims | UserLiteBox | -| guest-visible handle table view | UserLiteBox backed by BrokerCore | -| private sync, TLS, path conversion, guest marshalling | UserLiteBox | -| broker-owned data-channel wrappers | UserLiteBox | +| ergonomic in-process helpers used by shims | local core | +| guest-visible handle table view | local core backed by BrokerCore | +| private sync, TLS, path conversion, guest marshalling | local core | +| broker-owned data-channel wrappers | local core | | shared resource identity/lifetime | BrokerCore | | synchronization/wait/readiness authority for shared objects | BrokerCore | | final policy decision/audit | PolicyEngine | -| platform trait surface | internal UserLiteBox host-support layer, BrokerHost, and BrokerPlatform surfaces | -| shim/domain-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | +| platform trait surface | internal local-core deployment-support adapter, broker-kernel user-mode support, and BrokerPlatform surfaces | +| service-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | -The important change is that the current core API should not become the cross-boundary ABI. UserLiteBox can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker server/entry code adapts that protocol into BrokerCore domain calls. +The important change is that the current core API should not become the cross-boundary ABI. The local core can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker server/entry code adapts that protocol into BrokerCore domain calls. ### `litebox_platform_lvbs` @@ -712,14 +706,14 @@ Future mapping: | Current responsibility | Target component | |---|---| | page-table and address-space management | BrokerPlatform | -| VTL/trusted-domain trap and channel mechanism | BrokerHost | -| broker request decode and dispatch | broker server/entry layer | +| VTL/trusted-domain trap and channel mechanism | broker-kernel user-mode support | +| broker request decode and dispatch | `litebox_broker_host` | | domain validation and authorization | BrokerCore + PolicyEngine | | normal-world memory mapping and validation | BrokerPlatform | | host I/O, network backend hooks, root key/secrets | BrokerPlatform executing PolicyEngine-authorized operations | -| user-mode shim/platform helpers | UserLiteBox internals, not the current crate wholesale | +| user-mode shim/platform helpers | local-core internals, not the current crate wholesale | -In the new model, LVBS contributes both BrokerPlatform and BrokerHost pieces. BrokerPlatform owns privileged backend authority. BrokerHost exposes the host ABI that lets a user-mode UserLiteBox execute and reach the broker. The LVBS-targeted UserLiteBox selected by a deployment profile, such as `optee-on-lvbs`, should be smaller than the current LVBS crate. +In the new model, LVBS contributes both BrokerPlatform and broker-kernel user-mode support pieces. BrokerPlatform owns privileged backend authority. Broker-kernel user-mode support exposes the host ABI that lets a user-mode local core execute and reach the broker. The LVBS-targeted local-core profile selected by a deployment profile, such as `optee-on-lvbs`, should be smaller than the current LVBS crate. ### `litebox_runner_lvbs` @@ -730,14 +724,14 @@ Future mapping: | Current responsibility | Target component | |---|---| | early boot and trusted-domain initialization | broker bootstrap | -| VTL trap/channel dispatch | BrokerHost | -| broker request decode and dispatch | broker server/entry layer | +| VTL trap/channel dispatch | broker-kernel user-mode support | +| broker request decode and dispatch | `litebox_broker_host` | | domain validation and authorization | BrokerCore + PolicyEngine | | session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | -| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + UserLiteBox | +| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + local core | | authoritative TA/session/object identity currently held by the runner | BrokerCore + OP-TEE BrokerService + PolicyEngine | | user/broker compatibility checks | deployment profile negotiation | -| local user ABI support | BrokerHost | +| local user ABI support | broker-kernel user-mode support | The runner becomes less of an application runner and more of a broker bootstrap/entrypoint for the trusted deployment. @@ -750,8 +744,8 @@ Future mapping: | Current responsibility | Target component | |---|---| | command-line harness and binary loading for tests | runner/test harness | -| `OpteeShim` construction | Shim + UserLiteBox process setup | -| user-mode local execution | UserLiteBox setup | +| `OpteeShim` construction | Shim + local-core process setup | +| user-mode local execution | local-core setup | | broker/service compatibility | deployment profile negotiation | | shared/security-authoritative state | separate privileged broker process | @@ -765,8 +759,8 @@ Future mapping: | Current responsibility | Target component | |---|---| -| selecting a single `Platform` | separate into UserLiteBox profile selection, BrokerPlatform selection in the broker, and BrokerHost selection only for broker-kernel deployments | -| global platform accessor for shim-side code | UserLiteBox internal host-support accessor | +| selecting a single `Platform` | separate into local-core profile selection, BrokerPlatform selection in the broker, and broker-kernel user-mode support selection only for broker-kernel deployments | +| global platform accessor for shim-side code | local-core deployment-support accessor | | trusted backend selection | BrokerPlatform accessor inside the broker | | policy module/profile selection | PolicyEngine configuration inside the broker | @@ -778,17 +772,17 @@ The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-c | Current component | Target shape | |---|---| -| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus UserLiteBox-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | -| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted UserLiteBox host-support implementations; native host calls are limited to local non-authoritative mechanics and broker channels/rings. | -| `litebox_platform_linux_kernel` | broker-kernel pieces follow the LVBS boundary: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes BrokerHost. | -| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select UserLiteBox profile, create Shim/UserLiteBox, authenticate to the broker, and negotiate deployment profile compatibility. | -| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; move external entry/channel support into BrokerHost, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | +| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus local-core-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | +| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted local-core deployment-support implementations; native host calls are limited to local non-authoritative mechanics and broker channels/rings. | +| `litebox_platform_linux_kernel` | broker-kernel pieces follow the LVBS boundary: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes broker-kernel user-mode support. | +| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select local-core profile, create shim/local-core state, authenticate to the broker, and negotiate deployment profile compatibility. | +| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; move external entry/channel support into broker-kernel user-mode support, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | ## Initial implementation direction -The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority substrate: +The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority slice: -The first proof of concept should use: +The initial slice uses: ```text litebox_broker_protocol @@ -804,20 +798,20 @@ minimal PolicyEngine broker-owned event object ``` -Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through broker-backed local-core wrappers, while unrelated operations continue through the existing local compatibility path. Shims should keep calling local-core object interfaces; UserLiteBox/local-core entries can contain either local compatibility state or broker-backed wrappers with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. +Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through broker-backed local-core wrappers, while unrelated operations continue through the existing local compatibility path. Shims should keep calling local-core object interfaces; local-core entries can contain either local compatibility state or broker-backed wrappers with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. Then proceed incrementally: -1. Define the component boundary: `Shim`, `litebox_user`/UserLiteBox, optional shim-specific user clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, and, in broker-kernel deployments, `BrokerHost`. +1. Define the component boundary: `Shim`, local core, optional BrokerService clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, `litebox_broker_host`, and, in broker-kernel deployments, broker-kernel user-mode support. 2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces for the POC. -5. Add startup negotiation between runner/client and broker. The current PoC starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, UserLiteBox, and BrokerHost feature negotiation. +4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces. +5. Add startup negotiation between runner/client and broker. The current path starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, local-core profile, and deployment-support feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. 7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. -8. Introduce `litebox_user` as the untrusted UserLiteBox facade for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. +8. Continue shaping `litebox` as the untrusted local core for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. -10. Make UserLiteBox handle tables broker-backed views for migrated object families. +10. Make local-core handle tables broker-backed views for migrated object families. 11. Add a broker wait/wakeup channel. 12. Prototype a broker-owned pipe or queue with shared-ring data path. 13. Prototype a broker-owned file object with mediated control/data-channel I/O, not host-handle delegation. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index c01eeb9ff..7a45b4676 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -4,25 +4,25 @@ Implement the broker architecture as incremental vertical slices while keeping the existing LiteBox behavior working. -The target architecture is: +The target architecture has two trust domains: ```text User mode: - Shim + UserLiteBox + optional shim-specific user clients + Shim + local core + optional BrokerService clients Authority domain: - broker entry/server + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform - -Kernel-broker deployments: - BrokerHost supports user-mode UserLiteBox execution and broker channel + broker entry/server + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker-kernel user-mode support, in kernel-backed deployments ``` +The local core reaches local mechanics and broker channels through deployment support: the host OS user-mode ABI plus a broker transport endpoint in hosted userland, or calls into broker-kernel user-mode support plus broker-channel delivery in a broker-kernel deployment. + The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/event/data channels, authenticated per-process broker associations, and fail-closed behavior. ## Implementation principles - Build vertical slices, not a big-bang refactor. -- Keep UserLiteBox untrusted and broker authority explicit. +- Keep local core untrusted and broker authority explicit. - Keep BrokerCore shim-neutral. - Keep BrokerCore protocol-neutral and channel-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/server code adapts protocol requests and channel credentials before calling it. - Keep the broker protocol modular: the outer request/response envelope is for connection-level broker messages and coarse authority routing, while object/domain operations live in nested request/response families such as `CoreRequest::Event` and `EventResponse`. @@ -30,25 +30,27 @@ The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation - Put domain-specific authority in BrokerServices. - Put final allow/deny/audit decisions in PolicyEngine. - Keep BrokerPlatform as authorized backend execution, not a policy owner. -- Keep BrokerHost separate from broker request decode/authorization. +- Keep broker-kernel user-mode support separate from broker request decode/authorization; `litebox_broker_host` owns the reusable protocol-to-core adapter for both userland and kernel brokers. - Start in userland; move to kernel-broker deployment after broker semantics are proven. ## Phase 0: Boundary freeze Define and document the core vocabulary in code and docs: -- `UserLiteBox` -- `BrokerClient` adapter +- `local core` +- `litebox_broker_local` adapter +- optional BrokerService client - `BrokerCore` - `BrokerService` - `PolicyEngine` - `BrokerPlatform` -- `BrokerHost` +- `litebox_broker_host` +- broker-kernel user-mode support Exit criteria: - Design doc and code comments use one vocabulary. -- No new code treats UserLiteBox as trusted. +- No new code treats local core as trusted. - No broker API is modeled as host syscall proxying. ## Phase 1: Shared protocol/types crate @@ -95,7 +97,7 @@ Initial scope: Exit criteria: -- A user-side client can connect and negotiate. In the current hosted PoC, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. +- A user-side client can connect and negotiate. In the current hosted userland path, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. - Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. - Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. - The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. @@ -108,14 +110,14 @@ Exit criteria: - BrokerCore/object operations are grouped below the broker envelope instead of added as unrelated top-level `BrokerRequest` and `BrokerResponse` variants. - Control-channel contracts live in `litebox_broker_protocol::channel`, separate from semantic message DTO modules but in the same shared protocol crate. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. -## Phase 3: UserLiteBox facade +## Phase 3: Local core facade -Introduce UserLiteBox without moving every subsystem. +Introduce the local core without moving every subsystem. Initial scope: -- wrap existing LiteBox ergonomics behind UserLiteBox; -- add BrokerClient adapter; +- wrap existing LiteBox ergonomics in the local core; +- add the `litebox_broker_local` adapter; - keep existing local implementations available behind a profile/feature; - add a broker-backed handle-table view for experimental objects. @@ -123,7 +125,7 @@ Exit criteria: - Current tests can still use the local profile. - A broker-backed profile can issue a simple broker request. -- UserLiteBox handle entries can store opaque broker reference handles plus local cached rights hints. +- local-core handle entries can store opaque broker reference handles plus local cached rights hints. ## Phase 4: First broker-owned object @@ -136,7 +138,7 @@ Broker owns: - readiness state; - wait/wakeup state. -UserLiteBox owns: +The local core owns: - guest-visible handle number; - typed facade; @@ -150,7 +152,7 @@ Exit criteria: ## Phase 5: Broker-backed fd semantics -Move fd authority to BrokerCore while keeping guest fd numbers in UserLiteBox. +Move fd authority to BrokerCore while keeping guest fd numbers in local core. BrokerCore owns: @@ -162,7 +164,7 @@ BrokerCore owns: - inherited object tables; - process-exit cleanup. -UserLiteBox owns: +The local core owns: - guest fd number allocation; - raw-int fd conversion; @@ -172,7 +174,7 @@ UserLiteBox owns: Exit criteria: - Double close, stale fd, dup, inherited refs, and process-exit cleanup are tested. -- UserLiteBox cannot create a live broker object by editing local fd state. +- The local core cannot create a live broker object by editing local fd state. ## Phase 6: Control/notification/data channels @@ -216,7 +218,7 @@ Linux targets: Exit criteria: - Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, and `fcntl` cannot bypass broker policy after lockdown. -- Guest-visible mapping operations enter shim/UserLiteBox and are emulated or broker-mediated. +- Guest-visible mapping operations enter the shim/local-core path and are emulated or broker-mediated. ## Phase 8: Filesystem BrokerService @@ -234,7 +236,7 @@ Broker side owns: - permissions; - read/write policy. -UserLiteBox owns: +The local core owns: - path string conversion; - buffer marshalling; @@ -244,7 +246,7 @@ UserLiteBox owns: Exit criteria: - Filesystem operations are directory-relative. -- No host fd/HANDLE is exposed to UserLiteBox. +- No host fd/HANDLE is exposed to local core. - File data uses mediated control/data channel or broker-owned ring. - PolicyEngine can deny open/read/write independently. @@ -260,7 +262,7 @@ Broker side owns: - executable mapping policy; - page-fault decisions where applicable. -UserLiteBox owns: +The local core owns: - loader helpers; - guest pointer handling; @@ -271,7 +273,7 @@ Exit criteria: - Broker-visible mappings require BrokerCore validation and PolicyEngine authorization. - Executable memory cannot be created without rewrite/validation policy. -- Shared memory grants cannot be forged by UserLiteBox. +- Shared memory grants cannot be forged by local core. ## Phase 10: Multiprocess @@ -286,7 +288,7 @@ Broker owns: - process-exit cleanup; - inherited broker object table. -Shim/UserLiteBox owns: +Shim/local core owns: - ABI-specific fork semantics; - ABI-specific exec semantics; @@ -313,7 +315,7 @@ Broker side owns: - firewall policy context; - TX/RX rings where enabled. -UserLiteBox owns: +The local core owns: - socket syscall facade; - send/recv marshalling; @@ -322,7 +324,7 @@ UserLiteBox owns: Exit criteria: - L3/L4 firewall policy is enforced by PolicyEngine. -- UserLiteBox cannot send or receive guest-visible network traffic directly through host network devices. +- The local core cannot send or receive guest-visible network traffic directly through host network devices. - Data path uses broker-mediated operations or broker-owned rings. ## Phase 12: Kernel-broker deployment @@ -331,14 +333,15 @@ Only after userland semantics are stable, implement broker-kernel deployment. Separate trusted deployment code into: -- BrokerHost: user-mode execution support and channel delivery; +- broker-kernel user-mode support: trusted-domain support for user-mode execution and channel delivery; +- `litebox_broker_host`: broker protocol/core adaptation reused from the userland broker; - BrokerPlatform: privileged backend execution; - BrokerCore/BrokerServices/PolicyEngine: shared authority logic. Exit criteria: -- BrokerHost does not decode or authorize BrokerRequest. -- Broker server/entry protocol semantics and BrokerCore object semantics are reused through the same protocol-to-core adapter boundary. +- Broker-kernel user-mode support does not decode or authorize BrokerRequest. +- `litebox_broker_host` protocol semantics and BrokerCore object semantics are reused through the same protocol-to-core adapter boundary. - Kernel-broker deployment passes the same broker-object conformance tests as userland broker. ## Phase 13: OP-TEE BrokerService @@ -360,7 +363,7 @@ Exit criteria: - OP-TEE shim remains user-mode ABI code. - Trusted deployment does not need the full OP-TEE shim. -- OP-TEE authority cannot be created in UserLiteBox. +- OP-TEE authority cannot be created in local core. ## Suggested first milestone @@ -369,11 +372,11 @@ The smallest useful milestone is: ```text single process userland broker -typed broker client +typed control client control channel only minimal PolicyEngine broker-owned event object -UserLiteBox fd table maps guest fd -> broker reference handle +local-core fd table maps guest fd -> broker reference handle ``` This proves the trust boundary before taking on filesystem, networking, mapping, or multiprocess complexity. @@ -388,7 +391,7 @@ Add conformance tests at each layer: - stale reference IDs fail; - wrong reference generation fails; - process disconnect cleans up refs; -- UserLiteBox local handle edits cannot create authority; +- local-core handle edits cannot create authority; - shared-memory cursor/frame corruption fails closed; - broker failure forces session failure. diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index bc4f1bd0d..1560d4272 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -5,26 +5,12 @@ use crate::object::{ObjectId, ObjectKind}; use crate::{ BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, Result, }; -use litebox_broker_protocol::{EventConsumeMode, ObjectHandle, ReadinessState, WaitOutcome}; +use litebox_broker_protocol::{ + EventConsumeMode, EventConsumption, ObjectHandle, ReadinessState, WaitOutcome, +}; const MAX_EVENT_COUNT: u64 = u64::MAX - 1; -/// 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 } - } -} - impl BrokerCore

{ /// Creates a broker-owned event object. pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index e94fa5a08..974c142dd 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -26,7 +26,6 @@ mod policy; use alloc::collections::BTreeMap; pub use error::BrokerError; -pub use event::EventConsumption; use identity::BrokerCoreId; pub use identity::{BrokerAssociation, CallerCredential}; use litebox_broker_protocol::ObjectReferenceId; @@ -51,7 +50,7 @@ pub struct BrokerCoreLimits { } impl BrokerCoreLimits { - /// Conservative default limits for the first broker proof of concept. + /// Conservative default limits for initial broker deployments. pub const DEFAULT: Self = Self { max_objects: 4096, max_references: 4096, diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index dd1245f18..3d7adfdd6 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Channel-neutral broker server loop for the broker proof of concept. +//! Channel-neutral broker-side protocol/core adapter. //! //! This crate wires `litebox_broker_core` to any implementation of the neutral //! server control-channel trait. Concrete channels live in separate crates such as diff --git a/litebox_broker_host/src/server.rs b/litebox_broker_host/src/server.rs index 2e268c82f..c78605263 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/server.rs @@ -7,9 +7,9 @@ use litebox_broker_core::{ BrokerAssociation, BrokerCore, BrokerError, CallerCredential, PolicyEngine, }; use litebox_broker_protocol::{ - AddEventResponse, BrokerRequest, BrokerResponse, ConsumeEventResponse, CoreRequest, - CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, - ProtocolVersion, ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, + AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, + CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, ProtocolVersion, + ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, }; /// Protocol version this broker server implementation supports. @@ -159,12 +159,7 @@ fn handle_event_request( ), EventRequest::Consume(request) => handle_core_result( core.consume_event(association, request.handle, request.mode), - |consumption| { - event_response(EventResponse::Consume(ConsumeEventResponse::new( - consumption.value, - consumption.readiness, - ))) - }, + |consumption| event_response(EventResponse::Consume(consumption)), ), _ => BrokerResponse::Error(ErrorCode::UnsupportedOperation), } diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 5cd847b0e..8786c3866 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -5,7 +5,7 @@ use core::fmt; use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; -/// Errors returned by the broker client adapter. +/// Errors returned by the control client adapter. #[derive(Debug)] #[non_exhaustive] pub enum ClientError { @@ -57,8 +57,8 @@ impl fmt::Display for ClientError { 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 client has not negotiated protocol version"), - Self::AlreadyNegotiated => f.write_str("broker client already negotiated"), + Self::NotNegotiated => write!(f, "control client has not negotiated protocol version"), + Self::AlreadyNegotiated => f.write_str("control client already negotiated"), Self::ChannelClosed => write!(f, "broker closed the channel"), Self::UnknownResponse => f.write_str("unknown broker response"), Self::IncompatibleNegotiation { @@ -73,7 +73,7 @@ impl fmt::Display for ClientError { client_protocol_version, } => write!( f, - "broker client cannot request protocol version {requested:?}; client supports {client_protocol_version:?}" + "control client cannot request protocol version {requested:?}; client supports {client_protocol_version:?}" ), Self::UnsupportedNegotiatedVersion { required, @@ -118,5 +118,5 @@ where } } -/// Broker client result type. +/// Control client result type. pub type Result = core::result::Result>; diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index 3173555be..6ba025445 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -8,11 +8,11 @@ use litebox_broker_protocol::{ WaitOutcome, }; -use crate::{BrokerClient, ClientError, Result}; +use crate::{ClientError, ControlClient, Result}; const EVENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); -impl BrokerClient { +impl ControlClient { /// Creates a broker-owned event object. pub fn create_event(&mut self) -> Result { self.create_event_with_count(0) @@ -86,7 +86,7 @@ const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } -impl BrokerClient { +impl ControlClient { fn ensure_event_protocol(&self) -> Result<(), T::Error> { let negotiated = self.ensure_negotiated()?; if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index a22402258..3ec7be13c 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Typed client adapter for broker requests. +//! Typed control-channel client adapter for broker requests. //! -//! The client owns request/response sequencing but does not own a channel. +//! The control client owns request/response sequencing but does not own a channel. //! Userland, kernel, or ring-buffer deployments can provide channels by //! implementing [`litebox_broker_protocol::ClientControlChannel`]. @@ -24,13 +24,13 @@ use litebox_broker_protocol::{ pub use error::{ClientError, Result}; #[cfg(feature = "std")] -pub use worker::{BrokerClientWorker, BrokerClientWorkerError}; +pub use worker::{ControlClientWorker, ControlClientWorkerError}; /// Protocol version this client implementation requests by default. pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); -/// Typed client for broker operations. -pub struct BrokerClient { +/// Typed control-channel client for broker operations. +pub struct ControlClient { channel: T, state: ConnectionState, } @@ -43,8 +43,8 @@ enum ConnectionState { }, } -impl BrokerClient { - /// Creates a broker client over an already-connected control channel. +impl ControlClient { + /// Creates a control client over an already-connected control channel. pub const fn new(channel: T) -> Self { Self { channel, @@ -58,7 +58,7 @@ impl BrokerClient { } } -impl BrokerClient { +impl ControlClient { /// Returns the effective protocol version this connection negotiated. /// /// Feature gating must use this effective version because the broker may @@ -123,7 +123,7 @@ mod tests { fn event_operations_require_negotiation_without_sending() { let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert!(matches!( client.create_event(), @@ -136,7 +136,7 @@ mod tests { fn event_operations_require_event_protocol_version_without_sending() { let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::UnsupportedOperation))); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.state = ConnectionState::Active { negotiated_protocol_version: ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1), }; @@ -161,7 +161,7 @@ mod tests { let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: CLIENT_PROTOCOL_VERSION, })); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert_eq!(client.negotiate_version(requested).unwrap(), requested); assert_eq!( @@ -180,7 +180,7 @@ mod tests { let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: broker_version, })); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert!(matches!( client.negotiate_version(requested), @@ -201,7 +201,7 @@ mod tests { let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { broker_protocol_version: CLIENT_PROTOCOL_VERSION, })); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert!(matches!( client.negotiate_version(too_new), @@ -224,7 +224,7 @@ mod tests { let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { broker_protocol_version: fallback, })); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert!(matches!( client.negotiate_version(requested), diff --git a/litebox_broker_local/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs index 4a61ec211..760e738e5 100644 --- a/litebox_broker_local/src/negotiate.rs +++ b/litebox_broker_local/src/negotiate.rs @@ -5,9 +5,9 @@ use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, }; -use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION, ClientError, Result}; +use crate::{CLIENT_PROTOCOL_VERSION, ClientError, ControlClient, Result}; -impl BrokerClient { +impl ControlClient { /// Negotiates the default client protocol version. /// /// Returns the effective protocol version this connection will speak. diff --git a/litebox_broker_local/src/worker.rs b/litebox_broker_local/src/worker.rs index 88e72d414..c27168add 100644 --- a/litebox_broker_local/src/worker.rs +++ b/litebox_broker_local/src/worker.rs @@ -15,7 +15,7 @@ use std::{ use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ClientControlChannel}; -use crate::{BrokerClient, ClientError}; +use crate::{ClientError, ControlClient}; const PHASE_IDLE: u8 = 0; const PHASE_RESERVED: u8 = 1; @@ -24,26 +24,26 @@ const PHASE_RESPONSE_READY: u8 = 3; const PHASE_SHUTDOWN: u8 = 4; const DEFAULT_SHUTDOWN_WAIT: Duration = Duration::from_millis(100); -/// Error returned by [`BrokerClientWorker`]. +/// Error returned by [`ControlClientWorker`]. #[derive(Debug)] #[non_exhaustive] -pub enum BrokerClientWorkerError { - /// The wrapped broker client returned an error. +pub enum ControlClientWorkerError { + /// The wrapped control client returned an error. Client(ClientError), /// The worker is shutting down or has already shut down. Shutdown, } -impl fmt::Display for BrokerClientWorkerError { +impl fmt::Display for ControlClientWorkerError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Client(error) => write!(f, "broker client worker request failed: {error}"), - Self::Shutdown => f.write_str("broker client worker is shut down"), + Self::Client(error) => write!(f, "control client worker request failed: {error}"), + Self::Shutdown => f.write_str("control client worker is shut down"), } } } -impl core::error::Error for BrokerClientWorkerError +impl core::error::Error for ControlClientWorkerError where E: core::error::Error + 'static, { @@ -55,28 +55,28 @@ where } } -type WorkerResult = core::result::Result>; +type WorkerResult = core::result::Result>; type ShutdownHook = Box; -/// Dedicated worker for broker clients that must not run channel I/O on the caller thread. +/// Dedicated worker for control clients that must not run channel I/O on the caller thread. /// -/// The worker owns the [`BrokerClient`] and performs blocking channel operations +/// The worker owns the [`ControlClient`] and performs blocking channel operations /// on a dedicated thread. Callers submit one raw broker request at a time and /// block on an in-process condition variable until the worker publishes the /// response. This preserves the serial control-channel contract while keeping /// deployment-specific threads, such as rewritten guest syscall threads, away /// from host IPC syscalls. -pub struct BrokerClientWorker +pub struct ControlClientWorker where T: ClientControlChannel + Send + 'static, T::Error: Send + 'static, { - state: Arc>, + state: Arc>, worker: Mutex>>, finished: Mutex>, } -struct BrokerClientWorkerState { +struct ControlClientWorkerState { shutdown_requested: AtomicBool, phase: AtomicU8, request: Mutex>, @@ -87,18 +87,18 @@ struct BrokerClientWorkerState { shutdown_hook: Mutex>, } -impl BrokerClientWorker +impl ControlClientWorker where T: ClientControlChannel + Send + 'static, T::Error: Send + 'static, { - /// Starts a worker for an already-negotiated broker client. + /// Starts a worker for an already-negotiated control client. /// /// # Panics /// /// Panics if the worker-thread bookkeeping mutex is poisoned while the /// worker is being started. - pub fn new(client: BrokerClient) -> Self { + pub fn new(client: ControlClient) -> Self { Self::new_with_shutdown_hook(client, || {}) } @@ -108,11 +108,11 @@ where /// /// Panics if the worker-thread bookkeeping mutex is poisoned while the /// worker is being started. - pub fn new_with_shutdown_hook(client: BrokerClient, shutdown_hook: F) -> Self + pub fn new_with_shutdown_hook(client: ControlClient, shutdown_hook: F) -> Self where F: FnOnce() + Send + 'static, { - let state = Arc::new(BrokerClientWorkerState { + let state = Arc::new(ControlClientWorkerState { shutdown_requested: AtomicBool::new(false), phase: AtomicU8::new(PHASE_IDLE), request: Mutex::new(None), @@ -125,13 +125,13 @@ where let worker_state = state.clone(); let (finished_tx, finished_rx) = mpsc::channel(); let worker = thread::spawn(move || { - run_broker_client_worker(client, worker_state); + run_control_client_worker(client, worker_state); let _ = finished_tx.send(()); }); *state .worker_thread .lock() - .expect("broker client worker-thread mutex poisoned") = Some(worker.thread().clone()); + .expect("control client worker-thread mutex poisoned") = Some(worker.thread().clone()); Self { state, @@ -140,11 +140,11 @@ where } } - /// Sends one request on the active broker client and returns the raw protocol response. + /// Sends one request on the active control client and returns the raw protocol response. pub fn active_raw_request( &self, request: BrokerRequest, - ) -> core::result::Result> { + ) -> core::result::Result> { self.state.submit(request) } @@ -163,18 +163,18 @@ where let mut worker = self .worker .lock() - .expect("broker client worker mutex poisoned"); + .expect("control client worker mutex poisoned"); if let Some(worker) = worker.take() { self.state.request_shutdown(); match self .finished .lock() - .expect("broker client worker-finished mutex poisoned") + .expect("control client worker-finished mutex poisoned") .recv_timeout(DEFAULT_SHUTDOWN_WAIT) { - Ok(()) => worker.join().expect("broker client worker panicked"), + Ok(()) => worker.join().expect("control client worker panicked"), Err(RecvTimeoutError::Disconnected) => { - worker.join().expect("broker client worker panicked"); + worker.join().expect("control client worker panicked"); } Err(RecvTimeoutError::Timeout) => { drop(worker); @@ -184,7 +184,7 @@ where } } -impl Drop for BrokerClientWorker +impl Drop for ControlClientWorker where T: ClientControlChannel + Send + 'static, T::Error: Send + 'static, @@ -194,11 +194,11 @@ where } } -impl BrokerClientWorkerState { +impl ControlClientWorkerState { fn submit(&self, request: BrokerRequest) -> WorkerResult { loop { if self.shutdown_requested.load(Ordering::Acquire) { - return Err(BrokerClientWorkerError::Shutdown); + return Err(ControlClientWorkerError::Shutdown); } match self.phase.load(Ordering::Acquire) { PHASE_IDLE => { @@ -214,12 +214,12 @@ impl BrokerClientWorkerState { { if self.shutdown_requested.load(Ordering::Acquire) { self.cancel_reserved_request_on_shutdown(); - return Err(BrokerClientWorkerError::Shutdown); + return Err(ControlClientWorkerError::Shutdown); } break; } } - PHASE_SHUTDOWN => return Err(BrokerClientWorkerError::Shutdown), + PHASE_SHUTDOWN => return Err(ControlClientWorkerError::Shutdown), phase => self.wait_for_phase_change(phase, true), } } @@ -227,17 +227,17 @@ impl BrokerClientWorkerState { *self .request .lock() - .expect("broker client worker request mutex poisoned") = Some(request); + .expect("control client worker request mutex poisoned") = Some(request); self.phase.store(PHASE_REQUEST_READY, Ordering::Release); self.wake_worker(); loop { if self.shutdown_requested.load(Ordering::Acquire) { - return Err(BrokerClientWorkerError::Shutdown); + return Err(ControlClientWorkerError::Shutdown); } match self.phase.load(Ordering::Acquire) { PHASE_RESPONSE_READY => break, - PHASE_SHUTDOWN => return Err(BrokerClientWorkerError::Shutdown), + PHASE_SHUTDOWN => return Err(ControlClientWorkerError::Shutdown), phase => self.wait_for_phase_change(phase, true), } } @@ -245,9 +245,9 @@ impl BrokerClientWorkerState { let response = self .response .lock() - .expect("broker client worker response mutex poisoned") + .expect("control client worker response mutex poisoned") .take() - .expect("broker client worker did not publish a response"); + .expect("control client worker did not publish a response"); self.store_phase_and_notify(PHASE_IDLE); response } @@ -270,7 +270,7 @@ impl BrokerClientWorkerState { *self .response .lock() - .expect("broker client worker response mutex poisoned") = Some(response); + .expect("control client worker response mutex poisoned") = Some(response); self.store_phase_and_notify(PHASE_RESPONSE_READY); true } @@ -283,9 +283,9 @@ impl BrokerClientWorkerState { let request = self .request .lock() - .expect("broker client worker request mutex poisoned") + .expect("control client worker request mutex poisoned") .take() - .expect("broker client worker request missing"); + .expect("control client worker request missing"); if self.shutdown_requested.load(Ordering::Acquire) { self.store_phase_and_notify(PHASE_SHUTDOWN); None @@ -354,7 +354,7 @@ impl BrokerClientWorkerState { let _guard = self .requester_wait .lock() - .expect("broker client worker requester-wait mutex poisoned"); + .expect("control client worker requester-wait mutex poisoned"); self.shutdown_requested.store(true, Ordering::Release); self.requester_wakeup.notify_all(); } @@ -366,7 +366,7 @@ impl BrokerClientWorkerState { let _guard = self .requester_wait .lock() - .expect("broker client worker requester-wait mutex poisoned"); + .expect("control client worker requester-wait mutex poisoned"); self.phase.store(phase, Ordering::Release); self.requester_wakeup.notify_all(); } @@ -375,14 +375,14 @@ impl BrokerClientWorkerState { let mut guard = self .requester_wait .lock() - .expect("broker client worker requester-wait mutex poisoned"); + .expect("control client worker requester-wait mutex poisoned"); while self.phase.load(Ordering::Acquire) == observed && !(wake_on_shutdown && self.shutdown_requested.load(Ordering::Acquire)) { guard = self .requester_wakeup .wait(guard) - .expect("broker client worker requester-wait mutex poisoned"); + .expect("control client worker requester-wait mutex poisoned"); } } @@ -390,7 +390,7 @@ impl BrokerClientWorkerState { if let Some(worker) = self .worker_thread .lock() - .expect("broker client worker-thread mutex poisoned") + .expect("control client worker-thread mutex poisoned") .as_ref() { worker.unpark(); @@ -401,7 +401,7 @@ impl BrokerClientWorkerState { if let Some(hook) = self .shutdown_hook .lock() - .expect("broker client worker shutdown-hook mutex poisoned") + .expect("control client worker shutdown-hook mutex poisoned") .take() { hook(); @@ -409,9 +409,9 @@ impl BrokerClientWorkerState { } } -fn run_broker_client_worker( - mut client: BrokerClient, - state: Arc>, +fn run_control_client_worker( + mut client: ControlClient, + state: Arc>, ) where T: ClientControlChannel, { @@ -423,7 +423,7 @@ fn run_broker_client_worker( }; let response = client .active_raw_request(request) - .map_err(BrokerClientWorkerError::Client); + .map_err(ControlClientWorkerError::Client); if !state.publish_worker_response(response) { break; } @@ -454,7 +454,7 @@ mod tests { }; use super::*; - use crate::{BrokerClient, CLIENT_PROTOCOL_VERSION}; + use crate::{CLIENT_PROTOCOL_VERSION, ControlClient}; #[test] fn worker_returns_raw_protocol_errors() { @@ -468,9 +468,9 @@ mod tests { BrokerResponse::Error(ErrorCode::WouldBlock), ], ); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.negotiate().unwrap(); - let worker = BrokerClientWorker::new(client); + let worker = ControlClientWorker::new(client); let response = worker .active_raw_request(BrokerRequest::Negotiate { @@ -492,9 +492,9 @@ mod tests { broker_protocol_version: CLIENT_PROTOCOL_VERSION, }], ); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.negotiate().unwrap(); - let worker = BrokerClientWorker::new(client); + let worker = ControlClientWorker::new(client); worker.shutdown(); @@ -502,7 +502,7 @@ mod tests { worker.active_raw_request(BrokerRequest::Negotiate { protocol_version: CLIENT_PROTOCOL_VERSION, }), - Err(BrokerClientWorkerError::Shutdown) + Err(ControlClientWorkerError::Shutdown) )); } @@ -511,9 +511,9 @@ mod tests { let sent = Arc::new(Mutex::new(Vec::new())); let interrupted = Arc::new((Mutex::new(false), Condvar::new())); let channel = BlockingControlChannel::new(sent, interrupted.clone()); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.negotiate().unwrap(); - let worker = Arc::new(BrokerClientWorker::new_with_shutdown_hook(client, { + let worker = Arc::new(ControlClientWorker::new_with_shutdown_hook(client, { let interrupted = interrupted.clone(); move || { let (lock, wakeup) = &*interrupted; @@ -538,8 +538,8 @@ mod tests { assert!(matches!( requester.join().unwrap(), - Err(BrokerClientWorkerError::Shutdown - | BrokerClientWorkerError::Client(ClientError::Channel(_))) + Err(ControlClientWorkerError::Shutdown + | ControlClientWorkerError::Client(ClientError::Channel(_))) )); } @@ -548,9 +548,9 @@ mod tests { let sent = Arc::new(Mutex::new(Vec::new())); let interrupted = Arc::new((Mutex::new(false), Condvar::new())); let channel = BlockingControlChannel::new(sent, interrupted); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.negotiate().unwrap(); - let worker = Arc::new(BrokerClientWorker::new(client)); + let worker = Arc::new(ControlClientWorker::new(client)); let requester = { let worker = worker.clone(); @@ -573,7 +573,7 @@ mod tests { ); assert!(matches!( requester.join().unwrap(), - Err(BrokerClientWorkerError::Shutdown) + Err(ControlClientWorkerError::Shutdown) )); } @@ -582,9 +582,9 @@ mod tests { let sent = Arc::new(Mutex::new(Vec::new())); let interrupted = Arc::new((Mutex::new(false), Condvar::new())); let channel = BlockingControlChannel::new(sent, interrupted.clone()); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); client.negotiate().unwrap(); - let worker = Arc::new(BrokerClientWorker::new_with_shutdown_hook(client, { + let worker = Arc::new(ControlClientWorker::new_with_shutdown_hook(client, { let interrupted = interrupted.clone(); move || { let (lock, wakeup) = &*interrupted; @@ -610,8 +610,8 @@ mod tests { let (lock, _) = &*interrupted; assert!(*lock.lock().unwrap(), "shutdown hook was not invoked"); match requester.join().unwrap() { - Err(BrokerClientWorkerError::Shutdown) => {} - Err(BrokerClientWorkerError::Client(ClientError::Channel(error))) + Err(ControlClientWorkerError::Shutdown) => {} + Err(ControlClientWorkerError::Client(ClientError::Channel(error))) if error.kind() == io::ErrorKind::Interrupted => {} result => panic!("unexpected requester result: {result:?}"), } diff --git a/litebox_broker_protocol/src/event.rs b/litebox_broker_protocol/src/event.rs index 653be6677..efbaf7137 100644 --- a/litebox_broker_protocol/src/event.rs +++ b/litebox_broker_protocol/src/event.rs @@ -147,18 +147,21 @@ impl ConsumeEventRequest { } } -/// Response to an event consume request. +/// Result of consuming readiness credits from a broker-owned event object. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct ConsumeEventResponse { - /// The number of credits consumed. +pub struct EventConsumption { + /// Number of readiness credits consumed. pub value: u64, /// Readiness state after consuming credits. pub readiness: ReadinessState, } -impl ConsumeEventResponse { - /// Creates an event consume response. +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 index 221505d30..9aed5f01b 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -26,8 +26,8 @@ pub use channel::{ pub use error::ErrorCode; pub use event::{ AddEventRequest, AddEventResponse, ConsumeEventRequest, ConsumeEventResponse, - CreateEventRequest, CreateEventResponse, EventConsumeMode, ReadinessState, WaitEventRequest, - WaitEventResponse, WaitOutcome, + CreateEventRequest, CreateEventResponse, EventConsumeMode, EventConsumption, ReadinessState, + WaitEventRequest, WaitEventResponse, WaitOutcome, }; pub use message::{ BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, EventRequest, EventResponse, diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index b93b64057..ddd8cbe37 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -10,7 +10,7 @@ use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use litebox_broker_host::SUPPORTED_PROTOCOL_VERSION; -use litebox_broker_local::BrokerClient; +use litebox_broker_local::ControlClient; use litebox_broker_protocol::{ReadinessState, WaitOutcome}; use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; @@ -22,7 +22,7 @@ fn separate_process_broker_serves_event_object_requests() { channel .set_io_timeout(Some(Duration::from_secs(5))) .unwrap(); - let mut client = BrokerClient::new(channel); + let mut client = ControlClient::new(channel); assert_eq!(client.negotiate().unwrap(), SUPPORTED_PROTOCOL_VERSION); diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 8224d5177..dcee56719 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -11,7 +11,7 @@ use std::{ use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox::{BrokerControl, BrokerControlError}; -use litebox_broker_local::{BrokerClient, BrokerClientWorker}; +use litebox_broker_local::{ControlClient, ControlClientWorker}; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; @@ -19,8 +19,8 @@ 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 Client = BrokerClient; -type ClientWorker = BrokerClientWorker; +type Client = ControlClient; +type ClientWorker = ControlClientWorker; pub(crate) struct BrokerConnection { control: Arc, @@ -56,7 +56,7 @@ impl Drop for BrokerConnection { impl BrokerControlClient { fn new(client: Client, shutdown_stream: std::os::unix::net::UnixStream) -> Self { Self { - worker: BrokerClientWorker::new_with_shutdown_hook(client, move || { + worker: ControlClientWorker::new_with_shutdown_hook(client, move || { let _ = shutdown_stream.shutdown(Shutdown::Both); }), } @@ -108,7 +108,7 @@ fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result Date: Thu, 4 Jun 2026 14:52:49 -0700 Subject: [PATCH 43/66] Rename broker event channel to notification channel Update split-broker design and implementation docs to use control/notification/data channel terminology, reserving event for broker-owned event objects and eventfd-like behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 16 +++++++++------- docs/impl-plan.md | 6 +++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index c3e98c5aa..ba9983d6c 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -68,7 +68,7 @@ The local core contains: - guest pointer and guest memory marshalling helpers; - local caches and non-authoritative views of broker state; - private synchronization and wait helpers; -- broker-owned control/event/data channel wrappers; +- broker-owned control/notification/data channel wrappers; - the `litebox_broker_local` adapter; - internal deployment-support calls for local mechanics. @@ -221,14 +221,16 @@ Each sandboxed process has exactly one authenticated broker association. That as | Channel | Direction | Purpose | |---|---|---| | control | bidirectional | handshake, object operations, operation responses | -| event | broker to process | lifecycle, readiness, interrupt-like events, broker/session failure | +| notification | broker to process | lifecycle, readiness, interrupt-like notifications, broker/session failure | | data | bidirectional | bulk payload bytes associated with authorized object operations | The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. The local core does not prove identity by filling in request fields. +Use "notification channel" for broker-initiated asynchronous traffic. Avoid "event channel" because event already names a broker-owned object family and guest-visible eventfd-like behavior. + The protocol exposes broker-owned objects through opaque per-association reference handles: a reference identifier plus a reference generation. Object identifiers stay broker-internal. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, reference generations, rights, and policy. Object types and rights are broker-internal for authorization; the local core cannot amplify authority by editing request fields. -The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. The local core receives only broker reference handles, response data, event data, and broker-created channel endpoints whose authority is already bound by the broker. +The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. The local core receives only broker reference handles, response data, notification payloads, and broker-created channel endpoints whose authority is already bound by the broker. Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. The control channel authorizes an operation and binds it to an object and request identifier; the data channel carries bytes for that authorized operation. Shared memory is an optimization, not an authority transfer, and all shared-memory contents remain untrusted. @@ -314,8 +316,8 @@ The initial Linux ring set can use five unidirectional rings: |---|---|---| | control | broker to runner | broker responses, setup, control messages | | control | runner to broker | broker requests and responses | -| event | broker to runner | asynchronous events and fail-closed notifications | -| data | broker to runner | bulk response/event payload bytes | +| notification | broker to runner | asynchronous notifications and fail-closed notices | +| data | broker to runner | bulk response/notification payload bytes | | data | runner to broker | bulk request payload bytes | Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a local core scheduler, but keeping the MPSC layout preserves one ring format. @@ -344,7 +346,7 @@ Shared spec crates should define: - PolicyEngine policy versions, policy profile IDs, and audit requirements; - broker capability names and profiles; - deployment-support ABI names and versions; -- control/event/data channel formats; +- control/notification/data channel formats; - shared-memory/ring layout versions and validation rules; - host syscall profiles for bootstrap, fast local mode, and strict mode; - deployment profiles that bind a shim, local-core profile, broker channel, required services, and required broker features. @@ -601,7 +603,7 @@ Authority domain: |---|---| | local-core cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | | user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | -| ABI becomes too chatty | batching, shared memory data planes, control/event/data channel separation, local private fast paths | +| ABI becomes too chatty | batching, shared memory data planes, control/notification/data channel separation, local private fast paths | | duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in local core | | handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | | broker bottleneck from no host-handle delegation | use broker-owned rings, batching, object-specific data channels, and policy caching | diff --git a/docs/impl-plan.md b/docs/impl-plan.md index 7a45b4676..c3fe18f5d 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -17,7 +17,7 @@ Authority domain: The local core reaches local mechanics and broker channels through deployment support: the host OS user-mode ABI plus a broker transport endpoint in hosted userland, or calls into broker-kernel user-mode support plus broker-channel delivery in a broker-kernel deployment. -The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/event/data channels, authenticated per-process broker associations, and fail-closed behavior. +The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/notification/data channels, authenticated per-process broker associations, and fail-closed behavior. ## Implementation principles @@ -65,7 +65,7 @@ Initial contents: - readiness and wait outcome payloads; - ABI-neutral error categories; -Richer request/response envelopes, control/event/data channel frame headers, policy profile IDs, host syscall profile IDs, and feature negotiation structures are added when later milestones need them. +Richer request/response envelopes, control/notification/data channel frame headers, policy profile IDs, host syscall profile IDs, and feature negotiation structures are added when later milestones need them. Prefer `no_std` for shared types, adding `alloc` only in crates that need owned buffers, so they can be reused by userland and kernel-broker deployments. @@ -183,7 +183,7 @@ Add the durable-unicorn-style control/notification/data channel separation. Channels: - control: object operations and responses; -- event: broker-to-process async events; +- notification: broker-to-process asynchronous notifications; - data: bulk payload bytes. Linux hosted prototype: From 70fa831f2d1cb8dd73eecaab4ac8eb7996358303 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 15:22:09 -0700 Subject: [PATCH 44/66] Refactor broker policy engine Use a concrete configurable PolicyEngine instead of encoding policy choice in BrokerCore's type parameter. Update broker host and test call sites to construct explicit policy instances. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/event.rs | 42 ++---- litebox_broker_core/src/identity.rs | 10 +- litebox_broker_core/src/lib.rs | 15 +- litebox_broker_core/src/object.rs | 28 ++-- litebox_broker_core/src/policy.rs | 152 +++++++++++++++------ litebox_broker_host/src/server.rs | 54 ++++---- litebox_broker_userland/src/main.rs | 4 +- litebox_runner_linux_userland/tests/run.rs | 15 +- 8 files changed, 181 insertions(+), 139 deletions(-) diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 1560d4272..f9a93f487 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -2,16 +2,14 @@ // Licensed under the MIT license. use crate::object::{ObjectId, ObjectKind}; -use crate::{ - BrokerAssociation, BrokerCore, BrokerError, ObjectRights, ObjectType, PolicyEngine, Result, -}; +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

{ +impl BrokerCore { /// Creates a broker-owned event object. pub fn create_event(&mut self, association: &BrokerAssociation) -> Result { self.create_event_with_count(association, 0) @@ -169,15 +167,12 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{ - BrokerCore, CallerCredential, EventOnlyPolicy, ObjectOperation, PolicyDecision, - PolicyEngine, PolicyOperation, - }; + use crate::{BrokerCore, CallerCredential, PolicyEngine}; use litebox_broker_protocol::ObjectReferenceGeneration; #[test] fn wait_rejects_invalid_references_with_expected_errors() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -217,7 +212,9 @@ mod tests { #[test] fn create_event_uses_policy_granted_reference_rights() { - let mut core = BrokerCore::new(WaitOnlyCreatePolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only_with_reference_rights( + ObjectRights::WAIT, + )); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -236,7 +233,9 @@ mod tests { #[test] fn event_readiness_state_only_reports_authorized_directions() { - let mut core = BrokerCore::new(WaitOnlyCreatePolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only_with_reference_rights( + ObjectRights::WAIT, + )); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -278,25 +277,4 @@ mod tests { assert_eq!(event.count, 1); assert_eq!(event.readiness_generation, u64::MAX); } - - struct WaitOnlyCreatePolicy; - - impl PolicyEngine for WaitOnlyCreatePolicy { - fn authorize(&mut self, operation: PolicyOperation) -> Result { - match operation { - PolicyOperation::Object { - object_type: ObjectType::Event, - operation: ObjectOperation::Create, - .. - } => Ok(PolicyDecision::GrantObjectReference { - rights: ObjectRights::WAIT, - }), - PolicyOperation::Object { - object_type: ObjectType::Event, - operation: ObjectOperation::Use { .. }, - .. - } => Ok(PolicyDecision::Authorized), - } - } - } } diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 6b8e08e6f..01df25bd1 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -113,7 +113,7 @@ impl BrokerAssociation { } } -impl

BrokerCore

{ +impl BrokerCore { /// Allocates broker authority state for one authenticated caller association. pub fn create_association( &mut self, @@ -135,11 +135,11 @@ impl

BrokerCore

{ #[cfg(test)] mod tests { use super::*; - use crate::DefaultDenyPolicy; + use crate::PolicyEngine; #[test] fn create_association_assigns_distinct_identities_and_preserves_credential() { - let mut core = BrokerCore::new(DefaultDenyPolicy); + let mut core = BrokerCore::new(PolicyEngine::default_deny()); let first = core .create_association(CallerCredential::Unauthenticated) @@ -158,8 +158,8 @@ mod tests { #[test] fn create_association_scopes_identity_to_core_instance() { - let mut first_core = BrokerCore::new(DefaultDenyPolicy); - let mut second_core = BrokerCore::new(DefaultDenyPolicy); + let mut first_core = BrokerCore::new(PolicyEngine::default_deny()); + let mut second_core = BrokerCore::new(PolicyEngine::default_deny()); let first = first_core .create_association(CallerCredential::Unauthenticated) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 974c142dd..1d143b80d 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -31,10 +31,7 @@ pub use identity::{BrokerAssociation, CallerCredential}; use litebox_broker_protocol::ObjectReferenceId; use object::{ObjectEntry, ObjectId, ObjectReference}; pub use object::{ObjectRights, ObjectType}; -pub use policy::{ - DefaultDenyPolicy, EventOnlyPolicy, ObjectOperation, PolicyDecision, PolicyEngine, - PolicyOperation, -}; +pub use policy::{ObjectOperation, PolicyDecision, PolicyEngine, PolicyOperation, PolicyProfile}; /// BrokerCore result type. pub type Result = core::result::Result; @@ -72,9 +69,9 @@ impl Default for BrokerCoreLimits { } /// Channel-independent broker authority state. -pub struct BrokerCore

{ +pub struct BrokerCore { core_id: BrokerCoreId, - policy: P, + policy: PolicyEngine, limits: BrokerCoreLimits, next_process_id: u64, next_object_id: u64, @@ -83,14 +80,14 @@ pub struct BrokerCore

{ references: BTreeMap, } -impl

BrokerCore

{ +impl BrokerCore { /// Creates a broker core with the provided policy engine. - pub fn new(policy: P) -> Self { + pub fn new(policy: PolicyEngine) -> Self { Self::new_with_limits(policy, BrokerCoreLimits::DEFAULT) } /// Creates a broker core with explicit authority-state limits. - pub fn new_with_limits(policy: P, limits: BrokerCoreLimits) -> Self { + pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Self { Self { core_id: identity::allocate_core_id(), policy, diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index 408796bf3..f7d0f7b1f 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -5,9 +5,7 @@ use core::ops::BitOr; use crate::event::EventObject; use crate::identity::{AssociationIdentity, BrokerAssociation}; -use crate::{ - BrokerCore, BrokerError, PolicyDecision, PolicyEngine, PolicyOperation, Result, allocate_id, -}; +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. @@ -97,7 +95,7 @@ impl ObjectKind { } } -impl BrokerCore

{ +impl BrokerCore { /// Inserts a broker object and mints its first owned reference. /// /// The current POC never reuses reference slots, so the reference @@ -220,7 +218,7 @@ impl BrokerCore

{ } } -impl

BrokerCore

{ +impl BrokerCore { /// Closes one object reference owned by an association. /// /// The underlying object is released when this was the last live reference. @@ -290,13 +288,11 @@ pub(crate) struct AuthorizedObject { #[cfg(test)] mod tests { use super::*; - use crate::{ - BrokerCoreLimits, BrokerError, CallerCredential, DefaultDenyPolicy, EventOnlyPolicy, - }; + use crate::{BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine}; #[test] fn object_and_reference_allocators_issue_max_id_then_exhaust() { - let mut core = BrokerCore::new(DefaultDenyPolicy); + let mut core = BrokerCore::new(PolicyEngine::default_deny()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -329,7 +325,7 @@ mod tests { #[test] fn insert_object_with_reference_enforces_object_and_reference_limits() { let mut object_limited = - BrokerCore::new_with_limits(EventOnlyPolicy, BrokerCoreLimits::new(0, 1)); + BrokerCore::new_with_limits(PolicyEngine::event_only(), BrokerCoreLimits::new(0, 1)); let association = object_limited .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -340,7 +336,7 @@ mod tests { ); let mut reference_limited = - BrokerCore::new_with_limits(EventOnlyPolicy, BrokerCoreLimits::new(1, 0)); + BrokerCore::new_with_limits(PolicyEngine::event_only(), BrokerCoreLimits::new(1, 0)); let association = reference_limited .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -353,7 +349,7 @@ mod tests { #[test] fn close_association_releases_owned_references_and_orphaned_objects() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -370,13 +366,13 @@ mod tests { #[test] fn foreign_core_association_cannot_authorize_matching_handle_values() { - let mut owner_core = BrokerCore::new(EventOnlyPolicy); + let mut owner_core = BrokerCore::new(PolicyEngine::event_only()); let owner = owner_core .create_association(CallerCredential::Unauthenticated) .unwrap(); let handle = owner_core.create_event(&owner).unwrap(); - let mut other_core = BrokerCore::new(EventOnlyPolicy); + let mut other_core = BrokerCore::new(PolicyEngine::event_only()); let other = other_core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -391,7 +387,7 @@ mod tests { #[test] fn close_object_reference_releases_reference_and_orphaned_object() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -408,7 +404,7 @@ mod tests { #[test] fn close_object_reference_rejects_stale_and_foreign_handles() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let owner = core .create_association(CallerCredential::Unauthenticated) .unwrap(); diff --git a/litebox_broker_core/src/policy.rs b/litebox_broker_core/src/policy.rs index 534f9e9af..1c2ecdaeb 100644 --- a/litebox_broker_core/src/policy.rs +++ b/litebox_broker_core/src/policy.rs @@ -68,53 +68,106 @@ impl PolicyOperation { } } -/// Broker policy decision interface. -pub trait PolicyEngine { - /// Authorizes or denies a broker operation. - fn authorize(&mut self, operation: PolicyOperation) -> Result; +/// 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, } -/// Policy engine that denies every operation. -#[derive(Clone, Copy, Debug, Default)] -pub struct DefaultDenyPolicy; +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) + } -impl PolicyEngine for DefaultDenyPolicy { - fn authorize(&mut self, _operation: PolicyOperation) -> Result { - Err(BrokerError::PolicyDenied) + /// 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), + } } } -/// Policy engine that allows only the first POC event-object surface. -/// -/// The current event create operation grants `WAIT | WRITE` on the initial -/// reference. Use requests may ask for any non-empty subset of those rights. -#[derive(Clone, Copy, Debug, Default)] -pub struct EventOnlyPolicy; +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); -impl PolicyEngine for EventOnlyPolicy { - fn authorize(&mut self, 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_REFERENCE_RIGHTS.contains(rights) => { - Ok(PolicyDecision::Authorized) - } - PolicyOperation::Object { - object_type: ObjectType::Event, - .. - } => Err(BrokerError::PolicyDenied), +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), } } @@ -124,7 +177,7 @@ mod tests { #[test] fn event_only_policy_allows_only_current_event_surface() { - let mut policy = EventOnlyPolicy; + let mut policy = PolicyEngine::event_only(); assert_eq!( policy.authorize(PolicyOperation::create_object( @@ -168,4 +221,27 @@ mod tests { 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/src/server.rs b/litebox_broker_host/src/server.rs index c78605263..e66264a5f 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/server.rs @@ -3,9 +3,7 @@ use core::fmt; -use litebox_broker_core::{ - BrokerAssociation, BrokerCore, BrokerError, CallerCredential, PolicyEngine, -}; +use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCredential}; use litebox_broker_protocol::{ AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, ProtocolVersion, @@ -16,12 +14,11 @@ use litebox_broker_protocol::{ pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); /// Serves one broker connection over the provided connected control channel. -pub fn serve_connection( - core: &mut BrokerCore

, +pub fn serve_connection( + core: &mut BrokerCore, channel: &mut T, ) -> Result> where - P: PolicyEngine, T: ServerControlChannel, { let peer_credential = channel @@ -38,13 +35,12 @@ where result } -fn serve_request_loop( - core: &mut BrokerCore

, +fn serve_request_loop( + core: &mut BrokerCore, channel: &mut T, association: &BrokerAssociation, ) -> Result> where - P: PolicyEngine, T: ServerControlChannel, { let mut state = ConnectionState::AwaitingNegotiation; @@ -73,8 +69,8 @@ fn caller_credential_from_peer(peer_credential: PeerCredential) -> Result( - core: &mut BrokerCore

, +fn handle_received_request( + core: &mut BrokerCore, association: &BrokerAssociation, state: &mut ConnectionState, received: ReceivedBrokerRequest, @@ -87,8 +83,8 @@ fn handle_received_request( } } -fn handle_request( - core: &mut BrokerCore

, +fn handle_request( + core: &mut BrokerCore, association: &BrokerAssociation, state: &mut ConnectionState, request: BrokerRequest, @@ -109,8 +105,8 @@ fn handle_request( } } -fn handle_active_request( - core: &mut BrokerCore

, +fn handle_active_request( + core: &mut BrokerCore, association: &BrokerAssociation, _negotiated_protocol_version: ProtocolVersion, request: BrokerRequest, @@ -127,8 +123,8 @@ fn handle_active_request( } } -fn handle_core_request( - core: &mut BrokerCore

, +fn handle_core_request( + core: &mut BrokerCore, association: &BrokerAssociation, request: CoreRequest, ) -> BrokerResponse { @@ -138,8 +134,8 @@ fn handle_core_request( } } -fn handle_event_request( - core: &mut BrokerCore

, +fn handle_event_request( + core: &mut BrokerCore, association: &BrokerAssociation, request: EventRequest, ) -> BrokerResponse { @@ -318,7 +314,7 @@ where #[cfg(test)] mod tests { use super::*; - use litebox_broker_core::EventOnlyPolicy; + use litebox_broker_core::PolicyEngine; use litebox_broker_protocol::CreateEventRequest; #[test] @@ -410,7 +406,7 @@ mod tests { #[test] fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let mut channel = FakeServerChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { @@ -444,7 +440,7 @@ mod tests { #[test] fn serve_connection_closes_after_protocol_violation() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let mut channel = FakeServerChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request(event_create_request( 0, @@ -469,7 +465,7 @@ mod tests { #[test] fn serve_connection_returns_channel_error_when_response_send_fails() { - let mut core = BrokerCore::new(EventOnlyPolicy); + let mut core = BrokerCore::new(PolicyEngine::event_only()); let mut channel = FakeServerChannel::new(std::vec::Vec::from([Ok(Some( ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { protocol_version: SUPPORTED_PROTOCOL_VERSION, @@ -495,20 +491,16 @@ mod tests { ); } - fn new_association() -> ( - BrokerCore, - BrokerAssociation, - ConnectionState, - ) { - let mut core = BrokerCore::new(EventOnlyPolicy); + fn new_association() -> (BrokerCore, BrokerAssociation, ConnectionState) { + let mut core = BrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); (core, association, ConnectionState::AwaitingNegotiation) } - fn negotiate( - core: &mut BrokerCore

, + fn negotiate( + core: &mut BrokerCore, association: &BrokerAssociation, state: &mut ConnectionState, ) { diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index e9ddf6af3..57aa32af9 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -9,7 +9,7 @@ use std::os::unix::net::UnixListener; use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; -use litebox_broker_core::{BrokerCore, EventOnlyPolicy}; +use litebox_broker_core::{BrokerCore, PolicyEngine}; use litebox_broker_host::{BrokerServeError, serve_connection}; use litebox_broker_transport::unix_socket::UnixStreamServerControlChannel; @@ -22,7 +22,7 @@ fn main() -> io::Result<()> { let (stream, _) = listener.accept()?; let mut channel = UnixStreamServerControlChannel::from_accepted(stream); channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; - let mut broker = BrokerCore::new(EventOnlyPolicy); + let mut broker = BrokerCore::new(PolicyEngine::event_only()); serve_connection(&mut broker, &mut channel) .map(|_| ()) .map_err(broker_error) diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index d9bb52f15..638c5b474 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -289,10 +289,7 @@ impl Drop for TestBroker { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -fn spawn_test_broker

(socket_path: &Path, policy: P) -> TestBroker -where - P: litebox_broker_core::PolicyEngine + Send + 'static, -{ +fn spawn_test_broker(socket_path: &Path, policy: litebox_broker_core::PolicyEngine) -> TestBroker { let _ = std::fs::remove_file(socket_path); let (ready_tx, ready_rx) = std::sync::mpsc::channel(); @@ -345,7 +342,10 @@ where #[test] fn test_runner_connects_to_broker() { let socket_path = unique_test_socket_path("runner-broker"); - let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::DefaultDenyPolicy); + let broker_thread = spawn_test_broker( + &socket_path, + litebox_broker_core::PolicyEngine::default_deny(), + ); let true_path = run_which("true"); Runner::new(&true_path, "broker_true_rewriter") @@ -358,7 +358,10 @@ fn test_runner_connects_to_broker() { #[test] fn test_broker_backed_eventfd_with_rewriter() { let socket_path = unique_test_socket_path("broker-eventfd"); - let broker_thread = spawn_test_broker(&socket_path, litebox_broker_core::EventOnlyPolicy); + let broker_thread = spawn_test_broker( + &socket_path, + litebox_broker_core::PolicyEngine::event_only(), + ); let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); Runner::new(&target, "broker_eventfd_rewriter") From 28ae7fb2fb753e780873f3c0f45a3762c1cab41e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 15:28:16 -0700 Subject: [PATCH 45/66] Fix CI eventfd regressions Restore shim-local nonblocking eventfd behavior when no broker control channel is configured, while still using broker-backed counters when available. Update the ratchet and missing test C-file copyright header. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dev_tests/src/ratchet.rs | 1 + litebox_runner_linux_userland/tests/eventfd.c | 3 + litebox_shim_linux/src/syscalls/eventfd.rs | 58 ++++++++++++------- 3 files changed, 41 insertions(+), 21 deletions(-) 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_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 7f21825dd..7194e28f6 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + #include #include #include diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index f3dacaf38..78f8dc531 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -8,7 +8,7 @@ use core::sync::atomic::AtomicU32; use litebox::{ event::{ Events, IOPollable, - counter::{EventCounter, EventCounterReadMode}, + counter::{EventCounter, EventCounterError, EventCounterReadMode}, observer::Observer, polling::{Pollee, TryOpError}, wait::WaitContext, @@ -67,10 +67,6 @@ impl EventFileCounter bool { - matches!(self, Self::LocalCore(_)) - } - fn read( &self, cx: &WaitContext<'_, Platform>, @@ -192,14 +188,7 @@ impl EventFile { super::common_functions_for_file_status!(); pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { - let current_status = self.get_status(); let new_status = (self.get_status() & mask.complement()) | (requested & mask); - if !current_status.contains(OFlags::NONBLOCK) - && new_status.contains(OFlags::NONBLOCK) - && !self.counter.is_local_core() - { - return Err(Errno::EINVAL); - } if !new_status.contains(OFlags::NONBLOCK) && !self.counter.supports_blocking_operations() { return Err(Errno::EINVAL); } @@ -237,9 +226,11 @@ impl GlobalState { let count = u64::from(initval); let counter = if flags.contains(EfdFlags::NONBLOCK) { - EventFileCounter::local_core( - EventCounter::new(&self.litebox, count).map_err(Errno::from)?, - ) + 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) }; @@ -257,7 +248,7 @@ fn consume_mode(semaphore: bool) -> EventCounterReadMode { #[cfg(test)] mod tests { - use litebox::event::wait::WaitState; + use litebox::{event::wait::WaitState, fs::OFlags}; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::platform; @@ -347,14 +338,39 @@ mod tests { } #[test] - fn test_nonblocking_eventfd_requires_broker_control() { + fn test_nonblocking_eventfd_uses_shim_local_without_broker_control() { let task = crate::syscalls::tests::init_platform(None); + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::NONBLOCK) + .unwrap(); assert_eq!( - task.global - .create_linux_eventfd(0, EfdFlags::NONBLOCK) - .map(|_| ()), - Err(Errno::EIO) + 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)); + } + + #[test] + fn test_shim_local_eventfd_can_be_made_nonblocking() { + let task = crate::syscalls::tests::init_platform(None); + + let eventfd = task + .global + .create_linux_eventfd(0, EfdFlags::empty()) + .unwrap(); + + eventfd + .set_status_flags(OFlags::NONBLOCK, OFlags::NONBLOCK) + .unwrap(); + assert_eq!( + eventfd.read(&WaitState::new(platform()).context()), + Err(Errno::EAGAIN) ); } } From aacb196e9b965dec158c4a9c4ab242d024e050ae Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 16:27:21 -0700 Subject: [PATCH 46/66] Simplify broker identity model Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 4 +- litebox_broker_core/src/error.rs | 3 + litebox_broker_core/src/event.rs | 8 +- litebox_broker_core/src/identity.rs | 113 +++------------ litebox_broker_core/src/lib.rs | 120 ++++++++++++++-- litebox_broker_core/src/object.rs | 93 ++++++------- litebox_broker_host/src/server.rs | 152 ++++++++++++--------- litebox_broker_userland/src/main.rs | 2 +- litebox_runner_linux_userland/tests/run.rs | 65 +++++---- 9 files changed, 297 insertions(+), 263 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index ba9983d6c..48cb5d190 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -129,7 +129,7 @@ The broker architecture should use crate names that make the authority boundary | Crate | Initial role | |---|---| | `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, client/server control-channel traits, and request/response message-body encoding. | -| `litebox_broker_core` | Protocol- and channel-independent authority logic: broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | +| `litebox_broker_core` | Protocol- and channel-independent authority logic: the single broker core constructed for the broker process/kernel lifetime, broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | | `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_transport` | Hosted concrete broker transport implementations. The current implementation is a Unix-domain-socket control channel under `unix_socket`. The crate owns stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/host crates. | | `litebox_broker_userland` | Hosted `std` broker executable. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket transport together. | @@ -806,7 +806,7 @@ Then proceed incrementally: 1. Define the component boundary: `Shim`, local core, optional BrokerService clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, `litebox_broker_host`, and, in broker-kernel deployments, broker-kernel user-mode support. 2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process/session association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. +3. Create `litebox_broker_core` with broker-owned process association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. 4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces. 5. Add startup negotiation between runner/client and broker. The current path starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, local-core profile, and deployment-support feature negotiation. 6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. diff --git a/litebox_broker_core/src/error.rs b/litebox_broker_core/src/error.rs index 1550e31bf..8f9dceace 100644 --- a/litebox_broker_core/src/error.rs +++ b/litebox_broker_core/src/error.rs @@ -19,6 +19,8 @@ pub enum BrokerError { 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. @@ -36,6 +38,7 @@ impl fmt::Display for BrokerError { 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"), diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index f9a93f487..0f0d300d9 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -167,12 +167,12 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerCore, CallerCredential, PolicyEngine}; + use crate::{CallerCredential, PolicyEngine, test_support::TestBrokerCore}; use litebox_broker_protocol::ObjectReferenceGeneration; #[test] fn wait_rejects_invalid_references_with_expected_errors() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + let mut core = TestBrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -212,7 +212,7 @@ mod tests { #[test] fn create_event_uses_policy_granted_reference_rights() { - let mut core = BrokerCore::new(PolicyEngine::event_only_with_reference_rights( + let mut core = TestBrokerCore::new(PolicyEngine::event_only_with_reference_rights( ObjectRights::WAIT, )); let association = core @@ -233,7 +233,7 @@ mod tests { #[test] fn event_readiness_state_only_reports_authorized_directions() { - let mut core = BrokerCore::new(PolicyEngine::event_only_with_reference_rights( + let mut core = TestBrokerCore::new(PolicyEngine::event_only_with_reference_rights( ObjectRights::WAIT, )); let association = core diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 01df25bd1..335bc945f 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use core::sync::atomic::{AtomicU64, Ordering}; - use crate::{BrokerCore, Result, allocate_id}; /// Caller identity information supplied by the broker entry layer. @@ -17,61 +15,14 @@ pub enum CallerCredential { Unauthenticated, } -macro_rules! id_type { - ($(#[$meta:meta])* $name:ident) => { - $(#[$meta])* - #[repr(transparent)] - #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub(crate) struct $name(u64); - - impl $name { - pub(crate) const fn new(raw: u64) -> Self { - Self(raw) - } - } - }; -} - -id_type! { - /// Process-local broker core identity. - BrokerCoreId -} - -static NEXT_CORE_ID: AtomicU64 = AtomicU64::new(1); - -pub(crate) fn allocate_core_id() -> BrokerCoreId { - BrokerCoreId::new(NEXT_CORE_ID.fetch_add(1, Ordering::Relaxed)) -} - -id_type! { - /// Broker-assigned sandbox session identity. - SessionId -} - -impl SessionId { - pub(crate) const FIRST: Self = Self::new(1); -} - -id_type! { - /// Broker-assigned guest process identity. - ProcessId -} - -/// Broker-assigned identity for one caller association. -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct AssociationIdentity { - core: BrokerCoreId, - session: SessionId, - process: ProcessId, -} +/// Broker-assigned guest process identity. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct ProcessId(u64); -impl AssociationIdentity { - const fn new(core_id: BrokerCoreId, session_id: SessionId, process_id: ProcessId) -> Self { - Self { - core: core_id, - session: session_id, - process: process_id, - } +impl ProcessId { + pub(crate) const fn new(raw: u64) -> Self { + Self(raw) } } @@ -79,32 +30,26 @@ impl AssociationIdentity { /// /// 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. The token is scoped by a process-local BrokerCore -/// identity so handles cannot be replayed across distinct core instances. +/// on that association. #[derive(Debug, PartialEq, Eq)] pub struct BrokerAssociation { - /// Broker-assigned sandbox session and guest process identity. - identity: AssociationIdentity, + /// 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( - core_id: BrokerCoreId, - session_id: SessionId, - process_id: ProcessId, - caller_credential: CallerCredential, - ) -> Self { + pub(crate) const fn new(process_id: ProcessId, caller_credential: CallerCredential) -> Self { Self { - identity: AssociationIdentity::new(core_id, session_id, process_id), + process_id, caller_credential, } } - pub(crate) const fn identity(&self) -> AssociationIdentity { - self.identity + pub(crate) const fn process_id(&self) -> ProcessId { + self.process_id } /// Returns the broker-entry-authenticated caller credential for this association. @@ -120,14 +65,7 @@ impl BrokerCore { caller_credential: CallerCredential, ) -> Result { let process_id = allocate_id(&mut self.next_process_id)?; - // The POC models one sandbox session per BrokerCore; multi-session - // allocation belongs with the future deployment/session manager. - let association = BrokerAssociation::new( - self.core_id, - SessionId::FIRST, - ProcessId::new(process_id), - caller_credential, - ); + let association = BrokerAssociation::new(ProcessId::new(process_id), caller_credential); Ok(association) } } @@ -135,11 +73,11 @@ impl BrokerCore { #[cfg(test)] mod tests { use super::*; - use crate::PolicyEngine; + use crate::{PolicyEngine, test_support::TestBrokerCore}; #[test] fn create_association_assigns_distinct_identities_and_preserves_credential() { - let mut core = BrokerCore::new(PolicyEngine::default_deny()); + let mut core = TestBrokerCore::new(PolicyEngine::default_deny()); let first = core .create_association(CallerCredential::Unauthenticated) @@ -148,26 +86,11 @@ mod tests { .create_association(CallerCredential::Unauthenticated) .unwrap(); - assert_ne!(first.identity(), second.identity()); + assert_ne!(first.process_id(), second.process_id()); assert_eq!(first.caller_credential(), CallerCredential::Unauthenticated); assert_eq!( second.caller_credential(), CallerCredential::Unauthenticated ); } - - #[test] - fn create_association_scopes_identity_to_core_instance() { - let mut first_core = BrokerCore::new(PolicyEngine::default_deny()); - let mut second_core = BrokerCore::new(PolicyEngine::default_deny()); - - let first = first_core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let second = second_core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - assert_ne!(first.identity(), second.identity()); - } } diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 1d143b80d..2e5db53df 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -24,9 +24,9 @@ mod object; mod policy; use alloc::collections::BTreeMap; +use core::sync::atomic::{AtomicBool, Ordering}; pub use error::BrokerError; -use identity::BrokerCoreId; pub use identity::{BrokerAssociation, CallerCredential}; use litebox_broker_protocol::ObjectReferenceId; use object::{ObjectEntry, ObjectId, ObjectReference}; @@ -69,8 +69,11 @@ impl Default for BrokerCoreLimits { } /// 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 { - core_id: BrokerCoreId, policy: PolicyEngine, limits: BrokerCoreLimits, next_process_id: u64, @@ -80,16 +83,96 @@ pub struct BrokerCore { references: BTreeMap, } +static BROKER_CORE_CREATED: AtomicBool = AtomicBool::new(false); + impl BrokerCore { - /// Creates a broker core with the provided policy engine. - pub fn new(policy: PolicyEngine) -> Self { + /// Creates the broker core with the provided policy engine. + pub fn new(policy: PolicyEngine) -> Result { Self::new_with_limits(policy, BrokerCoreLimits::DEFAULT) } - /// Creates a broker core with explicit authority-state limits. - pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Self { - Self { - core_id: identity::allocate_core_id(), + /// 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(), + }) + } +} + +#[cfg(test)] +pub(crate) mod test_support { + use alloc::collections::BTreeMap; + use core::ops::{Deref, DerefMut}; + use std::sync::{Mutex, MutexGuard}; + + use crate::{BrokerCore, BrokerCoreLimits, PolicyEngine}; + + static BROKER_CORE_TEST_LOCK: Mutex<()> = Mutex::new(()); + + fn lock_broker_core() -> MutexGuard<'static, ()> { + BROKER_CORE_TEST_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + } + + /// Repeatable broker core wrapper for tests. + /// + /// This bypasses the production one-shot constructor while serializing test + /// cores in the current process. Production code must use + /// [`BrokerCore::new`] or [`BrokerCore::new_with_limits`]. + pub struct TestBrokerCore { + core: BrokerCore, + _guard: MutexGuard<'static, ()>, + } + + impl TestBrokerCore { + /// Creates a repeatable test broker core with the provided policy. + pub fn new(policy: PolicyEngine) -> Self { + let guard = lock_broker_core(); + let core = new_test_core(policy, BrokerCoreLimits::DEFAULT); + Self { + core, + _guard: guard, + } + } + + /// Creates a repeatable test broker core with explicit limits. + pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Self { + let guard = lock_broker_core(); + let core = new_test_core(policy, limits); + Self { + core, + _guard: guard, + } + } + } + + impl Deref for TestBrokerCore { + type Target = BrokerCore; + + fn deref(&self) -> &Self::Target { + &self.core + } + } + + impl DerefMut for TestBrokerCore { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.core + } + } + + fn new_test_core(policy: PolicyEngine, limits: BrokerCoreLimits) -> BrokerCore { + BrokerCore { policy, limits, next_process_id: 1, @@ -112,3 +195,24 @@ fn allocate_id(next_id: &mut u64) -> Result { *next_id = id.checked_add(1).unwrap_or(EXHAUSTED_ID); Ok(id) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn constructor_rejects_second_core_even_after_drop() { + let core = BrokerCore::new(PolicyEngine::default_deny()).unwrap(); + + assert!(matches!( + BrokerCore::new(PolicyEngine::default_deny()), + Err(BrokerError::BrokerCoreAlreadyExists) + )); + + drop(core); + assert!(matches!( + BrokerCore::new(PolicyEngine::default_deny()), + Err(BrokerError::BrokerCoreAlreadyExists) + )); + } +} diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index f7d0f7b1f..d91d190ef 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -4,7 +4,7 @@ use core::ops::BitOr; use crate::event::EventObject; -use crate::identity::{AssociationIdentity, BrokerAssociation}; +use crate::identity::{BrokerAssociation, ProcessId}; use crate::{BrokerCore, BrokerError, PolicyDecision, PolicyOperation, Result, allocate_id}; use litebox_broker_protocol::{ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId}; @@ -72,7 +72,7 @@ const FIRST_REFERENCE_GENERATION: ObjectReferenceGeneration = ObjectReferenceGen pub(crate) struct ObjectReference { pub(crate) object_id: ObjectId, pub(crate) reference_generation: ObjectReferenceGeneration, - pub(crate) owner: AssociationIdentity, + pub(crate) owner: ProcessId, pub(crate) object_type: ObjectType, pub(crate) rights: ObjectRights, } @@ -125,7 +125,7 @@ impl BrokerCore { ObjectReference { object_id, reference_generation, - owner: association.identity(), + owner: association.process_id(), object_type, rights, }, @@ -239,9 +239,9 @@ impl BrokerCore { /// Closes a broker association and releases references owned by it. pub fn close_association(&mut self, association: BrokerAssociation) { - let identity = association.identity(); + let process_id = association.process_id(); self.references - .retain(|_, reference| reference.owner != identity); + .retain(|_, reference| reference.owner != process_id); let references = &self.references; self.objects.retain(|object_id, _| { references @@ -259,7 +259,7 @@ impl BrokerCore { .references .get(&handle.reference_id) .ok_or(BrokerError::UnknownObject)?; - if reference.owner != association.identity() { + if reference.owner != association.process_id() { return Err(BrokerError::UnknownObject); } if reference.reference_generation != handle.reference_generation { @@ -288,11 +288,13 @@ pub(crate) struct AuthorizedObject { #[cfg(test)] mod tests { use super::*; - use crate::{BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine}; + use crate::{ + BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, test_support::TestBrokerCore, + }; #[test] fn object_and_reference_allocators_issue_max_id_then_exhaust() { - let mut core = BrokerCore::new(PolicyEngine::default_deny()); + let mut core = TestBrokerCore::new(PolicyEngine::default_deny()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -324,32 +326,40 @@ mod tests { #[test] fn insert_object_with_reference_enforces_object_and_reference_limits() { - let mut object_limited = - BrokerCore::new_with_limits(PolicyEngine::event_only(), BrokerCoreLimits::new(0, 1)); - let association = object_limited - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - assert_eq!( - object_limited.create_event(&association), - Err(BrokerError::ResourceExhausted) - ); - - let mut reference_limited = - BrokerCore::new_with_limits(PolicyEngine::event_only(), BrokerCoreLimits::new(1, 0)); - let association = reference_limited - .create_association(CallerCredential::Unauthenticated) - .unwrap(); + { + let mut object_limited = TestBrokerCore::new_with_limits( + PolicyEngine::event_only(), + BrokerCoreLimits::new(0, 1), + ); + let association = object_limited + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + assert_eq!( + object_limited.create_event(&association), + Err(BrokerError::ResourceExhausted) + ); + } - assert_eq!( - reference_limited.create_event(&association), - Err(BrokerError::ResourceExhausted) - ); + { + let mut reference_limited = TestBrokerCore::new_with_limits( + PolicyEngine::event_only(), + BrokerCoreLimits::new(1, 0), + ); + let association = reference_limited + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + + assert_eq!( + reference_limited.create_event(&association), + Err(BrokerError::ResourceExhausted) + ); + } } #[test] fn close_association_releases_owned_references_and_orphaned_objects() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + let mut core = TestBrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -364,30 +374,9 @@ mod tests { assert!(core.objects.is_empty()); } - #[test] - fn foreign_core_association_cannot_authorize_matching_handle_values() { - let mut owner_core = BrokerCore::new(PolicyEngine::event_only()); - let owner = owner_core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = owner_core.create_event(&owner).unwrap(); - - let mut other_core = BrokerCore::new(PolicyEngine::event_only()); - let other = other_core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let other_handle = other_core.create_event(&other).unwrap(); - assert_eq!(handle, other_handle); - - assert_eq!( - other_core.wait_event(&owner, handle), - Err(BrokerError::UnknownObject) - ); - } - #[test] fn close_object_reference_releases_reference_and_orphaned_object() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + let mut core = TestBrokerCore::new(PolicyEngine::event_only()); let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); @@ -404,7 +393,7 @@ mod tests { #[test] fn close_object_reference_rejects_stale_and_foreign_handles() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + let mut core = TestBrokerCore::new(PolicyEngine::event_only()); let owner = core .create_association(CallerCredential::Unauthenticated) .unwrap(); diff --git a/litebox_broker_host/src/server.rs b/litebox_broker_host/src/server.rs index e66264a5f..92cabacac 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/server.rs @@ -318,33 +318,50 @@ mod tests { use litebox_broker_protocol::CreateEventRequest; #[test] - fn dispatch_enforces_negotiation_state() { - let (mut core, association, mut state) = new_association(); - - let dispatch = handle_request(&mut core, &association, &mut state, event_create_request(0)); - assert_protocol_violation(dispatch); - assert_eq!(state, ConnectionState::AwaitingNegotiation); + fn server_request_handling_uses_one_broker_core() { + let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); + + dispatch_enforces_negotiation_state(&mut core); + dispatch_rejects_unsupported_protocol_version_without_activation(&mut core); + dispatch_handles_unknown_wire_requests_by_state(&mut core); + dispatch_negotiates_then_routes_event_create(&mut core); + 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 dispatch_enforces_negotiation_state(core: &mut BrokerCore) { + { + let (association, mut state) = new_association(core); + + let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); + assert_protocol_violation(dispatch); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + core.close_association(association); + } - let (mut core, association, mut state) = new_association(); - negotiate(&mut core, &association, &mut state); + { + let (association, mut state) = new_association(core); + negotiate(core, &association, &mut state); - let dispatch = handle_request( - &mut core, - &association, - &mut state, - BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, - }, - ); - assert_protocol_violation(dispatch); + let dispatch = handle_request( + core, + &association, + &mut state, + BrokerRequest::Negotiate { + protocol_version: SUPPORTED_PROTOCOL_VERSION, + }, + ); + assert_protocol_violation(dispatch); + core.close_association(association); + } } - #[test] - fn dispatch_rejects_unsupported_protocol_version_without_activation() { - let (mut core, association, mut state) = new_association(); + fn dispatch_rejects_unsupported_protocol_version_without_activation(core: &mut BrokerCore) { + let (association, mut state) = new_association(core); let dispatch = handle_request( - &mut core, + core, &association, &mut state, BrokerRequest::Negotiate { @@ -360,53 +377,59 @@ mod tests { ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); assert_eq!(state, ConnectionState::AwaitingNegotiation); + core.close_association(association); } - #[test] - fn dispatch_handles_unknown_wire_requests_by_state() { - let (mut core, association, mut state) = new_association(); + fn dispatch_handles_unknown_wire_requests_by_state(core: &mut BrokerCore) { + { + let (association, mut state) = new_association(core); - let dispatch = handle_received_request( - &mut core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - assert_protocol_violation(dispatch); - - let (mut core, association, mut state) = new_association(); - negotiate(&mut core, &association, &mut state); - - let dispatch = handle_received_request( - &mut core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); + let dispatch = handle_received_request( + core, + &association, + &mut state, + ReceivedBrokerRequest::Unknown, + ); + assert_protocol_violation(dispatch); + core.close_association(association); + } - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::UnsupportedOperation) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + { + let (association, mut state) = new_association(core); + negotiate(core, &association, &mut state); + + let dispatch = handle_received_request( + core, + &association, + &mut state, + ReceivedBrokerRequest::Unknown, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::UnsupportedOperation) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + core.close_association(association); + } } - #[test] - fn dispatch_negotiates_then_routes_event_create() { - let (mut core, association, mut state) = new_association(); - negotiate(&mut core, &association, &mut state); + fn dispatch_negotiates_then_routes_event_create(core: &mut BrokerCore) { + let (association, mut state) = new_association(core); + negotiate(core, &association, &mut state); - let dispatch = handle_request(&mut core, &association, &mut state, event_create_request(0)); + let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); match dispatch.response { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(_))) => {} response => panic!("unexpected response: {response:?}"), } + core.close_association(association); } - #[test] - fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( + core: &mut BrokerCore, + ) { let mut channel = FakeServerChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { @@ -420,7 +443,7 @@ mod tests { ])); assert_eq!( - serve_connection(&mut core, &mut channel).unwrap(), + serve_connection(core, &mut channel).unwrap(), ConnectionTermination::PeerClosed ); assert_eq!( @@ -435,12 +458,10 @@ mod tests { } response => panic!("unexpected response: {response:?}"), }; - assert_eq!(handle.reference_id.get(), 1); + assert_ne!(handle.reference_id.get(), 0); } - #[test] - fn serve_connection_closes_after_protocol_violation() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { let mut channel = FakeServerChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request(event_create_request( 0, @@ -453,7 +474,7 @@ mod tests { ])); assert_eq!( - serve_connection(&mut core, &mut channel).unwrap(), + serve_connection(core, &mut channel).unwrap(), ConnectionTermination::BrokerClosed(CloseReason::ProtocolViolation) ); assert_eq!( @@ -463,9 +484,7 @@ mod tests { assert_eq!(channel.requests.len(), 1); } - #[test] - fn serve_connection_returns_channel_error_when_response_send_fails() { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + fn serve_connection_returns_channel_error_when_response_send_fails(core: &mut BrokerCore) { let mut channel = FakeServerChannel::new(std::vec::Vec::from([Ok(Some( ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { protocol_version: SUPPORTED_PROTOCOL_VERSION, @@ -473,7 +492,7 @@ mod tests { ))])); channel.send_error = Some(FakeChannelError::Send); - match serve_connection(&mut core, &mut channel) { + match serve_connection(core, &mut channel) { Err(BrokerServeError::Channel(FakeChannelError::Send)) => {} result => panic!("unexpected serve result: {result:?}"), } @@ -491,12 +510,11 @@ mod tests { ); } - fn new_association() -> (BrokerCore, BrokerAssociation, ConnectionState) { - let mut core = BrokerCore::new(PolicyEngine::event_only()); + fn new_association(core: &mut BrokerCore) -> (BrokerAssociation, ConnectionState) { let association = core .create_association(CallerCredential::Unauthenticated) .unwrap(); - (core, association, ConnectionState::AwaitingNegotiation) + (association, ConnectionState::AwaitingNegotiation) } fn negotiate( diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 57aa32af9..bd11bd19a 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -22,7 +22,7 @@ fn main() -> io::Result<()> { let (stream, _) = listener.accept()?; let mut channel = UnixStreamServerControlChannel::from_accepted(stream); channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; - let mut broker = BrokerCore::new(PolicyEngine::event_only()); + let mut broker = BrokerCore::new(PolicyEngine::event_only()).map_err(io::Error::other)?; serve_connection(&mut broker, &mut channel) .map(|_| ()) .map_err(broker_error) diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 638c5b474..9d909a770 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -289,7 +289,11 @@ impl Drop for TestBroker { } #[cfg(all(target_arch = "x86_64", target_os = "linux"))] -fn spawn_test_broker(socket_path: &Path, policy: litebox_broker_core::PolicyEngine) -> TestBroker { +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(); @@ -300,26 +304,29 @@ fn spawn_test_broker(socket_path: &Path, policy: litebox_broker_core::PolicyEngi 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"); - let (stream, _) = listener - .accept() - .expect("failed to accept broker control client"); - 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 = - litebox_broker_transport::unix_socket::UnixStreamServerControlChannel::from_accepted(stream); - let mut core = litebox_broker_core::BrokerCore::new(policy); - let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) - .expect("broker server failed"); - assert_eq!( - termination, - litebox_broker_host::ConnectionTermination::PeerClosed - ); + for _ in 0..connection_count { + let (stream, _) = listener + .accept() + .expect("failed to accept broker control client"); + 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 = + litebox_broker_transport::unix_socket::UnixStreamServerControlChannel::from_accepted(stream); + let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) + .expect("broker server failed"); + assert_eq!( + termination, + litebox_broker_host::ConnectionTermination::PeerClosed + ); + } })); let _ = std::fs::remove_file(&server_socket_path); let _ = done_tx.send(()); @@ -340,29 +347,19 @@ fn spawn_test_broker(socket_path: &Path, policy: litebox_broker_core::PolicyEngi #[cfg(all(target_arch = "x86_64", target_os = "linux"))] #[test] -fn test_runner_connects_to_broker() { +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::default_deny(), + litebox_broker_core::PolicyEngine::event_only(), + 2, ); - let true_path = run_which("true"); Runner::new(&true_path, "broker_true_rewriter") .broker_socket(&socket_path) .run(); - broker_thread.join(); -} - -#[cfg(all(target_arch = "x86_64", target_os = "linux"))] -#[test] -fn test_broker_backed_eventfd_with_rewriter() { - let socket_path = unique_test_socket_path("broker-eventfd"); - let broker_thread = spawn_test_broker( - &socket_path, - litebox_broker_core::PolicyEngine::event_only(), - ); - let target = common::compile("./tests/eventfd.c", "broker_eventfd_rewriter", false, false); Runner::new(&target, "broker_eventfd_rewriter") .broker_socket(&socket_path) From 02f7d709609d923d8892204917c77cb2fc42d28e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 16:29:47 -0700 Subject: [PATCH 47/66] Update eventfd fcntl test expectation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/tests.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 66d031449..4d878ba94 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -92,9 +92,11 @@ fn test_fcntl() { .expect("Failed to create eventfd"); let eventfd = i32::try_from(eventfd).unwrap(); check(eventfd, OFlags::RDWR, OFlags::RDWR); + task.sys_fcntl(eventfd, FcntlArg::SETFL(OFlags::RDWR | OFlags::NONBLOCK)) + .unwrap(); assert_eq!( - task.sys_fcntl(eventfd, FcntlArg::SETFL(OFlags::RDWR | OFlags::NONBLOCK)), - Err(Errno::EINVAL) + task.sys_fcntl(eventfd, FcntlArg::GETFL).unwrap(), + (OFlags::RDWR | OFlags::NONBLOCK).bits() ); // Test fcntl with DUPFD From 5d1cdf75fd35e525a6cb01723f7a127e469ae1ce Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 19:50:59 -0700 Subject: [PATCH 48/66] Restore eventfd fcntl test coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/tests.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index 4d878ba94..81f4e07ed 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -88,16 +88,13 @@ fn test_fcntl() { // Test eventfd let eventfd = task - .sys_eventfd2(0, EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE) + .sys_eventfd2( + 0, + EfdFlags::CLOEXEC | EfdFlags::SEMAPHORE | EfdFlags::NONBLOCK, + ) .expect("Failed to create eventfd"); let eventfd = i32::try_from(eventfd).unwrap(); - check(eventfd, OFlags::RDWR, OFlags::RDWR); - task.sys_fcntl(eventfd, FcntlArg::SETFL(OFlags::RDWR | OFlags::NONBLOCK)) - .unwrap(); - assert_eq!( - task.sys_fcntl(eventfd, FcntlArg::GETFL).unwrap(), - (OFlags::RDWR | OFlags::NONBLOCK).bits() - ); + check(eventfd, OFlags::RDWR | OFlags::NONBLOCK, OFlags::RDWR); // Test fcntl with DUPFD let fd = task From d70248900a823ada6cd804d4d07adfba9700d6b3 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 19:56:40 -0700 Subject: [PATCH 49/66] Avoid extra broker core test global Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_core/src/lib.rs | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 2e5db53df..7e88bf906 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -113,47 +113,28 @@ impl BrokerCore { pub(crate) mod test_support { use alloc::collections::BTreeMap; use core::ops::{Deref, DerefMut}; - use std::sync::{Mutex, MutexGuard}; use crate::{BrokerCore, BrokerCoreLimits, PolicyEngine}; - static BROKER_CORE_TEST_LOCK: Mutex<()> = Mutex::new(()); - - fn lock_broker_core() -> MutexGuard<'static, ()> { - BROKER_CORE_TEST_LOCK - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner) - } - /// Repeatable broker core wrapper for tests. /// - /// This bypasses the production one-shot constructor while serializing test - /// cores in the current process. Production code must use - /// [`BrokerCore::new`] or [`BrokerCore::new_with_limits`]. + /// This bypasses the production one-shot constructor. Production code must + /// use [`BrokerCore::new`] or [`BrokerCore::new_with_limits`]. pub struct TestBrokerCore { core: BrokerCore, - _guard: MutexGuard<'static, ()>, } impl TestBrokerCore { /// Creates a repeatable test broker core with the provided policy. pub fn new(policy: PolicyEngine) -> Self { - let guard = lock_broker_core(); let core = new_test_core(policy, BrokerCoreLimits::DEFAULT); - Self { - core, - _guard: guard, - } + Self { core } } /// Creates a repeatable test broker core with explicit limits. pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Self { - let guard = lock_broker_core(); let core = new_test_core(policy, limits); - Self { - core, - _guard: guard, - } + Self { core } } } From 6f310feb2b4261d52e37af9fbd2d81d16a310fad Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 20:23:57 -0700 Subject: [PATCH 50/66] Prune redundant broker tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 195 ---------------------------- litebox_broker_core/src/event.rs | 81 +----------- litebox_broker_core/src/identity.rs | 25 ---- litebox_broker_core/src/lib.rs | 77 ----------- litebox_broker_core/src/object.rs | 128 +++++------------- 5 files changed, 36 insertions(+), 470 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 62b1d8448..3d522f417 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -185,198 +185,3 @@ fn event_response_from_core(response: CoreResponse) -> Result Err(BrokerObjectError::UnexpectedResponse), } } - -#[cfg(test)] -mod tests { - extern crate std; - - use alloc::sync::Arc; - use core::sync::atomic::{AtomicUsize, Ordering}; - use std::{collections::VecDeque, sync::Mutex}; - - use litebox_broker_protocol::{ - AddEventResponse, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventResponse, - EventResponse, ObjectHandle, ObjectReferenceGeneration, ObjectReferenceId, ReadinessState, - WaitEventResponse, - }; - - use super::*; - use crate::{ - BrokerControlError, - event::{Events, IOPollable, observer::Observer, wait::WaitState}, - platform::mock::MockPlatform, - }; - - struct MockBrokerControl { - responses: Mutex>, - } - - impl MockBrokerControl { - fn new(responses: impl IntoIterator) -> Self { - Self { - responses: Mutex::new(responses.into_iter().collect()), - } - } - } - - impl BrokerControl for MockBrokerControl { - fn request( - &self, - _request: CoreRequest, - ) -> core::result::Result { - self.responses - .lock() - .unwrap() - .pop_front() - .ok_or(BrokerControlError::UnexpectedResponse) - } - } - - struct CountingObserver { - notifications: AtomicUsize, - } - - impl CountingObserver { - fn new() -> Self { - Self { - notifications: AtomicUsize::new(0), - } - } - - fn notifications(&self) -> usize { - self.notifications.load(Ordering::Relaxed) - } - } - - impl Observer for CountingObserver { - fn on_events(&self, _events: &Events) { - self.notifications.fetch_add(1, Ordering::Relaxed); - } - } - - fn sample_handle() -> ObjectHandle { - ObjectHandle::new(ObjectReferenceId::new(1), ObjectReferenceGeneration::new(1)) - } - - fn create_response() -> CoreResponse { - CoreResponse::Event(EventResponse::Create(CreateEventResponse::new( - sample_handle(), - ))) - } - - fn add_response(read_ready: bool, write_ready: bool) -> CoreResponse { - CoreResponse::Event(EventResponse::Add(AddEventResponse::new( - ReadinessState::new(read_ready, write_ready, 1), - ))) - } - - fn consume_response(value: u64, read_ready: bool, write_ready: bool) -> CoreResponse { - CoreResponse::Event(EventResponse::Consume(ConsumeEventResponse::new( - value, - ReadinessState::new(read_ready, write_ready, 1), - ))) - } - - fn observer() -> (Arc, Arc>) { - let observer = Arc::new(CountingObserver::new()); - let observer_dyn: Arc> = observer.clone(); - (observer, observer_dyn) - } - - fn litebox(responses: impl IntoIterator) -> LiteBox { - LiteBox::new_with_broker_control( - MockPlatform::new(), - Arc::new(MockBrokerControl::new(responses)), - ) - } - - #[test] - fn create_maps_unexpected_response_shape() { - let litebox = litebox([add_response(false, true)]); - let result = EventCounter::::new(&litebox, 0); - - assert!(matches!(result, Err(EventCounterError::UnexpectedResponse))); - } - - #[test] - fn cached_broker_object_errors_map_to_io() { - assert_eq!( - EventCounterError::from(BrokerObjectError::InvalidObject), - EventCounterError::Io - ); - } - - #[test] - fn zero_write_does_not_notify_read_observers_even_when_broker_reports_ready() { - let litebox = litebox([create_response(), add_response(true, true)]); - let event = EventCounter::new(&litebox, 0).unwrap(); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!(event.write(&wait.context(), true, 0).unwrap(), 8); - - assert_eq!(observer.notifications(), 0); - } - - #[test] - fn nonzero_write_notifies_read_observers_when_broker_reports_ready() { - let litebox = litebox([create_response(), add_response(true, true)]); - let event = EventCounter::new(&litebox, 0).unwrap(); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::IN); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!(event.write(&wait.context(), true, 1).unwrap(), 8); - - assert_eq!(observer.notifications(), 1); - } - - #[test] - fn read_notifies_write_observers_only_when_broker_reports_write_ready() { - let litebox = litebox([ - create_response(), - consume_response(1, false, false), - consume_response(1, false, true), - ]); - let event = EventCounter::new(&litebox, 0).unwrap(); - let (observer, observer_dyn) = observer(); - event.register_observer(Arc::downgrade(&observer_dyn), Events::OUT); - - let wait = WaitState::new(MockPlatform::new()); - assert_eq!( - event - .read(&wait.context(), true, EventCounterReadMode::All) - .unwrap(), - 1 - ); - - assert_eq!(observer.notifications(), 0); - - assert_eq!( - event - .read(&wait.context(), true, EventCounterReadMode::All) - .unwrap(), - 1 - ); - - assert_eq!(observer.notifications(), 1); - } - - #[test] - fn poll_uses_broker_read_and_write_readiness() { - let litebox = litebox([ - create_response(), - CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::Ready(ReadinessState::new(true, false, 1)), - ))), - CoreResponse::Event(EventResponse::Wait(WaitEventResponse::new( - WaitOutcome::WouldBlock(ReadinessState::new(false, true, 2)), - ))), - ]); - let event = EventCounter::new(&litebox, 0).unwrap(); - - assert_eq!(event.check_io_events(), Events::IN); - assert_eq!(event.check_io_events(), Events::OUT); - } -} diff --git a/litebox_broker_core/src/event.rs b/litebox_broker_core/src/event.rs index 0f0d300d9..523489cea 100644 --- a/litebox_broker_core/src/event.rs +++ b/litebox_broker_core/src/event.rs @@ -167,90 +167,21 @@ impl EventObject { #[cfg(test)] mod tests { use super::*; - use crate::{CallerCredential, PolicyEngine, test_support::TestBrokerCore}; - use litebox_broker_protocol::ObjectReferenceGeneration; #[test] - fn wait_rejects_invalid_references_with_expected_errors() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only()); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core - .insert_object_with_reference( - &association, - ObjectKind::Event(EventObject::new(0)), - ObjectType::Event, - ObjectRights::WRITE, - ) - .unwrap(); - - assert_eq!( - core.wait_event(&association, handle), - Err(BrokerError::InvalidRights) - ); - - let mut handle = core.create_event(&association).unwrap(); - handle.reference_generation = - ObjectReferenceGeneration::new(handle.reference_generation.get() + 1); + fn event_readiness_state_only_reports_authorized_directions() { + let readiness = ReadinessState::new(true, true, 7); assert_eq!( - core.wait_event(&association, handle), - Err(BrokerError::StaleHandle) + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WAIT), + ReadinessState::new(true, false, 7) ); - - let other = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core.create_event(&association).unwrap(); - assert_eq!( - core.wait_event(&other, handle), - Err(BrokerError::UnknownObject) + BrokerCore::filter_readiness_for_rights(readiness, ObjectRights::WRITE), + ReadinessState::new(false, true, 7) ); } - #[test] - fn create_event_uses_policy_granted_reference_rights() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only_with_reference_rights( - ObjectRights::WAIT, - )); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - let handle = core.create_event(&association).unwrap(); - - assert!(matches!( - core.wait_event(&association, handle), - Ok(WaitOutcome::WouldBlock(_)) - )); - assert_eq!( - core.add_event(&association, handle, 1), - Err(BrokerError::InvalidRights) - ); - } - - #[test] - fn event_readiness_state_only_reports_authorized_directions() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only_with_reference_rights( - ObjectRights::WAIT, - )); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core.create_event_with_count(&association, 1).unwrap(); - - assert!(matches!( - core.wait_event(&association, handle), - Ok(WaitOutcome::Ready(ReadinessState { - read_ready: true, - write_ready: false, - .. - })) - )); - } - #[test] fn add_event_does_not_mutate_count_when_generation_is_exhausted() { let mut event = EventObject { diff --git a/litebox_broker_core/src/identity.rs b/litebox_broker_core/src/identity.rs index 335bc945f..5b26f26d0 100644 --- a/litebox_broker_core/src/identity.rs +++ b/litebox_broker_core/src/identity.rs @@ -69,28 +69,3 @@ impl BrokerCore { Ok(association) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::{PolicyEngine, test_support::TestBrokerCore}; - - #[test] - fn create_association_assigns_distinct_identities_and_preserves_credential() { - let mut core = TestBrokerCore::new(PolicyEngine::default_deny()); - - let first = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let second = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - assert_ne!(first.process_id(), second.process_id()); - assert_eq!(first.caller_credential(), CallerCredential::Unauthenticated); - assert_eq!( - second.caller_credential(), - CallerCredential::Unauthenticated - ); - } -} diff --git a/litebox_broker_core/src/lib.rs b/litebox_broker_core/src/lib.rs index 7e88bf906..e1c54f5d5 100644 --- a/litebox_broker_core/src/lib.rs +++ b/litebox_broker_core/src/lib.rs @@ -109,62 +109,6 @@ impl BrokerCore { } } -#[cfg(test)] -pub(crate) mod test_support { - use alloc::collections::BTreeMap; - use core::ops::{Deref, DerefMut}; - - use crate::{BrokerCore, BrokerCoreLimits, PolicyEngine}; - - /// Repeatable broker core wrapper for tests. - /// - /// This bypasses the production one-shot constructor. Production code must - /// use [`BrokerCore::new`] or [`BrokerCore::new_with_limits`]. - pub struct TestBrokerCore { - core: BrokerCore, - } - - impl TestBrokerCore { - /// Creates a repeatable test broker core with the provided policy. - pub fn new(policy: PolicyEngine) -> Self { - let core = new_test_core(policy, BrokerCoreLimits::DEFAULT); - Self { core } - } - - /// Creates a repeatable test broker core with explicit limits. - pub fn new_with_limits(policy: PolicyEngine, limits: BrokerCoreLimits) -> Self { - let core = new_test_core(policy, limits); - Self { core } - } - } - - impl Deref for TestBrokerCore { - type Target = BrokerCore; - - fn deref(&self) -> &Self::Target { - &self.core - } - } - - impl DerefMut for TestBrokerCore { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.core - } - } - - fn new_test_core(policy: PolicyEngine, limits: BrokerCoreLimits) -> BrokerCore { - BrokerCore { - 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 { @@ -176,24 +120,3 @@ fn allocate_id(next_id: &mut u64) -> Result { *next_id = id.checked_add(1).unwrap_or(EXHAUSTED_ID); Ok(id) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn constructor_rejects_second_core_even_after_drop() { - let core = BrokerCore::new(PolicyEngine::default_deny()).unwrap(); - - assert!(matches!( - BrokerCore::new(PolicyEngine::default_deny()), - Err(BrokerError::BrokerCoreAlreadyExists) - )); - - drop(core); - assert!(matches!( - BrokerCore::new(PolicyEngine::default_deny()), - Err(BrokerError::BrokerCoreAlreadyExists) - )); - } -} diff --git a/litebox_broker_core/src/object.rs b/litebox_broker_core/src/object.rs index d91d190ef..06cca10ff 100644 --- a/litebox_broker_core/src/object.rs +++ b/litebox_broker_core/src/object.rs @@ -288,112 +288,24 @@ pub(crate) struct AuthorizedObject { #[cfg(test)] mod tests { use super::*; - use crate::{ - BrokerCoreLimits, BrokerError, CallerCredential, PolicyEngine, test_support::TestBrokerCore, - }; + use crate::{BrokerError, CallerCredential, PolicyEngine}; + use litebox_broker_protocol::WaitOutcome; #[test] - fn object_and_reference_allocators_issue_max_id_then_exhaust() { - let mut core = TestBrokerCore::new(PolicyEngine::default_deny()); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - core.next_object_id = u64::MAX; - core.next_reference_id = u64::MAX; - - let handle = core - .insert_object_with_reference( - &association, - ObjectKind::Event(EventObject::new(0)), - ObjectType::Event, - ObjectRights::WAIT, - ) - .unwrap(); + fn allocator_issues_max_id_then_exhausts() { + let mut next_id = u64::MAX; - assert_eq!(handle.reference_id, ObjectReferenceId::new(u64::MAX)); - assert_eq!(core.next_object_id, 0); - assert_eq!(core.next_reference_id, 0); + assert_eq!(allocate_id(&mut next_id), Ok(u64::MAX)); + assert_eq!(next_id, 0); assert_eq!( - core.insert_object_with_reference( - &association, - ObjectKind::Event(EventObject::new(0)), - ObjectType::Event, - ObjectRights::WAIT, - ), + allocate_id(&mut next_id), Err(BrokerError::ResourceExhausted) ); } #[test] - fn insert_object_with_reference_enforces_object_and_reference_limits() { - { - let mut object_limited = TestBrokerCore::new_with_limits( - PolicyEngine::event_only(), - BrokerCoreLimits::new(0, 1), - ); - let association = object_limited - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - assert_eq!( - object_limited.create_event(&association), - Err(BrokerError::ResourceExhausted) - ); - } - - { - let mut reference_limited = TestBrokerCore::new_with_limits( - PolicyEngine::event_only(), - BrokerCoreLimits::new(1, 0), - ); - let association = reference_limited - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - - assert_eq!( - reference_limited.create_event(&association), - Err(BrokerError::ResourceExhausted) - ); - } - } - - #[test] - fn close_association_releases_owned_references_and_orphaned_objects() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only()); - 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()); - } - - #[test] - fn close_object_reference_releases_reference_and_orphaned_object() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only()); - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - let handle = core.create_event(&association).unwrap(); - - assert_eq!(core.close_object_reference(&association, handle), Ok(())); - assert!(core.references.is_empty()); - assert!(core.objects.is_empty()); - assert_eq!( - core.close_object_reference(&association, handle), - Err(BrokerError::UnknownObject) - ); - } - - #[test] - fn close_object_reference_rejects_stale_and_foreign_handles() { - let mut core = TestBrokerCore::new(PolicyEngine::event_only()); + 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(); @@ -417,7 +329,27 @@ mod tests { ); assert!(matches!( core.wait_event(&owner, handle), - Ok(litebox_broker_protocol::WaitOutcome::WouldBlock(_)) + 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()); } } From 6a520d10eba35b83c093a461be0e70d71754d482 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 20:46:29 -0700 Subject: [PATCH 51/66] Rename broker local and host errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_host/src/error.rs | 38 +++++++++++++++ litebox_broker_host/src/lib.rs | 5 +- litebox_broker_host/src/server.rs | 67 ++++++--------------------- litebox_broker_local/src/error.rs | 45 ++++++++++-------- litebox_broker_local/src/event.rs | 12 ++--- litebox_broker_local/src/lib.rs | 28 +++++------ litebox_broker_local/src/negotiate.rs | 14 +++--- litebox_broker_local/src/worker.rs | 8 ++-- litebox_broker_userland/src/main.rs | 8 ++-- 9 files changed, 114 insertions(+), 111 deletions(-) create mode 100644 litebox_broker_host/src/error.rs 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 index 3d7adfdd6..bea8a1141 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -12,9 +12,10 @@ #[cfg(test)] extern crate std; +mod error; mod server; +pub use error::{BrokerHostError, Result}; pub use server::{ - BrokerServeError, CloseReason, ConnectionTermination, SUPPORTED_PROTOCOL_VERSION, - serve_connection, + CloseReason, ConnectionTermination, SUPPORTED_PROTOCOL_VERSION, serve_connection, }; diff --git a/litebox_broker_host/src/server.rs b/litebox_broker_host/src/server.rs index 92cabacac..893bcbd04 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/server.rs @@ -10,6 +10,8 @@ use litebox_broker_protocol::{ ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, }; +use crate::error::{BrokerHostError, Result as HostResult}; + /// Protocol version this broker server implementation supports. pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); @@ -17,18 +19,18 @@ pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, pub fn serve_connection( core: &mut BrokerCore, channel: &mut T, -) -> Result> +) -> HostResult where T: ServerControlChannel, { let peer_credential = channel .peer_credential() - .map_err(BrokerServeError::Channel)?; + .map_err(BrokerHostError::Channel)?; let caller_credential = caller_credential_from_peer(peer_credential) - .map_err(|()| BrokerServeError::AssociationSetup)?; + .map_err(|()| BrokerHostError::AssociationSetup)?; let association = core .create_association(caller_credential) - .map_err(|_error| BrokerServeError::AssociationSetup)?; + .map_err(|_error| BrokerHostError::AssociationSetup)?; let result = serve_request_loop(core, channel, &association); core.close_association(association); @@ -39,20 +41,20 @@ fn serve_request_loop( core: &mut BrokerCore, channel: &mut T, association: &BrokerAssociation, -) -> Result> +) -> HostResult where T: ServerControlChannel, { let mut state = ConnectionState::AwaitingNegotiation; loop { - let Some(received) = channel.recv_request().map_err(BrokerServeError::Channel)? else { + 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(BrokerServeError::Channel)?; + .map_err(BrokerHostError::Channel)?; if let DispatchOutcome::Close(reason) = dispatch.outcome { return Ok(ConnectionTermination::BrokerClosed(reason)); } @@ -61,7 +63,9 @@ where Ok(ConnectionTermination::PeerClosed) } -fn caller_credential_from_peer(peer_credential: PeerCredential) -> Result { +fn caller_credential_from_peer( + peer_credential: PeerCredential, +) -> core::result::Result { if peer_credential == PeerCredential::Unauthenticated { Ok(CallerCredential::Unauthenticated) } else { @@ -280,37 +284,6 @@ pub enum ConnectionTermination { BrokerClosed(CloseReason), } -/// Errors returned by a broker receive/send loop. -#[derive(Debug)] -#[non_exhaustive] -pub enum BrokerServeError { - /// The server could not authenticate the peer or allocate broker association state. - AssociationSetup, - /// The concrete channel failed. - Channel(E), -} - -impl fmt::Display for BrokerServeError { - 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 BrokerServeError -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), - } - } -} - #[cfg(test)] mod tests { use super::*; @@ -324,7 +297,6 @@ mod tests { dispatch_enforces_negotiation_state(&mut core); dispatch_rejects_unsupported_protocol_version_without_activation(&mut core); dispatch_handles_unknown_wire_requests_by_state(&mut core); - dispatch_negotiates_then_routes_event_create(&mut core); 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); @@ -414,19 +386,6 @@ mod tests { } } - fn dispatch_negotiates_then_routes_event_create(core: &mut BrokerCore) { - let (association, mut state) = new_association(core); - negotiate(core, &association, &mut state); - - let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - match dispatch.response { - BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(_))) => {} - response => panic!("unexpected response: {response:?}"), - } - core.close_association(association); - } - fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( core: &mut BrokerCore, ) { @@ -493,7 +452,7 @@ mod tests { channel.send_error = Some(FakeChannelError::Send); match serve_connection(core, &mut channel) { - Err(BrokerServeError::Channel(FakeChannelError::Send)) => {} + Err(BrokerHostError::Channel(FakeChannelError::Send)) => {} result => panic!("unexpected serve result: {result:?}"), } assert!(channel.responses.is_empty()); diff --git a/litebox_broker_local/src/error.rs b/litebox_broker_local/src/error.rs index 8786c3866..45696b20d 100644 --- a/litebox_broker_local/src/error.rs +++ b/litebox_broker_local/src/error.rs @@ -5,33 +5,33 @@ use core::fmt; use litebox_broker_protocol::{BrokerResponse, ErrorCode, ProtocolVersion}; -/// Errors returned by the control client adapter. +/// Errors returned by the broker-local control adapter. #[derive(Debug)] #[non_exhaustive] -pub enum ClientError { +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 client was already active. + /// 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 client does not understand. + /// 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 client. + /// Protocol version requested by this local adapter. requested: ProtocolVersion, /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, }, - /// This client cannot speak the requested protocol version. - UnsupportedClientVersion { + /// This local adapter cannot speak the requested protocol version. + UnsupportedLocalVersion { /// Protocol version requested by the caller. requested: ProtocolVersion, - /// Protocol version supported by this client implementation. - client_protocol_version: 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 { @@ -42,7 +42,7 @@ pub enum ClientError { }, /// The broker does not support the requested protocol version. UnsupportedVersion { - /// Protocol version requested by this client. + /// Protocol version requested by this local adapter. requested: ProtocolVersion, /// Protocol version advertised by the broker. broker_protocol_version: ProtocolVersion, @@ -53,12 +53,17 @@ pub enum ClientError { UnexpectedResponse(BrokerResponse), } -impl fmt::Display for ClientError { +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, "control client has not negotiated protocol version"), - Self::AlreadyNegotiated => f.write_str("control client already negotiated"), + 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 { @@ -68,12 +73,12 @@ impl fmt::Display for ClientError { f, "broker accepted incompatible protocol negotiation: requested {requested:?}, broker supports {broker_protocol_version:?}" ), - Self::UnsupportedClientVersion { + Self::UnsupportedLocalVersion { requested, - client_protocol_version, + local_protocol_version, } => write!( f, - "control client cannot request protocol version {requested:?}; client supports {client_protocol_version:?}" + "broker local adapter cannot request protocol version {requested:?}; local adapter supports {local_protocol_version:?}" ), Self::UnsupportedNegotiatedVersion { required, @@ -97,7 +102,7 @@ impl fmt::Display for ClientError { } } -impl core::error::Error for ClientError +impl core::error::Error for BrokerLocalError where E: core::error::Error + 'static, { @@ -110,7 +115,7 @@ where | Self::ChannelClosed | Self::UnknownResponse | Self::IncompatibleNegotiation { .. } - | Self::UnsupportedClientVersion { .. } + | Self::UnsupportedLocalVersion { .. } | Self::UnsupportedNegotiatedVersion { .. } | Self::UnsupportedVersion { .. } | Self::UnexpectedResponse(_) => None, @@ -118,5 +123,5 @@ where } } -/// Control client result type. -pub type Result = core::result::Result>; +/// 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 index 6ba025445..b8eecc51b 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -8,7 +8,7 @@ use litebox_broker_protocol::{ WaitOutcome, }; -use crate::{ClientError, ControlClient, Result}; +use crate::{BrokerLocalError, ControlClient, Result}; const EVENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); @@ -30,7 +30,7 @@ impl ControlClient { BrokerResponse::Core(CoreResponse::Event(EventResponse::Create(response))) => { Ok(response.handle) } - response => Err(ClientError::UnexpectedResponse(response)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), } } @@ -43,7 +43,7 @@ impl ControlClient { BrokerResponse::Core(CoreResponse::Event(EventResponse::Wait(response))) => { Ok(response.outcome) } - response => Err(ClientError::UnexpectedResponse(response)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), } } @@ -60,7 +60,7 @@ impl ControlClient { BrokerResponse::Core(CoreResponse::Event(EventResponse::Add(response))) => { Ok(response.readiness) } - response => Err(ClientError::UnexpectedResponse(response)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), } } @@ -77,7 +77,7 @@ impl ControlClient { BrokerResponse::Core(CoreResponse::Event(EventResponse::Consume(response))) => { Ok(response) } - response => Err(ClientError::UnexpectedResponse(response)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), } } } @@ -92,7 +92,7 @@ impl ControlClient { if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { Ok(()) } else { - Err(ClientError::UnsupportedNegotiatedVersion { + 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 index 3ec7be13c..7eab7b1d7 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -22,7 +22,7 @@ use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, ReceivedBrokerResponse, }; -pub use error::{ClientError, Result}; +pub use error::{BrokerLocalError, Result}; #[cfg(feature = "std")] pub use worker::{ControlClientWorker, ControlClientWorkerError}; @@ -74,7 +74,7 @@ impl ControlClient { pub(crate) fn ensure_negotiated(&self) -> Result { match self.state { - ConnectionState::AwaitingNegotiation => Err(ClientError::NotNegotiated), + ConnectionState::AwaitingNegotiation => Err(BrokerLocalError::NotNegotiated), ConnectionState::Active { negotiated_protocol_version, } => Ok(negotiated_protocol_version), @@ -83,7 +83,7 @@ impl ControlClient { pub(crate) fn request(&mut self, request: BrokerRequest) -> Result { match self.raw_request(request)? { - BrokerResponse::Error(error) => Err(ClientError::Broker(error)), + BrokerResponse::Error(error) => Err(BrokerLocalError::Broker(error)), response => Ok(response), } } @@ -91,15 +91,15 @@ impl ControlClient { fn raw_request(&mut self, request: BrokerRequest) -> Result { self.channel .send_request(&request) - .map_err(ClientError::Channel)?; + .map_err(BrokerLocalError::Channel)?; match self .channel .recv_response() - .map_err(ClientError::Channel)? - .ok_or(ClientError::ChannelClosed)? + .map_err(BrokerLocalError::Channel)? + .ok_or(BrokerLocalError::ChannelClosed)? { ReceivedBrokerResponse::Response(response) => Ok(response), - _ => Err(ClientError::UnknownResponse), + _ => Err(BrokerLocalError::UnknownResponse), } } @@ -127,7 +127,7 @@ mod tests { assert!(matches!( client.create_event(), - Err(ClientError::NotNegotiated) + Err(BrokerLocalError::NotNegotiated) )); assert_eq!(client.channel.sent_request, None); } @@ -143,7 +143,7 @@ mod tests { assert!(matches!( client.create_event(), - Err(ClientError::UnsupportedNegotiatedVersion { + Err(BrokerLocalError::UnsupportedNegotiatedVersion { required, negotiated_protocol_version }) if required == CLIENT_PROTOCOL_VERSION @@ -184,7 +184,7 @@ mod tests { assert!(matches!( client.negotiate_version(requested), - Err(ClientError::IncompatibleNegotiation { + Err(BrokerLocalError::IncompatibleNegotiation { requested: actual_requested, broker_protocol_version }) if actual_requested == requested && broker_protocol_version == broker_version @@ -205,10 +205,10 @@ mod tests { assert!(matches!( client.negotiate_version(too_new), - Err(ClientError::UnsupportedClientVersion { + Err(BrokerLocalError::UnsupportedLocalVersion { requested, - client_protocol_version - }) if requested == too_new && client_protocol_version == CLIENT_PROTOCOL_VERSION + local_protocol_version + }) if requested == too_new && local_protocol_version == CLIENT_PROTOCOL_VERSION )); assert_eq!(client.negotiated_protocol_version(), None); assert_eq!(client.channel.sent_request, None); @@ -228,7 +228,7 @@ mod tests { assert!(matches!( client.negotiate_version(requested), - Err(ClientError::UnsupportedVersion { + Err(BrokerLocalError::UnsupportedVersion { requested: actual_requested, broker_protocol_version }) if actual_requested == requested && broker_protocol_version == fallback diff --git a/litebox_broker_local/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs index 760e738e5..87cc1763f 100644 --- a/litebox_broker_local/src/negotiate.rs +++ b/litebox_broker_local/src/negotiate.rs @@ -5,7 +5,7 @@ use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, }; -use crate::{CLIENT_PROTOCOL_VERSION, ClientError, ControlClient, Result}; +use crate::{BrokerLocalError, CLIENT_PROTOCOL_VERSION, ControlClient, Result}; impl ControlClient { /// Negotiates the default client protocol version. @@ -25,12 +25,12 @@ impl ControlClient { protocol_version: ProtocolVersion, ) -> Result { if self.state != crate::ConnectionState::AwaitingNegotiation { - return Err(ClientError::AlreadyNegotiated); + return Err(BrokerLocalError::AlreadyNegotiated); } if !protocol_version.is_supported_by(CLIENT_PROTOCOL_VERSION) { - return Err(ClientError::UnsupportedClientVersion { + return Err(BrokerLocalError::UnsupportedLocalVersion { requested: protocol_version, - client_protocol_version: CLIENT_PROTOCOL_VERSION, + local_protocol_version: CLIENT_PROTOCOL_VERSION, }); } @@ -40,7 +40,7 @@ impl ControlClient { broker_protocol_version, } => { if !protocol_version.is_supported_by(broker_protocol_version) { - return Err(ClientError::IncompatibleNegotiation { + return Err(BrokerLocalError::IncompatibleNegotiation { requested: protocol_version, broker_protocol_version, }); @@ -52,11 +52,11 @@ impl ControlClient { } BrokerResponse::VersionMismatch { broker_protocol_version, - } => Err(ClientError::UnsupportedVersion { + } => Err(BrokerLocalError::UnsupportedVersion { requested: protocol_version, broker_protocol_version, }), - response => Err(ClientError::UnexpectedResponse(response)), + response => Err(BrokerLocalError::UnexpectedResponse(response)), } } } diff --git a/litebox_broker_local/src/worker.rs b/litebox_broker_local/src/worker.rs index c27168add..286e989fa 100644 --- a/litebox_broker_local/src/worker.rs +++ b/litebox_broker_local/src/worker.rs @@ -15,7 +15,7 @@ use std::{ use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ClientControlChannel}; -use crate::{ClientError, ControlClient}; +use crate::{BrokerLocalError, ControlClient}; const PHASE_IDLE: u8 = 0; const PHASE_RESERVED: u8 = 1; @@ -29,7 +29,7 @@ const DEFAULT_SHUTDOWN_WAIT: Duration = Duration::from_millis(100); #[non_exhaustive] pub enum ControlClientWorkerError { /// The wrapped control client returned an error. - Client(ClientError), + Client(BrokerLocalError), /// The worker is shutting down or has already shut down. Shutdown, } @@ -539,7 +539,7 @@ mod tests { assert!(matches!( requester.join().unwrap(), Err(ControlClientWorkerError::Shutdown - | ControlClientWorkerError::Client(ClientError::Channel(_))) + | ControlClientWorkerError::Client(BrokerLocalError::Channel(_))) )); } @@ -611,7 +611,7 @@ mod tests { assert!(*lock.lock().unwrap(), "shutdown hook was not invoked"); match requester.join().unwrap() { Err(ControlClientWorkerError::Shutdown) => {} - Err(ControlClientWorkerError::Client(ClientError::Channel(error))) + Err(ControlClientWorkerError::Client(BrokerLocalError::Channel(error))) if error.kind() == io::ErrorKind::Interrupted => {} result => panic!("unexpected requester result: {result:?}"), } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index bd11bd19a..11f115e4c 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -10,7 +10,7 @@ use std::path::{Path, PathBuf}; use std::time::{Duration, Instant}; use litebox_broker_core::{BrokerCore, PolicyEngine}; -use litebox_broker_host::{BrokerServeError, serve_connection}; +use litebox_broker_host::{BrokerHostError, serve_connection}; use litebox_broker_transport::unix_socket::UnixStreamServerControlChannel; const SESSION_TIMEOUT: Duration = Duration::from_secs(5); @@ -77,10 +77,10 @@ impl Drop for SocketPathCleanup { } } -fn broker_error(error: BrokerServeError) -> io::Error { +fn broker_error(error: BrokerHostError) -> io::Error { match error { - BrokerServeError::AssociationSetup => io::Error::other("broker association setup failed"), - BrokerServeError::Channel(error) => error, + BrokerHostError::AssociationSetup => io::Error::other("broker association setup failed"), + BrokerHostError::Channel(error) => error, error => io::Error::other(error.to_string()), } } From 4bc307717db06877d3190524c4f5d313afc673c4 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Thu, 4 Jun 2026 21:37:26 -0700 Subject: [PATCH 52/66] Remove broker local worker Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_local/Cargo.toml | 3 - litebox_broker_local/src/lib.rs | 6 +- litebox_broker_local/src/worker.rs | 701 -------------------- litebox_broker_transport/src/unix_socket.rs | 5 - litebox_platform_linux_userland/src/lib.rs | 69 ++ litebox_runner_linux_userland/Cargo.toml | 2 +- litebox_runner_linux_userland/src/broker.rs | 38 +- litebox_runner_linux_userland/src/lib.rs | 4 +- 8 files changed, 81 insertions(+), 747 deletions(-) delete mode 100644 litebox_broker_local/src/worker.rs diff --git a/litebox_broker_local/Cargo.toml b/litebox_broker_local/Cargo.toml index 142555c93..d3574f925 100644 --- a/litebox_broker_local/Cargo.toml +++ b/litebox_broker_local/Cargo.toml @@ -3,9 +3,6 @@ name = "litebox_broker_local" version = "0.1.0" edition = "2024" -[features] -std = [] - [dependencies] litebox_broker_protocol = { path = "../litebox_broker_protocol", version = "0.1.0" } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 7eab7b1d7..3892e8ec4 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -9,22 +9,18 @@ #![no_std] -#[cfg(any(feature = "std", test))] +#[cfg(test)] extern crate std; mod error; mod event; mod negotiate; -#[cfg(feature = "std")] -mod worker; use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, ReceivedBrokerResponse, }; pub use error::{BrokerLocalError, Result}; -#[cfg(feature = "std")] -pub use worker::{ControlClientWorker, ControlClientWorkerError}; /// Protocol version this client implementation requests by default. pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); diff --git a/litebox_broker_local/src/worker.rs b/litebox_broker_local/src/worker.rs deleted file mode 100644 index 286e989fa..000000000 --- a/litebox_broker_local/src/worker.rs +++ /dev/null @@ -1,701 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use core::fmt; -use std::{ - boxed::Box, - sync::mpsc::{self, Receiver, RecvTimeoutError}, - sync::{ - Arc, Condvar, Mutex, - atomic::{AtomicBool, AtomicU8, Ordering}, - }, - thread::{self, JoinHandle, Thread}, - time::Duration, -}; - -use litebox_broker_protocol::{BrokerRequest, BrokerResponse, ClientControlChannel}; - -use crate::{BrokerLocalError, ControlClient}; - -const PHASE_IDLE: u8 = 0; -const PHASE_RESERVED: u8 = 1; -const PHASE_REQUEST_READY: u8 = 2; -const PHASE_RESPONSE_READY: u8 = 3; -const PHASE_SHUTDOWN: u8 = 4; -const DEFAULT_SHUTDOWN_WAIT: Duration = Duration::from_millis(100); - -/// Error returned by [`ControlClientWorker`]. -#[derive(Debug)] -#[non_exhaustive] -pub enum ControlClientWorkerError { - /// The wrapped control client returned an error. - Client(BrokerLocalError), - /// The worker is shutting down or has already shut down. - Shutdown, -} - -impl fmt::Display for ControlClientWorkerError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Client(error) => write!(f, "control client worker request failed: {error}"), - Self::Shutdown => f.write_str("control client worker is shut down"), - } - } -} - -impl core::error::Error for ControlClientWorkerError -where - E: core::error::Error + 'static, -{ - fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { - match self { - Self::Client(error) => Some(error), - Self::Shutdown => None, - } - } -} - -type WorkerResult = core::result::Result>; -type ShutdownHook = Box; - -/// Dedicated worker for control clients that must not run channel I/O on the caller thread. -/// -/// The worker owns the [`ControlClient`] and performs blocking channel operations -/// on a dedicated thread. Callers submit one raw broker request at a time and -/// block on an in-process condition variable until the worker publishes the -/// response. This preserves the serial control-channel contract while keeping -/// deployment-specific threads, such as rewritten guest syscall threads, away -/// from host IPC syscalls. -pub struct ControlClientWorker -where - T: ClientControlChannel + Send + 'static, - T::Error: Send + 'static, -{ - state: Arc>, - worker: Mutex>>, - finished: Mutex>, -} - -struct ControlClientWorkerState { - shutdown_requested: AtomicBool, - phase: AtomicU8, - request: Mutex>, - response: Mutex>>, - requester_wait: Mutex<()>, - requester_wakeup: Condvar, - worker_thread: Mutex>, - shutdown_hook: Mutex>, -} - -impl ControlClientWorker -where - T: ClientControlChannel + Send + 'static, - T::Error: Send + 'static, -{ - /// Starts a worker for an already-negotiated control client. - /// - /// # Panics - /// - /// Panics if the worker-thread bookkeeping mutex is poisoned while the - /// worker is being started. - pub fn new(client: ControlClient) -> Self { - Self::new_with_shutdown_hook(client, || {}) - } - - /// Starts a worker and registers a hook used to interrupt blocking channel I/O during shutdown. - /// - /// # Panics - /// - /// Panics if the worker-thread bookkeeping mutex is poisoned while the - /// worker is being started. - pub fn new_with_shutdown_hook(client: ControlClient, shutdown_hook: F) -> Self - where - F: FnOnce() + Send + 'static, - { - let state = Arc::new(ControlClientWorkerState { - shutdown_requested: AtomicBool::new(false), - phase: AtomicU8::new(PHASE_IDLE), - request: Mutex::new(None), - response: Mutex::new(None), - requester_wait: Mutex::new(()), - requester_wakeup: Condvar::new(), - worker_thread: Mutex::new(None), - shutdown_hook: Mutex::new(Some(Box::new(shutdown_hook))), - }); - let worker_state = state.clone(); - let (finished_tx, finished_rx) = mpsc::channel(); - let worker = thread::spawn(move || { - run_control_client_worker(client, worker_state); - let _ = finished_tx.send(()); - }); - *state - .worker_thread - .lock() - .expect("control client worker-thread mutex poisoned") = Some(worker.thread().clone()); - - Self { - state, - worker: Mutex::new(Some(worker)), - finished: Mutex::new(finished_rx), - } - } - - /// Sends one request on the active control client and returns the raw protocol response. - pub fn active_raw_request( - &self, - request: BrokerRequest, - ) -> core::result::Result> { - self.state.submit(request) - } - - /// Shuts down the worker. - /// - /// If an in-flight blocking channel operation does not exit promptly, the - /// worker thread is detached after a short grace period so shutdown callers - /// are not blocked indefinitely. Use [`Self::new_with_shutdown_hook`] for - /// blocking channels that can be explicitly interrupted. - /// - /// # Panics - /// - /// Panics if the worker bookkeeping mutex is poisoned or if the worker - /// thread panicked before shutdown completed. - pub fn shutdown(&self) { - let mut worker = self - .worker - .lock() - .expect("control client worker mutex poisoned"); - if let Some(worker) = worker.take() { - self.state.request_shutdown(); - match self - .finished - .lock() - .expect("control client worker-finished mutex poisoned") - .recv_timeout(DEFAULT_SHUTDOWN_WAIT) - { - Ok(()) => worker.join().expect("control client worker panicked"), - Err(RecvTimeoutError::Disconnected) => { - worker.join().expect("control client worker panicked"); - } - Err(RecvTimeoutError::Timeout) => { - drop(worker); - } - } - } - } -} - -impl Drop for ControlClientWorker -where - T: ClientControlChannel + Send + 'static, - T::Error: Send + 'static, -{ - fn drop(&mut self) { - self.shutdown(); - } -} - -impl ControlClientWorkerState { - fn submit(&self, request: BrokerRequest) -> WorkerResult { - loop { - if self.shutdown_requested.load(Ordering::Acquire) { - return Err(ControlClientWorkerError::Shutdown); - } - match self.phase.load(Ordering::Acquire) { - PHASE_IDLE => { - if self - .phase - .compare_exchange( - PHASE_IDLE, - PHASE_RESERVED, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - if self.shutdown_requested.load(Ordering::Acquire) { - self.cancel_reserved_request_on_shutdown(); - return Err(ControlClientWorkerError::Shutdown); - } - break; - } - } - PHASE_SHUTDOWN => return Err(ControlClientWorkerError::Shutdown), - phase => self.wait_for_phase_change(phase, true), - } - } - - *self - .request - .lock() - .expect("control client worker request mutex poisoned") = Some(request); - self.phase.store(PHASE_REQUEST_READY, Ordering::Release); - self.wake_worker(); - - loop { - if self.shutdown_requested.load(Ordering::Acquire) { - return Err(ControlClientWorkerError::Shutdown); - } - match self.phase.load(Ordering::Acquire) { - PHASE_RESPONSE_READY => break, - PHASE_SHUTDOWN => return Err(ControlClientWorkerError::Shutdown), - phase => self.wait_for_phase_change(phase, true), - } - } - - let response = self - .response - .lock() - .expect("control client worker response mutex poisoned") - .take() - .expect("control client worker did not publish a response"); - self.store_phase_and_notify(PHASE_IDLE); - response - } - - fn request_aborted_before_send(&self) -> bool { - if self.shutdown_requested.load(Ordering::Acquire) { - self.store_phase_and_notify(PHASE_SHUTDOWN); - self.wake_worker(); - true - } else { - false - } - } - - fn publish_worker_response(&self, response: WorkerResult) -> bool { - if self.shutdown_requested.load(Ordering::Acquire) { - self.store_phase_and_notify(PHASE_SHUTDOWN); - false - } else { - *self - .response - .lock() - .expect("control client worker response mutex poisoned") = Some(response); - self.store_phase_and_notify(PHASE_RESPONSE_READY); - true - } - } - - fn take_request(&self) -> Option { - if self.request_aborted_before_send() { - return None; - } - let request = self - .request - .lock() - .expect("control client worker request mutex poisoned") - .take() - .expect("control client worker request missing"); - if self.shutdown_requested.load(Ordering::Acquire) { - self.store_phase_and_notify(PHASE_SHUTDOWN); - None - } else { - Some(request) - } - } - - fn cancel_reserved_request_on_shutdown(&self) { - if self - .phase - .compare_exchange( - PHASE_RESERVED, - PHASE_SHUTDOWN, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - { - self.wake_worker(); - self.requester_wakeup.notify_all(); - } - } - - fn worker_should_shutdown(&self) -> bool { - self.shutdown_requested.load(Ordering::Acquire) - || self.phase.load(Ordering::Acquire) == PHASE_SHUTDOWN - } - - fn wait_for_work_or_shutdown(&self) -> bool { - if self.worker_should_shutdown() { - return false; - } - thread::park(); - !self.worker_should_shutdown() - } - - fn transition_idle_to_shutdown(&self) -> bool { - self.phase - .compare_exchange( - PHASE_IDLE, - PHASE_SHUTDOWN, - Ordering::AcqRel, - Ordering::Acquire, - ) - .is_ok() - } - - fn cancel_in_flight_request(&self) { - self.store_phase_and_notify(PHASE_SHUTDOWN); - self.wake_worker(); - } - - fn request_shutdown_without_waiting(&self) { - if self.transition_idle_to_shutdown() { - self.wake_worker(); - } else if self.phase.load(Ordering::Acquire) == PHASE_RESERVED { - self.cancel_reserved_request_on_shutdown(); - } else { - self.cancel_in_flight_request(); - } - } - - fn request_shutdown(&self) { - { - let _guard = self - .requester_wait - .lock() - .expect("control client worker requester-wait mutex poisoned"); - self.shutdown_requested.store(true, Ordering::Release); - self.requester_wakeup.notify_all(); - } - self.run_shutdown_hook(); - self.request_shutdown_without_waiting(); - } - - fn store_phase_and_notify(&self, phase: u8) { - let _guard = self - .requester_wait - .lock() - .expect("control client worker requester-wait mutex poisoned"); - self.phase.store(phase, Ordering::Release); - self.requester_wakeup.notify_all(); - } - - fn wait_for_phase_change(&self, observed: u8, wake_on_shutdown: bool) { - let mut guard = self - .requester_wait - .lock() - .expect("control client worker requester-wait mutex poisoned"); - while self.phase.load(Ordering::Acquire) == observed - && !(wake_on_shutdown && self.shutdown_requested.load(Ordering::Acquire)) - { - guard = self - .requester_wakeup - .wait(guard) - .expect("control client worker requester-wait mutex poisoned"); - } - } - - fn wake_worker(&self) { - if let Some(worker) = self - .worker_thread - .lock() - .expect("control client worker-thread mutex poisoned") - .as_ref() - { - worker.unpark(); - } - } - - fn run_shutdown_hook(&self) { - if let Some(hook) = self - .shutdown_hook - .lock() - .expect("control client worker shutdown-hook mutex poisoned") - .take() - { - hook(); - } - } -} - -fn run_control_client_worker( - mut client: ControlClient, - state: Arc>, -) where - T: ClientControlChannel, -{ - loop { - match state.phase.load(Ordering::Acquire) { - PHASE_REQUEST_READY => { - let Some(request) = state.take_request() else { - break; - }; - let response = client - .active_raw_request(request) - .map_err(ControlClientWorkerError::Client); - if !state.publish_worker_response(response) { - break; - } - } - PHASE_SHUTDOWN => break, - _ => { - if !state.wait_for_work_or_shutdown() { - break; - } - } - } - } -} - -#[cfg(test)] -mod tests { - use core::convert::Infallible; - use std::{ - collections::VecDeque, - io, - sync::{Arc, Condvar, Mutex}, - time::{Duration, Instant}, - vec::Vec, - }; - - use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ClientControlChannel, ErrorCode, ReceivedBrokerResponse, - }; - - use super::*; - use crate::{CLIENT_PROTOCOL_VERSION, ControlClient}; - - #[test] - fn worker_returns_raw_protocol_errors() { - let sent = Arc::new(Mutex::new(Vec::new())); - let channel = FakeControlChannel::new( - sent.clone(), - [ - BrokerResponse::Negotiated { - broker_protocol_version: CLIENT_PROTOCOL_VERSION, - }, - BrokerResponse::Error(ErrorCode::WouldBlock), - ], - ); - let mut client = ControlClient::new(channel); - client.negotiate().unwrap(); - let worker = ControlClientWorker::new(client); - - let response = worker - .active_raw_request(BrokerRequest::Negotiate { - protocol_version: CLIENT_PROTOCOL_VERSION, - }) - .unwrap(); - - assert_eq!(response, BrokerResponse::Error(ErrorCode::WouldBlock)); - assert_eq!(sent.lock().unwrap().len(), 2); - worker.shutdown(); - } - - #[test] - fn worker_rejects_requests_after_shutdown() { - let sent = Arc::new(Mutex::new(Vec::new())); - let channel = FakeControlChannel::new( - sent, - [BrokerResponse::Negotiated { - broker_protocol_version: CLIENT_PROTOCOL_VERSION, - }], - ); - let mut client = ControlClient::new(channel); - client.negotiate().unwrap(); - let worker = ControlClientWorker::new(client); - - worker.shutdown(); - - assert!(matches!( - worker.active_raw_request(BrokerRequest::Negotiate { - protocol_version: CLIENT_PROTOCOL_VERSION, - }), - Err(ControlClientWorkerError::Shutdown) - )); - } - - #[test] - fn worker_shutdown_interrupts_in_flight_channel_io() { - let sent = Arc::new(Mutex::new(Vec::new())); - let interrupted = Arc::new((Mutex::new(false), Condvar::new())); - let channel = BlockingControlChannel::new(sent, interrupted.clone()); - let mut client = ControlClient::new(channel); - client.negotiate().unwrap(); - let worker = Arc::new(ControlClientWorker::new_with_shutdown_hook(client, { - let interrupted = interrupted.clone(); - move || { - let (lock, wakeup) = &*interrupted; - *lock.lock().unwrap() = true; - wakeup.notify_all(); - } - })); - - let requester = { - let worker = worker.clone(); - thread::spawn(move || { - worker.active_raw_request(BrokerRequest::Negotiate { - protocol_version: CLIENT_PROTOCOL_VERSION, - }) - }) - }; - - while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { - thread::yield_now(); - } - worker.shutdown(); - - assert!(matches!( - requester.join().unwrap(), - Err(ControlClientWorkerError::Shutdown - | ControlClientWorkerError::Client(BrokerLocalError::Channel(_))) - )); - } - - #[test] - fn worker_default_shutdown_does_not_wait_forever_for_in_flight_channel_io() { - let sent = Arc::new(Mutex::new(Vec::new())); - let interrupted = Arc::new((Mutex::new(false), Condvar::new())); - let channel = BlockingControlChannel::new(sent, interrupted); - let mut client = ControlClient::new(channel); - client.negotiate().unwrap(); - let worker = Arc::new(ControlClientWorker::new(client)); - - let requester = { - let worker = worker.clone(); - thread::spawn(move || { - worker.active_raw_request(BrokerRequest::Negotiate { - protocol_version: CLIENT_PROTOCOL_VERSION, - }) - }) - }; - - while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { - thread::yield_now(); - } - let started = Instant::now(); - worker.shutdown(); - - assert!( - started.elapsed() < Duration::from_secs(1), - "default worker shutdown waited too long" - ); - assert!(matches!( - requester.join().unwrap(), - Err(ControlClientWorkerError::Shutdown) - )); - } - - #[test] - fn worker_shutdown_hook_interrupts_worker_thread() { - let sent = Arc::new(Mutex::new(Vec::new())); - let interrupted = Arc::new((Mutex::new(false), Condvar::new())); - let channel = BlockingControlChannel::new(sent, interrupted.clone()); - let mut client = ControlClient::new(channel); - client.negotiate().unwrap(); - let worker = Arc::new(ControlClientWorker::new_with_shutdown_hook(client, { - let interrupted = interrupted.clone(); - move || { - let (lock, wakeup) = &*interrupted; - *lock.lock().unwrap() = true; - wakeup.notify_all(); - } - })); - - let requester = { - let worker = worker.clone(); - thread::spawn(move || { - worker.active_raw_request(BrokerRequest::Negotiate { - protocol_version: CLIENT_PROTOCOL_VERSION, - }) - }) - }; - - while worker.state.phase.load(Ordering::Acquire) != PHASE_REQUEST_READY { - thread::yield_now(); - } - worker.shutdown(); - - let (lock, _) = &*interrupted; - assert!(*lock.lock().unwrap(), "shutdown hook was not invoked"); - match requester.join().unwrap() { - Err(ControlClientWorkerError::Shutdown) => {} - Err(ControlClientWorkerError::Client(BrokerLocalError::Channel(error))) - if error.kind() == io::ErrorKind::Interrupted => {} - result => panic!("unexpected requester result: {result:?}"), - } - } - - struct FakeControlChannel { - sent: Arc>>, - responses: VecDeque, - } - - impl FakeControlChannel { - fn new( - sent: Arc>>, - responses: [BrokerResponse; N], - ) -> Self { - Self { - sent, - responses: VecDeque::from(responses), - } - } - } - - impl ClientControlChannel for FakeControlChannel { - type Error = Infallible; - - fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error> { - self.sent.lock().unwrap().push(request.clone()); - Ok(()) - } - - fn recv_response(&mut self) -> Result, Self::Error> { - Ok(self - .responses - .pop_front() - .map(ReceivedBrokerResponse::Response)) - } - } - - struct BlockingControlChannel { - sent: Arc>>, - interrupted: Arc<(Mutex, Condvar)>, - negotiated: bool, - } - - impl BlockingControlChannel { - fn new( - sent: Arc>>, - interrupted: Arc<(Mutex, Condvar)>, - ) -> Self { - Self { - sent, - interrupted, - negotiated: false, - } - } - } - - impl ClientControlChannel for BlockingControlChannel { - type Error = io::Error; - - fn send_request(&mut self, request: &BrokerRequest) -> Result<(), Self::Error> { - self.sent.lock().unwrap().push(request.clone()); - Ok(()) - } - - fn recv_response(&mut self) -> Result, Self::Error> { - if !self.negotiated { - self.negotiated = true; - return Ok(Some(ReceivedBrokerResponse::Response( - BrokerResponse::Negotiated { - broker_protocol_version: CLIENT_PROTOCOL_VERSION, - }, - ))); - } - - let (lock, wakeup) = &*self.interrupted; - let mut interrupted = lock.lock().unwrap(); - while !*interrupted { - interrupted = wakeup.wait(interrupted).unwrap(); - } - Err(io::Error::new( - io::ErrorKind::Interrupted, - "interrupted by shutdown", - )) - } - } -} diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index dc5ad5fc4..8e78a689f 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -65,11 +65,6 @@ impl UnixStreamClientControlChannel { } } - /// Clones the underlying stream so another owner can interrupt blocking I/O. - pub fn try_clone_stream(&self) -> io::Result { - self.stream.try_clone() - } - fn set_stream_io_timeout(&self, timeout: Option) -> io::Result<()> { self.stream.set_read_timeout(timeout)?; self.stream.set_write_timeout(timeout) 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 1a8b603e6..25c47f8a3 100644 --- a/litebox_runner_linux_userland/Cargo.toml +++ b/litebox_runner_linux_userland/Cargo.toml @@ -8,7 +8,7 @@ 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", features = ["std"] } +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" } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index dcee56719..5a5d9a79c 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -2,8 +2,8 @@ // Licensed under the MIT license. use std::{ - net::Shutdown, path::Path, + sync::Mutex, thread, time::{Duration, Instant}, }; @@ -11,23 +11,21 @@ use std::{ use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox::{BrokerControl, BrokerControlError}; -use litebox_broker_local::{ControlClient, ControlClientWorker}; +use litebox_broker_local::ControlClient; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; 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 Client = ControlClient; -type ClientWorker = ControlClientWorker; pub(crate) struct BrokerConnection { control: Arc, } struct BrokerControlClient { - worker: ClientWorker, + client: Mutex, } pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { @@ -41,30 +39,14 @@ impl BrokerConnection { pub(crate) fn control(&self) -> Arc { self.control.clone() } - - pub(crate) fn shutdown(self) { - self.control.shutdown(); - } -} - -impl Drop for BrokerConnection { - fn drop(&mut self) { - self.control.shutdown(); - } } impl BrokerControlClient { - fn new(client: Client, shutdown_stream: std::os::unix::net::UnixStream) -> Self { + fn new(client: Client) -> Self { Self { - worker: ControlClientWorker::new_with_shutdown_hook(client, move || { - let _ = shutdown_stream.shutdown(Shutdown::Both); - }), + client: Mutex::new(client), } } - - fn shutdown(&self) { - self.worker.shutdown(); - } } impl BrokerControl for BrokerControlClient { @@ -73,7 +55,9 @@ impl BrokerControl for BrokerControlClient { request: CoreRequest, ) -> core::result::Result { match self - .worker + .client + .lock() + .map_err(|_| BrokerControlError::Transport)? .active_raw_request(BrokerRequest::Core(request)) .map_err(|_| BrokerControlError::Transport)? { @@ -88,16 +72,12 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; let mut client = connect_with_retry(socket_path, setup_deadline) .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; - let shutdown_stream = client - .control_channel_mut() - .try_clone_stream() - .context("failed to clone broker control channel for shutdown")?; client .control_channel_mut() .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) .context("failed to configure broker active request timeout")?; Ok(BrokerConnection { - control: Arc::new(BrokerControlClient::new(client, shutdown_stream)), + control: Arc::new(BrokerControlClient::new(client)), }) } diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index ab5182a4c..40be86e5e 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -437,9 +437,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { net_worker.join().unwrap(); } let exit_status = program.process.wait(); - if let Some(broker_connection) = broker_connection { - broker_connection.shutdown(); - } + drop(broker_connection); std::process::exit(exit_status) } From 83117fc3bf046f89575b723625121fec6c87a460 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 12:58:45 -0700 Subject: [PATCH 53/66] Align broker local and host naming Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 36 +++--- docs/impl-plan.md | 30 ++--- litebox_broker_host/src/lib.rs | 8 +- .../src/{server.rs => negotiate.rs} | 60 +++++----- litebox_broker_local/src/event.rs | 12 +- litebox_broker_local/src/lib.rs | 108 +++++++++--------- litebox_broker_local/src/negotiate.rs | 14 +-- litebox_broker_protocol/src/channel.rs | 12 +- litebox_broker_protocol/src/error.rs | 2 +- litebox_broker_protocol/src/lib.rs | 4 +- litebox_broker_protocol/src/message.rs | 4 +- litebox_broker_transport/src/unix_socket.rs | 59 +++++----- litebox_broker_userland/src/main.rs | 4 +- .../tests/userland_broker.rs | 24 ++-- litebox_runner_linux_userland/src/broker.rs | 38 +++--- litebox_runner_linux_userland/tests/run.rs | 14 +-- 16 files changed, 213 insertions(+), 216 deletions(-) rename litebox_broker_host/src/{server.rs => negotiate.rs} (90%) diff --git a/docs/broker-design.md b/docs/broker-design.md index 48cb5d190..c1d4ef76a 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -14,7 +14,7 @@ User mode: optional BrokerService clients Authority domain: - broker entry/server + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker entry/host + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform broker-kernel user-mode support, in kernel-backed deployments ``` @@ -128,27 +128,27 @@ The broker architecture should use crate names that make the authority boundary | Crate | Initial role | |---|---| -| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, client/server control-channel traits, and request/response message-body encoding. | +| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, local/host control-channel traits, and request/response message-body encoding. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: the single broker core constructed for the broker process/kernel lifetime, broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic server control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | +| `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic host control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_transport` | Hosted concrete broker transport implementations. The current implementation is a Unix-domain-socket control channel under `unix_socket`. The crate owns stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/host crates. | -| `litebox_broker_userland` | Hosted `std` broker executable. This deployment crate wires `BrokerCore`, the current policy, the generic broker server loop, and the Unix-socket transport together. | -| `litebox_broker_local` | `no_std` channel-neutral user-side adapter linked into local-core deployments and runners: negotiate broker protocol, track client negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. Its optional `std` worker adapter runs blocking channel I/O off guest-facing threads. | +| `litebox_broker_userland` | Hosted `std` broker executable. This deployment crate wires `BrokerCore`, the current policy, the generic broker host loop, and the Unix-socket transport together. | +| `litebox_broker_local` | `no_std` channel-neutral local-side adapter linked into local-core deployments and runners: negotiate broker protocol, track local negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | | `litebox` | Local core crate: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | -`litebox_broker_local` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the client only carries broker-delegated operations over the selected control channel. +`litebox_broker_local` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the local adapter only carries broker-delegated operations over the selected control channel. -Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker server adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_transport` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. +Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker host adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_transport` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. Shared broker DTOs and the current wire codec live in `litebox_broker_protocol` to avoid repeating request/response, handle/readiness, and message-body encoding shapes across protocol, core, local, transport, and host code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_host` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. -Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to client requests. +Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to local requests. -Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic server can return `UnsupportedOperation` without closing the connection or exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the server adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. +Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic host can return `UnsupportedOperation` without closing the connection or exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the host adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. -Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while client and server connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so clients can retry with a compatible version on expensive or credentialed channels instead of reconnecting and guessing. +Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while local and host connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so local peers can retry with a compatible version on expensive or credentialed channels instead of reconnecting and guessing. The known broker protocol keeps the outer envelope intentionally small. Connection-level messages such as negotiation and common errors stay at the broker layer; BrokerCore/object operations are grouped below that layer by authority domain and object family, for example `BrokerRequest::Core(CoreRequest::Event(EventRequest::Wait { .. }))`. New object families should add a nested request/response family instead of growing a flat top-level `BrokerRequest`/`BrokerResponse` operation list. The wire codec may encode those nested families as layered tags, but tag widths and unknown-tag handling remain private to the codec. @@ -156,14 +156,14 @@ Kernel/trusted deployments will likely link broker-kernel user-mode support and | Future crate/layer | Role | |---|---| -| broker-kernel user-mode support | Trusted-domain support for user-mode execution, trap/upcall/channel delivery, process/thread setup, broker-channel endpoints, and the kernel support ABI used by local core. It supplies or adapts a server control channel, but `litebox_broker_host` still owns broker-side protocol/channel adaptation into BrokerCore. | +| broker-kernel user-mode support | Trusted-domain support for user-mode execution, trap/upcall/channel delivery, process/thread setup, broker-channel endpoints, and the kernel support ABI used by local core. It supplies or adapts a host control channel, but `litebox_broker_host` still owns broker-side protocol/channel adaptation into BrokerCore. | | `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, `litebox_broker_host`, BrokerServices, PolicyEngine, BrokerPlatform, and broker-kernel user-mode support. | These names do not require separate runtime processes. In a kernel-broker deployment, broker-kernel user-mode support, `litebox_broker_host`, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: broker-kernel user-mode support carries or classifies traffic, `litebox_broker_host` decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. ## Runtime interfaces -There are three logical interfaces. In a broker-kernel deployment, the local-core deployment-support interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, broker-kernel user-mode support should only classify and deliver traffic to the server control channel; `litebox_broker_host` decodes and sequences `BrokerRequest` values, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. +There are three logical interfaces. In a broker-kernel deployment, the local-core deployment-support interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, broker-kernel user-mode support should only classify and deliver traffic to the host control channel; `litebox_broker_host` decodes and sequences `BrokerRequest` values, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. | Interface | Userland deployment | Kernel deployment | Purpose | |---|---|---|---| @@ -199,7 +199,7 @@ External broker authority APIs: | API | Shape | |---|---| -| `local core -> broker server/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | +| `local core -> broker host/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | | `BrokerService client -> BrokerService` | service-specific protocol for an optional BrokerService | Internal broker-authority APIs: @@ -355,7 +355,7 @@ The eventual deployment contract should fail closed: 1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. 2. The runner selects a local-core profile that matches the deployment's host ABI. -3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The current Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `ServerControlChannel` API that later authenticated channels will implement. +3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The current Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `HostControlChannel` API that later authenticated channels will implement. 4. The broker binds the caller identity used for dispatch to the authenticated peer credential. User mode does not choose its own authority identity. 5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, local-core profile, deployment-support, channel/ring, and host-syscall-profile versions. 6. The broker replies with supported services and capabilities. @@ -697,7 +697,7 @@ Future mapping: | platform trait surface | internal local-core deployment-support adapter, broker-kernel user-mode support, and BrokerPlatform surfaces | | service-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | -The important change is that the current core API should not become the cross-boundary ABI. The local core can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker server/entry code adapts that protocol into BrokerCore domain calls. +The important change is that the current core API should not become the cross-boundary ABI. The local core can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker host/entry code adapts that protocol into BrokerCore domain calls. ### `litebox_platform_lvbs` @@ -808,9 +808,9 @@ Then proceed incrementally: 2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. 3. Create `litebox_broker_core` with broker-owned process association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. 4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces. -5. Add startup negotiation between runner/client and broker. The current path starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, local-core profile, and deployment-support feature negotiation. -6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/server cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. -7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the client independent of Unix sockets so future channel implementations can implement the same neutral channel traits. +5. Add startup negotiation between the local side and broker. The current path starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, local-core profile, and deployment-support feature negotiation. +6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/host cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. +7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the local adapter independent of Unix sockets so future channel implementations can implement the same neutral channel traits. 8. Continue shaping `litebox` as the untrusted local core for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. 9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. 10. Make local-core handle tables broker-backed views for migrated object families. diff --git a/docs/impl-plan.md b/docs/impl-plan.md index c3fe18f5d..db52e3686 100644 --- a/docs/impl-plan.md +++ b/docs/impl-plan.md @@ -11,7 +11,7 @@ User mode: Shim + local core + optional BrokerService clients Authority domain: - broker entry/server + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform + broker entry/host + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform broker-kernel user-mode support, in kernel-backed deployments ``` @@ -24,7 +24,7 @@ The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation - Build vertical slices, not a big-bang refactor. - Keep local core untrusted and broker authority explicit. - Keep BrokerCore shim-neutral. -- Keep BrokerCore protocol-neutral and channel-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/server code adapts protocol requests and channel credentials before calling it. +- Keep BrokerCore protocol-neutral and channel-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/host code adapts protocol requests and channel credentials before calling it. - Keep the broker protocol modular: the outer request/response envelope is for connection-level broker messages and coarse authority routing, while object/domain operations live in nested request/response families such as `CoreRequest::Event` and `EventResponse`. - Keep the control channel strictly paired request/response; broker-initiated readiness, interrupt, fault, revocation, and session-failure messages should use a separate notification channel/message family. - Put domain-specific authority in BrokerServices. @@ -86,27 +86,27 @@ Initial scope: - control channel only; - major-version/minor-compatible protocol negotiation; - neutral blocking `no_std` control-channel traits with channel-specific error types and explicit clean-close receive semantics; -- channel-produced peer credentials returned through the server control-channel trait and mapped by the broker server into BrokerCore caller credentials; +- channel-produced peer credentials returned through the host control-channel trait and mapped by the broker host into BrokerCore caller credentials; - reusable `no_std + alloc` request/response wire codec for byte-stream channel implementations; - Unix-domain-socket framing as the first concrete userland channel implementation; -- a Unix-socket executable that wires the generic channel-neutral server to the concrete Unix control-channel implementation; -- server-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; +- a Unix-socket executable that wires the generic channel-neutral host to the concrete Unix control-channel implementation; +- host-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; - BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and association cleanup; - default-deny PolicyEngine; - fail-closed channel/session behavior. Exit criteria: -- A user-side client can connect and negotiate. In the current hosted userland path, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. -- Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same server API that later deployment-specific authentication will use. -- Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic server owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. -- The Unix-socket channel adapter and hosted broker executable live in separate crates, so clients can depend on the channel without pulling in broker core/server deployment code. +- A local-side adapter can connect and negotiate. In the current hosted userland path, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. +- Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same host API that later deployment-specific authentication will use. +- Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic host owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. +- The Unix-socket channel adapter and hosted broker executable live in separate crates, so local-side code can depend on the channel without pulling in broker core/host deployment code. - BrokerCore depends only on shared broker value DTOs from `litebox_broker_protocol`; it does not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC crates. -- Client code does not need to depend on the userland broker server crate to use the first Unix socket channel. -- The generic broker server library does not depend on concrete Unix socket channel code and remains `no_std`. +- Local-side code does not need to depend on the userland broker executable or host crate to use the first Unix socket channel. +- The generic broker host library does not depend on concrete Unix socket channel code and remains `no_std`. - Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. -- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so clients can probe optional features explicitly; newer core error categories or wait outcomes that the server adapter cannot represent return `Internal`. -- Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so clients can downgrade without reconnecting or guessing. +- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so local peers can probe optional features explicitly; newer core error categories or wait outcomes that the host adapter cannot represent return `Internal`. +- Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so local peers can downgrade without reconnecting or guessing. - BrokerCore/object operations are grouped below the broker envelope instead of added as unrelated top-level `BrokerRequest` and `BrokerResponse` variants. - Control-channel contracts live in `litebox_broker_protocol::channel`, separate from semantic message DTO modules but in the same shared protocol crate. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. @@ -148,7 +148,7 @@ The local core owns: Exit criteria: - Create, wait, and signal work through BrokerCore and the separate broker process. -- BrokerCore and the server already release association-owned references on channel disconnect, and BrokerCore has an explicit in-domain `close_object_reference` operation. Protocol-level close, duplicate, explicit readiness queries, and broader stale-handle coverage remain future work after the first end-to-end path is proven. +- BrokerCore and the host already release association-owned references on channel disconnect, and BrokerCore has an explicit in-domain `close_object_reference` operation. Protocol-level close, duplicate, explicit readiness queries, and broader stale-handle coverage remain future work after the first end-to-end path is proven. ## Phase 5: Broker-backed fd semantics @@ -372,7 +372,7 @@ The smallest useful milestone is: ```text single process userland broker -typed control client +typed broker-local adapter control channel only minimal PolicyEngine broker-owned event object diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index bea8a1141..d0397bd8c 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -4,7 +4,7 @@ //! Channel-neutral broker-side protocol/core adapter. //! //! This crate wires `litebox_broker_core` to any implementation of the neutral -//! server control-channel trait. Concrete channels live in separate crates such as +//! host-side control-channel trait. Concrete channels live in separate crates such as //! `litebox_broker_transport`. #![no_std] @@ -13,9 +13,7 @@ extern crate std; mod error; -mod server; +mod negotiate; pub use error::{BrokerHostError, Result}; -pub use server::{ - CloseReason, ConnectionTermination, SUPPORTED_PROTOCOL_VERSION, serve_connection, -}; +pub use negotiate::{CloseReason, ConnectionTermination, HOST_PROTOCOL_VERSION, serve_connection}; diff --git a/litebox_broker_host/src/server.rs b/litebox_broker_host/src/negotiate.rs similarity index 90% rename from litebox_broker_host/src/server.rs rename to litebox_broker_host/src/negotiate.rs index 893bcbd04..6a8611657 100644 --- a/litebox_broker_host/src/server.rs +++ b/litebox_broker_host/src/negotiate.rs @@ -6,14 +6,14 @@ use core::fmt; use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCredential}; use litebox_broker_protocol::{ AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, - CreateEventResponse, ErrorCode, EventRequest, EventResponse, PeerCredential, ProtocolVersion, - ReceivedBrokerRequest, ServerControlChannel, WaitEventResponse, + CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, + PeerCredential, ProtocolVersion, ReceivedBrokerRequest, WaitEventResponse, }; use crate::error::{BrokerHostError, Result as HostResult}; -/// Protocol version this broker server implementation supports. -pub const SUPPORTED_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); +/// Protocol version this broker host implementation supports. +pub const HOST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); /// Serves one broker connection over the provided connected control channel. pub fn serve_connection( @@ -21,7 +21,7 @@ pub fn serve_connection( channel: &mut T, ) -> HostResult where - T: ServerControlChannel, + T: HostControlChannel, { let peer_credential = channel .peer_credential() @@ -43,7 +43,7 @@ fn serve_request_loop( association: &BrokerAssociation, ) -> HostResult where - T: ServerControlChannel, + T: HostControlChannel, { let mut state = ConnectionState::AwaitingNegotiation; loop { @@ -96,7 +96,7 @@ fn handle_request( match *state { ConnectionState::AwaitingNegotiation => match request { BrokerRequest::Negotiate { protocol_version } => { - handle_negotiation(state, protocol_version) + negotiate_version(state, protocol_version) } _ => BrokerDispatch::close_after( BrokerResponse::Error(ErrorCode::ProtocolState), @@ -176,20 +176,20 @@ fn handle_unknown_request(state: ConnectionState) -> BrokerDispatch { } } -fn handle_negotiation( +fn negotiate_version( state: &mut ConnectionState, protocol_version: ProtocolVersion, ) -> BrokerDispatch { - if protocol_version.is_supported_by(SUPPORTED_PROTOCOL_VERSION) { + 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: SUPPORTED_PROTOCOL_VERSION, + broker_protocol_version: HOST_PROTOCOL_VERSION, }) } else { BrokerDispatch::continue_after(BrokerResponse::VersionMismatch { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION, + broker_protocol_version: HOST_PROTOCOL_VERSION, }) } } @@ -258,7 +258,7 @@ impl BrokerDispatch { } } -/// Reason the broker server closed the connection after sending a response. +/// Reason the broker host closed the connection after sending a response. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CloseReason { @@ -280,7 +280,7 @@ impl fmt::Display for CloseReason { pub enum ConnectionTermination { /// The peer cleanly closed the channel. PeerClosed, - /// The server sent a terminal protocol response and closed the connection. + /// The host sent a terminal protocol response and closed the connection. BrokerClosed(CloseReason), } @@ -291,7 +291,7 @@ mod tests { use litebox_broker_protocol::CreateEventRequest; #[test] - fn server_request_handling_uses_one_broker_core() { + fn host_request_handling_uses_one_broker_core() { let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); dispatch_enforces_negotiation_state(&mut core); @@ -321,7 +321,7 @@ mod tests { &association, &mut state, BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, + protocol_version: HOST_PROTOCOL_VERSION, }, ); assert_protocol_violation(dispatch); @@ -337,14 +337,14 @@ mod tests { &association, &mut state, BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(SUPPORTED_PROTOCOL_VERSION.major + 1, 0), + protocol_version: ProtocolVersion::new(HOST_PROTOCOL_VERSION.major + 1, 0), }, ); assert_eq!( dispatch.response, BrokerResponse::VersionMismatch { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + broker_protocol_version: HOST_PROTOCOL_VERSION } ); assert_eq!(dispatch.outcome, DispatchOutcome::Continue); @@ -389,10 +389,10 @@ mod tests { fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( core: &mut BrokerCore, ) { - let mut channel = FakeServerChannel::new(std::vec::Vec::from([ + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([ Ok(Some(ReceivedBrokerRequest::Request( BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, + protocol_version: HOST_PROTOCOL_VERSION, }, ))), Ok(Some(ReceivedBrokerRequest::Request(event_create_request( @@ -408,7 +408,7 @@ mod tests { assert_eq!( channel.responses[0], BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + broker_protocol_version: HOST_PROTOCOL_VERSION } ); let handle = match &channel.responses[1] { @@ -421,13 +421,13 @@ mod tests { } fn serve_connection_closes_after_protocol_violation(core: &mut BrokerCore) { - let mut channel = FakeServerChannel::new(std::vec::Vec::from([ + 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: SUPPORTED_PROTOCOL_VERSION, + protocol_version: HOST_PROTOCOL_VERSION, }, ))), ])); @@ -444,9 +444,9 @@ mod tests { } fn serve_connection_returns_channel_error_when_response_send_fails(core: &mut BrokerCore) { - let mut channel = FakeServerChannel::new(std::vec::Vec::from([Ok(Some( + let mut channel = FakeHostControlChannel::new(std::vec::Vec::from([Ok(Some( ReceivedBrokerRequest::Request(BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, + protocol_version: HOST_PROTOCOL_VERSION, }), ))])); channel.send_error = Some(FakeChannelError::Send); @@ -486,19 +486,19 @@ mod tests { association, state, BrokerRequest::Negotiate { - protocol_version: SUPPORTED_PROTOCOL_VERSION, + protocol_version: HOST_PROTOCOL_VERSION, }, ); assert_eq!( dispatch.response, BrokerResponse::Negotiated { - broker_protocol_version: SUPPORTED_PROTOCOL_VERSION + broker_protocol_version: HOST_PROTOCOL_VERSION } ); assert_eq!( *state, ConnectionState::Active { - negotiated_protocol_version: SUPPORTED_PROTOCOL_VERSION + negotiated_protocol_version: HOST_PROTOCOL_VERSION } ); } @@ -526,14 +526,14 @@ mod tests { impl core::error::Error for FakeChannelError {} - struct FakeServerChannel { + struct FakeHostControlChannel { requests: std::vec::Vec, FakeChannelError>>, responses: std::vec::Vec, send_error: Option, } - impl FakeServerChannel { + impl FakeHostControlChannel { fn new( requests: std::vec::Vec< core::result::Result, FakeChannelError>, @@ -547,7 +547,7 @@ mod tests { } } - impl ServerControlChannel for FakeServerChannel { + impl HostControlChannel for FakeHostControlChannel { type Error = FakeChannelError; fn peer_credential(&self) -> core::result::Result { diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index b8eecc51b..e26ed4807 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -2,17 +2,17 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - AddEventRequest, BrokerRequest, BrokerResponse, ClientControlChannel, ConsumeEventRequest, - ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, - EventRequest, EventResponse, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, + AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, + CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, + LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, WaitOutcome, }; -use crate::{BrokerLocalError, ControlClient, Result}; +use crate::{BrokerLocal, BrokerLocalError, Result}; const EVENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); -impl ControlClient { +impl BrokerLocal { /// Creates a broker-owned event object. pub fn create_event(&mut self) -> Result { self.create_event_with_count(0) @@ -86,7 +86,7 @@ const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } -impl ControlClient { +impl BrokerLocal { fn ensure_event_protocol(&self) -> Result<(), T::Error> { let negotiated = self.ensure_negotiated()?; if EVENT_PROTOCOL_VERSION.is_supported_by(negotiated) { diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 3892e8ec4..ced1da57b 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -//! Typed control-channel client adapter for broker requests. +//! Typed broker-local control adapter for broker requests. //! -//! The control client owns request/response sequencing but does not own a channel. +//! 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::ClientControlChannel`]. +//! implementing [`litebox_broker_protocol::LocalControlChannel`]. #![no_std] @@ -17,16 +17,16 @@ mod event; mod negotiate; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, ReceivedBrokerResponse, + BrokerRequest, BrokerResponse, LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, }; pub use error::{BrokerLocalError, Result}; -/// Protocol version this client implementation requests by default. -pub const CLIENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); +/// Protocol version this broker-local implementation requests by default. +pub const LOCAL_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); -/// Typed control-channel client for broker operations. -pub struct ControlClient { +/// Typed broker-local control adapter for broker operations. +pub struct BrokerLocal { channel: T, state: ConnectionState, } @@ -39,8 +39,8 @@ enum ConnectionState { }, } -impl ControlClient { - /// Creates a control client over an already-connected control channel. +impl BrokerLocal { + /// Creates a broker-local control adapter over an already-connected control channel. pub const fn new(channel: T) -> Self { Self { channel, @@ -54,11 +54,11 @@ impl ControlClient { } } -impl ControlClient { +impl BrokerLocal { /// 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 client requested. + /// support a newer minor version than this local adapter requested. pub fn negotiated_protocol_version(&self) -> Option { match self.state { ConnectionState::AwaitingNegotiation => None, @@ -119,130 +119,130 @@ mod tests { fn event_operations_require_negotiation_without_sending() { let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); assert!(matches!( - client.create_event(), + local.create_event(), Err(BrokerLocalError::NotNegotiated) )); - assert_eq!(client.channel.sent_request, None); + assert_eq!(local.channel.sent_request, None); } #[test] fn event_operations_require_event_protocol_version_without_sending() { let channel = FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::UnsupportedOperation))); - let mut client = ControlClient::new(channel); - client.state = ConnectionState::Active { - negotiated_protocol_version: ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1), + let mut local = BrokerLocal::new(channel); + local.state = ConnectionState::Active { + negotiated_protocol_version: ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1), }; assert!(matches!( - client.create_event(), + local.create_event(), Err(BrokerLocalError::UnsupportedNegotiatedVersion { required, negotiated_protocol_version - }) if required == CLIENT_PROTOCOL_VERSION - && negotiated_protocol_version == ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1) + }) if required == LOCAL_PROTOCOL_VERSION + && negotiated_protocol_version == ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1) )); - assert_eq!(client.channel.sent_request, None); + assert_eq!(local.channel.sent_request, None); } #[test] - fn negotiate_version_sends_requested_version_and_activates_client() { + fn negotiate_version_sends_requested_version_and_activates_local_connection() { let requested = ProtocolVersion::new( - CLIENT_PROTOCOL_VERSION.major, - CLIENT_PROTOCOL_VERSION.minor - 1, + LOCAL_PROTOCOL_VERSION.major, + LOCAL_PROTOCOL_VERSION.minor - 1, ); let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version: CLIENT_PROTOCOL_VERSION, + broker_protocol_version: LOCAL_PROTOCOL_VERSION, })); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); - assert_eq!(client.negotiate_version(requested).unwrap(), requested); + assert_eq!(local.negotiate_version(requested).unwrap(), requested); assert_eq!( - client.channel.sent_request, + local.channel.sent_request, Some(BrokerRequest::Negotiate { protocol_version: requested }) ); - assert_eq!(client.negotiated_protocol_version(), Some(requested)); + assert_eq!(local.negotiated_protocol_version(), Some(requested)); } #[test] fn negotiate_version_rejects_incompatible_broker_response() { - let requested = CLIENT_PROTOCOL_VERSION; - let broker_version = ProtocolVersion::new(CLIENT_PROTOCOL_VERSION.major, 1); + let requested = LOCAL_PROTOCOL_VERSION; + let broker_version = ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1); let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { broker_protocol_version: broker_version, })); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); assert!(matches!( - client.negotiate_version(requested), + local.negotiate_version(requested), Err(BrokerLocalError::IncompatibleNegotiation { requested: actual_requested, broker_protocol_version }) if actual_requested == requested && broker_protocol_version == broker_version )); - assert_eq!(client.negotiated_protocol_version(), None); + assert_eq!(local.negotiated_protocol_version(), None); } #[test] fn negotiate_version_rejects_locally_unsupported_version_without_sending() { let too_new = ProtocolVersion::new( - CLIENT_PROTOCOL_VERSION.major, - CLIENT_PROTOCOL_VERSION.minor + 1, + LOCAL_PROTOCOL_VERSION.major, + LOCAL_PROTOCOL_VERSION.minor + 1, ); let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { - broker_protocol_version: CLIENT_PROTOCOL_VERSION, + broker_protocol_version: LOCAL_PROTOCOL_VERSION, })); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); assert!(matches!( - client.negotiate_version(too_new), + local.negotiate_version(too_new), Err(BrokerLocalError::UnsupportedLocalVersion { requested, local_protocol_version - }) if requested == too_new && local_protocol_version == CLIENT_PROTOCOL_VERSION + }) if requested == too_new && local_protocol_version == LOCAL_PROTOCOL_VERSION )); - assert_eq!(client.negotiated_protocol_version(), None); - assert_eq!(client.channel.sent_request, None); + assert_eq!(local.negotiated_protocol_version(), None); + assert_eq!(local.channel.sent_request, None); } #[test] fn negotiate_version_reports_broker_supported_version_and_allows_retry() { - let requested = CLIENT_PROTOCOL_VERSION; + let requested = LOCAL_PROTOCOL_VERSION; let fallback = ProtocolVersion::new( - CLIENT_PROTOCOL_VERSION.major, - CLIENT_PROTOCOL_VERSION.minor - 1, + LOCAL_PROTOCOL_VERSION.major, + LOCAL_PROTOCOL_VERSION.minor - 1, ); let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { broker_protocol_version: fallback, })); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); assert!(matches!( - client.negotiate_version(requested), + local.negotiate_version(requested), Err(BrokerLocalError::UnsupportedVersion { requested: actual_requested, broker_protocol_version }) if actual_requested == requested && broker_protocol_version == fallback )); - assert_eq!(client.negotiated_protocol_version(), None); + assert_eq!(local.negotiated_protocol_version(), None); assert_eq!( - client.channel.sent_request, + local.channel.sent_request, Some(BrokerRequest::Negotiate { protocol_version: requested }) ); - client.channel.response = Some(BrokerResponse::Negotiated { + local.channel.response = Some(BrokerResponse::Negotiated { broker_protocol_version: fallback, }); - assert_eq!(client.negotiate_version(fallback).unwrap(), fallback); - assert_eq!(client.negotiated_protocol_version(), Some(fallback)); + assert_eq!(local.negotiate_version(fallback).unwrap(), fallback); + assert_eq!(local.negotiated_protocol_version(), Some(fallback)); } struct FakeControlChannel { @@ -259,7 +259,7 @@ mod tests { } } - impl ClientControlChannel for FakeControlChannel { + impl LocalControlChannel for FakeControlChannel { type Error = Infallible; fn send_request( diff --git a/litebox_broker_local/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs index 87cc1763f..6916303e3 100644 --- a/litebox_broker_local/src/negotiate.rs +++ b/litebox_broker_local/src/negotiate.rs @@ -2,17 +2,17 @@ // Licensed under the MIT license. use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, ClientControlChannel, ProtocolVersion, + BrokerRequest, BrokerResponse, LocalControlChannel, ProtocolVersion, }; -use crate::{BrokerLocalError, CLIENT_PROTOCOL_VERSION, ControlClient, Result}; +use crate::{BrokerLocal, BrokerLocalError, LOCAL_PROTOCOL_VERSION, Result}; -impl ControlClient { - /// Negotiates the default client protocol version. +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(CLIENT_PROTOCOL_VERSION) + self.negotiate_version(LOCAL_PROTOCOL_VERSION) } /// Negotiates a caller-selected protocol version. @@ -27,10 +27,10 @@ impl ControlClient { if self.state != crate::ConnectionState::AwaitingNegotiation { return Err(BrokerLocalError::AlreadyNegotiated); } - if !protocol_version.is_supported_by(CLIENT_PROTOCOL_VERSION) { + if !protocol_version.is_supported_by(LOCAL_PROTOCOL_VERSION) { return Err(BrokerLocalError::UnsupportedLocalVersion { requested: protocol_version, - local_protocol_version: CLIENT_PROTOCOL_VERSION, + local_protocol_version: LOCAL_PROTOCOL_VERSION, }); } diff --git a/litebox_broker_protocol/src/channel.rs b/litebox_broker_protocol/src/channel.rs index a3486881d..83f4d848d 100644 --- a/litebox_broker_protocol/src/channel.rs +++ b/litebox_broker_protocol/src/channel.rs @@ -6,7 +6,7 @@ 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 server layer +/// 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] @@ -14,7 +14,7 @@ pub enum PeerCredential { /// Explicit deployment mode for the initial unauthenticated userland POC. /// /// Channels that are expected to authenticate peers must return an error - /// from [`ServerControlChannel::peer_credential`] when authentication is + /// from [`HostControlChannel::peer_credential`] when authentication is /// unavailable or fails; this variant is only for deployments that /// deliberately choose unauthenticated operation. Unauthenticated, @@ -52,8 +52,8 @@ impl From for ReceivedBrokerResponse { } } -/// Client-side control channel for broker authority calls. -pub trait ClientControlChannel { +/// Local-side control channel for broker authority calls. +pub trait LocalControlChannel { /// Channel-specific error type. type Error; @@ -67,8 +67,8 @@ pub trait ClientControlChannel { fn recv_response(&mut self) -> Result, Self::Error>; } -/// Server-side control channel for broker authority calls. -pub trait ServerControlChannel { +/// Host-side control channel for broker authority calls. +pub trait HostControlChannel { /// Channel-specific error type. type Error; diff --git a/litebox_broker_protocol/src/error.rs b/litebox_broker_protocol/src/error.rs index 1562a6275..e2113225f 100644 --- a/litebox_broker_protocol/src/error.rs +++ b/litebox_broker_protocol/src/error.rs @@ -31,7 +31,7 @@ pub enum ErrorCode { ResourceExhausted, /// The operation would block in the current event state. WouldBlock, - /// Error code emitted by a newer broker and not understood by this client. + /// 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. diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 9aed5f01b..641028fa0 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -20,8 +20,8 @@ pub mod object; pub mod wire; pub use channel::{ - ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, - ServerControlChannel, + HostControlChannel, LocalControlChannel, PeerCredential, ReceivedBrokerRequest, + ReceivedBrokerResponse, }; pub use error::ErrorCode; pub use event::{ diff --git a/litebox_broker_protocol/src/message.rs b/litebox_broker_protocol/src/message.rs index 126ae6e0c..2b321711b 100644 --- a/litebox_broker_protocol/src/message.rs +++ b/litebox_broker_protocol/src/message.rs @@ -24,7 +24,7 @@ pub enum BrokerRequest { Core(CoreRequest), } -/// Request adapted by the broker server into a BrokerCore domain call. +/// Request adapted by the broker host into a BrokerCore domain call. #[derive(Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum CoreRequest { @@ -65,7 +65,7 @@ pub enum BrokerResponse { }, /// Negotiation failed because the requested version is unsupported. /// - /// The connection remains in negotiation state and the client may retry + /// The connection remains in negotiation state and the local peer may retry /// with a compatible version using the broker-supported version advertised /// here. VersionMismatch { diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 8e78a689f..4271bd955 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -15,24 +15,23 @@ 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}; use litebox_broker_protocol::{ - ClientControlChannel, PeerCredential, ReceivedBrokerRequest, ReceivedBrokerResponse, - ServerControlChannel, + BrokerRequest, BrokerResponse, HostControlChannel, LocalControlChannel, PeerCredential, + ReceivedBrokerRequest, ReceivedBrokerResponse, }; const MAX_FRAME_LEN: usize = 64 * 1024; -/// Client-side Unix-domain-socket control channel for the hosted userland POC. -pub struct UnixStreamClientControlChannel { +/// 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 UnixStreamClientControlChannel { - /// Creates a client control channel from an already-connected Unix stream. +impl UnixStreamLocalControlChannel { + /// Creates a local control channel from an already-connected Unix stream. pub const fn from_connected(stream: UnixStream) -> Self { Self { stream, @@ -94,14 +93,14 @@ impl UnixStreamClientControlChannel { } } -/// Server-side Unix-domain-socket control channel for the hosted userland POC. -pub struct UnixStreamServerControlChannel { +/// Host-side Unix-domain-socket control channel for the hosted userland POC. +pub struct UnixStreamHostControlChannel { stream: UnixStream, io_deadline: Option, } -impl UnixStreamServerControlChannel { - /// Creates a server control channel from an accepted Unix stream. +impl UnixStreamHostControlChannel { + /// Creates a host control channel from an accepted Unix stream. pub const fn from_accepted(stream: UnixStream) -> Self { Self { stream, @@ -123,7 +122,7 @@ impl UnixStreamServerControlChannel { } } -impl ClientControlChannel for UnixStreamClientControlChannel { +impl LocalControlChannel for UnixStreamLocalControlChannel { type Error = io::Error; fn send_request(&mut self, request: &BrokerRequest) -> io::Result<()> { @@ -147,7 +146,7 @@ impl ClientControlChannel for UnixStreamClientControlChannel { } } -impl ServerControlChannel for UnixStreamServerControlChannel { +impl HostControlChannel for UnixStreamHostControlChannel { type Error = io::Error; fn peer_credential(&self) -> io::Result { @@ -351,9 +350,9 @@ mod tests { } #[test] - fn client_response_read_honors_io_timeout() { - let (client, _server) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamClientControlChannel::from_connected(client); + fn local_response_read_honors_io_timeout() { + let (local_stream, _host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); channel .set_io_timeout(Some(Duration::from_millis(10))) .unwrap(); @@ -369,18 +368,18 @@ mod tests { } #[test] - fn client_response_read_io_timeout_is_wall_clock() { - let (mut server, client) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamClientControlChannel::from_connected(client); + 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()); - server.write_all(&8u32.to_le_bytes()).unwrap(); + host_stream.write_all(&8u32.to_le_bytes()).unwrap(); for _ in 0..8 { std::thread::sleep(Duration::from_millis(20)); - if server.write_all(&[0]).is_err() { + if host_stream.write_all(&[0]).is_err() { break; } } @@ -396,15 +395,15 @@ mod tests { } #[test] - fn client_response_read_honors_io_deadline() { - let (mut server, client) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamClientControlChannel::from_connected(client); + fn local_response_read_honors_io_deadline() { + let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); channel .set_io_deadline(Some(Instant::now() + Duration::from_millis(20))) .unwrap(); let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); - server.write_all(&8u32.to_le_bytes()).unwrap(); + host_stream.write_all(&8u32.to_le_bytes()).unwrap(); let error = reader.join().expect("deadline reader panicked"); assert!( @@ -417,18 +416,18 @@ mod tests { } #[test] - fn server_request_read_io_deadline_is_wall_clock() { - let (mut client, server) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamServerControlChannel::from_accepted(server); + fn host_request_read_io_deadline_is_wall_clock() { + let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); + let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); channel .set_io_deadline(Some(Instant::now() + Duration::from_millis(50))) .unwrap(); let reader = std::thread::spawn(move || channel.recv_request().unwrap_err()); - client.write_all(&8u32.to_le_bytes()).unwrap(); + local_stream.write_all(&8u32.to_le_bytes()).unwrap(); for _ in 0..8 { std::thread::sleep(Duration::from_millis(20)); - if client.write_all(&[0]).is_err() { + if local_stream.write_all(&[0]).is_err() { break; } } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 11f115e4c..5ee05f6ed 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -11,7 +11,7 @@ use std::time::{Duration, Instant}; use litebox_broker_core::{BrokerCore, PolicyEngine}; use litebox_broker_host::{BrokerHostError, serve_connection}; -use litebox_broker_transport::unix_socket::UnixStreamServerControlChannel; +use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; const SESSION_TIMEOUT: Duration = Duration::from_secs(5); @@ -20,7 +20,7 @@ fn main() -> io::Result<()> { let listener = bind_listener(&args.socket_path)?; let _socket_cleanup = SocketPathCleanup::new(args.socket_path.clone()); let (stream, _) = listener.accept()?; - let mut channel = UnixStreamServerControlChannel::from_accepted(stream); + let mut channel = UnixStreamHostControlChannel::from_accepted(stream); channel.set_io_deadline(Some(Instant::now() + SESSION_TIMEOUT))?; let mut broker = BrokerCore::new(PolicyEngine::event_only()).map_err(io::Error::other)?; serve_connection(&mut broker, &mut channel) diff --git a/litebox_broker_userland/tests/userland_broker.rs b/litebox_broker_userland/tests/userland_broker.rs index ddd8cbe37..1d18bdb42 100644 --- a/litebox_broker_userland/tests/userland_broker.rs +++ b/litebox_broker_userland/tests/userland_broker.rs @@ -9,10 +9,10 @@ use std::process::{Child, Command, ExitStatus}; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use litebox_broker_host::SUPPORTED_PROTOCOL_VERSION; -use litebox_broker_local::ControlClient; +use litebox_broker_host::HOST_PROTOCOL_VERSION; +use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::{ReadinessState, WaitOutcome}; -use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; +use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; #[test] fn separate_process_broker_serves_event_object_requests() { @@ -22,26 +22,26 @@ fn separate_process_broker_serves_event_object_requests() { channel .set_io_timeout(Some(Duration::from_secs(5))) .unwrap(); - let mut client = ControlClient::new(channel); + let mut local = BrokerLocal::new(channel); - assert_eq!(client.negotiate().unwrap(), SUPPORTED_PROTOCOL_VERSION); + assert_eq!(local.negotiate().unwrap(), HOST_PROTOCOL_VERSION); - let handle = client.create_event().unwrap(); + let handle = local.create_event().unwrap(); assert_eq!( - client.wait_event(handle).unwrap(), + local.wait_event(handle).unwrap(), WaitOutcome::WouldBlock(ReadinessState::new(false, true, 0)) ); assert_eq!( - client.add_event(handle, 1).unwrap(), + local.add_event(handle, 1).unwrap(), ReadinessState::new(true, true, 1) ); assert_eq!( - client.wait_event(handle).unwrap(), + local.wait_event(handle).unwrap(), WaitOutcome::Ready(ReadinessState::new(true, true, 1)) ); - drop(client); + drop(local); assert!(child.wait().unwrap().success()); } @@ -109,10 +109,10 @@ impl Drop for SocketPathGuard { } } -fn connect_with_retry(socket_path: &Path) -> io::Result { +fn connect_with_retry(socket_path: &Path) -> io::Result { let deadline = Instant::now() + Duration::from_secs(5); loop { - match UnixStreamClientControlChannel::connect(socket_path) { + match UnixStreamLocalControlChannel::connect(socket_path) { Ok(channel) => return Ok(channel), Err(error) if Instant::now() < deadline => { if error.kind() != io::ErrorKind::NotFound diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 5a5d9a79c..4586bbd58 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -11,21 +11,21 @@ use std::{ use alloc::sync::Arc; use anyhow::{Context as _, Result}; use litebox::{BrokerControl, BrokerControlError}; -use litebox_broker_local::ControlClient; +use litebox_broker_local::BrokerLocal; use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; -use litebox_broker_transport::unix_socket::UnixStreamClientControlChannel; +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 Client = ControlClient; +type Local = BrokerLocal; pub(crate) struct BrokerConnection { - control: Arc, + control: Arc, } -struct BrokerControlClient { - client: Mutex, +struct BrokerLocalControl { + local: Mutex, } pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { @@ -41,21 +41,21 @@ impl BrokerConnection { } } -impl BrokerControlClient { - fn new(client: Client) -> Self { +impl BrokerLocalControl { + fn new(local: Local) -> Self { Self { - client: Mutex::new(client), + local: Mutex::new(local), } } } -impl BrokerControl for BrokerControlClient { +impl BrokerControl for BrokerLocalControl { fn request( &self, request: CoreRequest, ) -> core::result::Result { match self - .client + .local .lock() .map_err(|_| BrokerControlError::Transport)? .active_raw_request(BrokerRequest::Core(request)) @@ -70,27 +70,27 @@ impl BrokerControl for BrokerControlClient { fn connect_to_endpoint(socket_path: &Path) -> Result { let setup_deadline = Instant::now() + SETUP_TIMEOUT; - let mut client = connect_with_retry(socket_path, setup_deadline) + let mut local = connect_with_retry(socket_path, setup_deadline) .with_context(|| format!("failed to connect to broker at {}", socket_path.display()))?; - client + local .control_channel_mut() .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) .context("failed to configure broker active request timeout")?; Ok(BrokerConnection { - control: Arc::new(BrokerControlClient::new(client)), + control: Arc::new(BrokerLocalControl::new(local)), }) } -fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { +fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { loop { - match UnixStreamClientControlChannel::connect(socket_path) { + match UnixStreamLocalControlChannel::connect(socket_path) { Ok(mut channel) => { channel .set_io_deadline(Some(setup_deadline)) .context("failed to configure broker setup deadline")?; - let mut client = ControlClient::new(channel); - client.negotiate().context("broker negotiation failed")?; - return Ok(client); + let mut local = BrokerLocal::new(channel); + local.negotiate().context("broker negotiation failed")?; + return Ok(local); } Err(error) => { if Instant::now() >= setup_deadline { diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 9d909a770..344ff5ec8 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -271,12 +271,12 @@ impl TestBroker { fn join(mut self) { self.done_rx .recv_timeout(BROKER_HELPER_TIMEOUT) - .expect("broker test server did not finish"); + .expect("broker test host did not finish"); self.thread .take() - .expect("broker test server thread missing") + .expect("broker test host thread missing") .join() - .expect("broker test server panicked"); + .expect("broker test host panicked"); let _ = std::fs::remove_file(&self.socket_path); } } @@ -311,7 +311,7 @@ fn spawn_test_broker( for _ in 0..connection_count { let (stream, _) = listener .accept() - .expect("failed to accept broker control client"); + .expect("failed to accept broker local control connection"); stream .set_read_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test read timeout"); @@ -319,9 +319,9 @@ fn spawn_test_broker( .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); let mut channel = - litebox_broker_transport::unix_socket::UnixStreamServerControlChannel::from_accepted(stream); + litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream); let termination = litebox_broker_host::serve_connection(&mut core, &mut channel) - .expect("broker server failed"); + .expect("broker host failed"); assert_eq!( termination, litebox_broker_host::ConnectionTermination::PeerClosed @@ -337,7 +337,7 @@ fn spawn_test_broker( ready_rx .recv_timeout(std::time::Duration::from_secs(5)) - .expect("broker test server did not start"); + .expect("broker test host did not start"); TestBroker { thread: Some(broker_thread), done_rx, From 45732e1f68a252953e357b3d2d6dafd52f526fa1 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 13:08:57 -0700 Subject: [PATCH 54/66] Fold broker negotiation modules into crate roots Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_host/src/lib.rs | 576 ++++++++++++++++++++++++- litebox_broker_host/src/negotiate.rs | 578 -------------------------- litebox_broker_local/src/lib.rs | 53 ++- litebox_broker_local/src/negotiate.rs | 62 --- 4 files changed, 626 insertions(+), 643 deletions(-) delete mode 100644 litebox_broker_host/src/negotiate.rs delete mode 100644 litebox_broker_local/src/negotiate.rs diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index d0397bd8c..49b7f88b5 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -12,8 +12,580 @@ #[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, + PeerCredential, ProtocolVersion, ReceivedBrokerRequest, WaitEventResponse, +}; + mod error; -mod negotiate; pub use error::{BrokerHostError, Result}; -pub use negotiate::{CloseReason, ConnectionTermination, HOST_PROTOCOL_VERSION, serve_connection}; + +/// Protocol version this broker host implementation supports. +pub const HOST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); + +/// 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(); + + dispatch_enforces_negotiation_state(&mut core); + dispatch_rejects_unsupported_protocol_version_without_activation(&mut core); + dispatch_handles_unknown_wire_requests_by_state(&mut core); + 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 dispatch_enforces_negotiation_state(core: &mut BrokerCore) { + { + let (association, mut state) = new_association(core); + + let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); + assert_protocol_violation(dispatch); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + core.close_association(association); + } + + { + let (association, mut state) = new_association(core); + negotiate(core, &association, &mut state); + + let dispatch = handle_request( + core, + &association, + &mut state, + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ); + assert_protocol_violation(dispatch); + core.close_association(association); + } + } + + fn dispatch_rejects_unsupported_protocol_version_without_activation(core: &mut BrokerCore) { + let (association, mut state) = new_association(core); + + let dispatch = handle_request( + core, + &association, + &mut state, + BrokerRequest::Negotiate { + protocol_version: ProtocolVersion::new(HOST_PROTOCOL_VERSION.major + 1, 0), + }, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::VersionMismatch { + broker_protocol_version: HOST_PROTOCOL_VERSION + } + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + assert_eq!(state, ConnectionState::AwaitingNegotiation); + core.close_association(association); + } + + fn dispatch_handles_unknown_wire_requests_by_state(core: &mut BrokerCore) { + { + let (association, mut state) = new_association(core); + + let dispatch = handle_received_request( + core, + &association, + &mut state, + ReceivedBrokerRequest::Unknown, + ); + assert_protocol_violation(dispatch); + core.close_association(association); + } + + { + let (association, mut state) = new_association(core); + negotiate(core, &association, &mut state); + + let dispatch = handle_received_request( + core, + &association, + &mut state, + ReceivedBrokerRequest::Unknown, + ); + + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::UnsupportedOperation) + ); + assert_eq!(dispatch.outcome, DispatchOutcome::Continue); + core.close_association(association); + } + } + + 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()); + } + + fn assert_protocol_violation(dispatch: BrokerDispatch) { + assert_eq!( + dispatch.response, + BrokerResponse::Error(ErrorCode::ProtocolState) + ); + assert_eq!( + dispatch.outcome, + DispatchOutcome::Close(CloseReason::ProtocolViolation) + ); + } + + fn new_association(core: &mut BrokerCore) -> (BrokerAssociation, ConnectionState) { + let association = core + .create_association(CallerCredential::Unauthenticated) + .unwrap(); + (association, ConnectionState::AwaitingNegotiation) + } + + fn negotiate( + core: &mut BrokerCore, + association: &BrokerAssociation, + state: &mut ConnectionState, + ) { + let dispatch = handle_request( + core, + association, + state, + BrokerRequest::Negotiate { + protocol_version: HOST_PROTOCOL_VERSION, + }, + ); + assert_eq!( + dispatch.response, + BrokerResponse::Negotiated { + broker_protocol_version: HOST_PROTOCOL_VERSION + } + ); + assert_eq!( + *state, + ConnectionState::Active { + negotiated_protocol_version: HOST_PROTOCOL_VERSION + } + ); + } + + 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_host/src/negotiate.rs b/litebox_broker_host/src/negotiate.rs deleted file mode 100644 index 6a8611657..000000000 --- a/litebox_broker_host/src/negotiate.rs +++ /dev/null @@ -1,578 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -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, - PeerCredential, ProtocolVersion, ReceivedBrokerRequest, WaitEventResponse, -}; - -use crate::error::{BrokerHostError, Result as HostResult}; - -/// Protocol version this broker host implementation supports. -pub const HOST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); - -/// Serves one broker connection over the provided connected control channel. -pub fn serve_connection( - core: &mut BrokerCore, - channel: &mut T, -) -> HostResult -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, -) -> HostResult -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(); - - dispatch_enforces_negotiation_state(&mut core); - dispatch_rejects_unsupported_protocol_version_without_activation(&mut core); - dispatch_handles_unknown_wire_requests_by_state(&mut core); - 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 dispatch_enforces_negotiation_state(core: &mut BrokerCore) { - { - let (association, mut state) = new_association(core); - - let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); - assert_protocol_violation(dispatch); - assert_eq!(state, ConnectionState::AwaitingNegotiation); - core.close_association(association); - } - - { - let (association, mut state) = new_association(core); - negotiate(core, &association, &mut state); - - let dispatch = handle_request( - core, - &association, - &mut state, - BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, - }, - ); - assert_protocol_violation(dispatch); - core.close_association(association); - } - } - - fn dispatch_rejects_unsupported_protocol_version_without_activation(core: &mut BrokerCore) { - let (association, mut state) = new_association(core); - - let dispatch = handle_request( - core, - &association, - &mut state, - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(HOST_PROTOCOL_VERSION.major + 1, 0), - }, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::VersionMismatch { - broker_protocol_version: HOST_PROTOCOL_VERSION - } - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!(state, ConnectionState::AwaitingNegotiation); - core.close_association(association); - } - - fn dispatch_handles_unknown_wire_requests_by_state(core: &mut BrokerCore) { - { - let (association, mut state) = new_association(core); - - let dispatch = handle_received_request( - core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - assert_protocol_violation(dispatch); - core.close_association(association); - } - - { - let (association, mut state) = new_association(core); - negotiate(core, &association, &mut state); - - let dispatch = handle_received_request( - core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::UnsupportedOperation) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - core.close_association(association); - } - } - - 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()); - } - - fn assert_protocol_violation(dispatch: BrokerDispatch) { - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::ProtocolViolation) - ); - } - - fn new_association(core: &mut BrokerCore) -> (BrokerAssociation, ConnectionState) { - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - (association, ConnectionState::AwaitingNegotiation) - } - - fn negotiate( - core: &mut BrokerCore, - association: &BrokerAssociation, - state: &mut ConnectionState, - ) { - let dispatch = handle_request( - core, - association, - state, - BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, - }, - ); - assert_eq!( - dispatch.response, - BrokerResponse::Negotiated { - broker_protocol_version: HOST_PROTOCOL_VERSION - } - ); - assert_eq!( - *state, - ConnectionState::Active { - negotiated_protocol_version: HOST_PROTOCOL_VERSION - } - ); - } - - 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/src/lib.rs b/litebox_broker_local/src/lib.rs index ced1da57b..8b40d1623 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -14,7 +14,6 @@ extern crate std; mod error; mod event; -mod negotiate; use litebox_broker_protocol::{ BrokerRequest, BrokerResponse, LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, @@ -55,6 +54,58 @@ impl BrokerLocal { } 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 diff --git a/litebox_broker_local/src/negotiate.rs b/litebox_broker_local/src/negotiate.rs deleted file mode 100644 index 6916303e3..000000000 --- a/litebox_broker_local/src/negotiate.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, LocalControlChannel, ProtocolVersion, -}; - -use crate::{BrokerLocal, BrokerLocalError, LOCAL_PROTOCOL_VERSION, Result}; - -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 != crate::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 = crate::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)), - } - } -} From 24529d7e534e98568f1a4fce9d50d822824b4bee Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 13:25:03 -0700 Subject: [PATCH 55/66] Prune broker local and host tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_host/src/lib.rs | 132 -------------------------------- litebox_broker_local/src/lib.rs | 83 +------------------- 2 files changed, 3 insertions(+), 212 deletions(-) diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 49b7f88b5..8f62feb3a 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -307,98 +307,11 @@ mod tests { fn host_request_handling_uses_one_broker_core() { let mut core = BrokerCore::new(PolicyEngine::event_only()).unwrap(); - dispatch_enforces_negotiation_state(&mut core); - dispatch_rejects_unsupported_protocol_version_without_activation(&mut core); - dispatch_handles_unknown_wire_requests_by_state(&mut core); 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 dispatch_enforces_negotiation_state(core: &mut BrokerCore) { - { - let (association, mut state) = new_association(core); - - let dispatch = handle_request(core, &association, &mut state, event_create_request(0)); - assert_protocol_violation(dispatch); - assert_eq!(state, ConnectionState::AwaitingNegotiation); - core.close_association(association); - } - - { - let (association, mut state) = new_association(core); - negotiate(core, &association, &mut state); - - let dispatch = handle_request( - core, - &association, - &mut state, - BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, - }, - ); - assert_protocol_violation(dispatch); - core.close_association(association); - } - } - - fn dispatch_rejects_unsupported_protocol_version_without_activation(core: &mut BrokerCore) { - let (association, mut state) = new_association(core); - - let dispatch = handle_request( - core, - &association, - &mut state, - BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(HOST_PROTOCOL_VERSION.major + 1, 0), - }, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::VersionMismatch { - broker_protocol_version: HOST_PROTOCOL_VERSION - } - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - assert_eq!(state, ConnectionState::AwaitingNegotiation); - core.close_association(association); - } - - fn dispatch_handles_unknown_wire_requests_by_state(core: &mut BrokerCore) { - { - let (association, mut state) = new_association(core); - - let dispatch = handle_received_request( - core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - assert_protocol_violation(dispatch); - core.close_association(association); - } - - { - let (association, mut state) = new_association(core); - negotiate(core, &association, &mut state); - - let dispatch = handle_received_request( - core, - &association, - &mut state, - ReceivedBrokerRequest::Unknown, - ); - - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::UnsupportedOperation) - ); - assert_eq!(dispatch.outcome, DispatchOutcome::Continue); - core.close_association(association); - } - } - fn serve_connection_negotiates_routes_one_request_and_returns_peer_closed( core: &mut BrokerCore, ) { @@ -471,51 +384,6 @@ mod tests { assert!(channel.responses.is_empty()); } - fn assert_protocol_violation(dispatch: BrokerDispatch) { - assert_eq!( - dispatch.response, - BrokerResponse::Error(ErrorCode::ProtocolState) - ); - assert_eq!( - dispatch.outcome, - DispatchOutcome::Close(CloseReason::ProtocolViolation) - ); - } - - fn new_association(core: &mut BrokerCore) -> (BrokerAssociation, ConnectionState) { - let association = core - .create_association(CallerCredential::Unauthenticated) - .unwrap(); - (association, ConnectionState::AwaitingNegotiation) - } - - fn negotiate( - core: &mut BrokerCore, - association: &BrokerAssociation, - state: &mut ConnectionState, - ) { - let dispatch = handle_request( - core, - association, - state, - BrokerRequest::Negotiate { - protocol_version: HOST_PROTOCOL_VERSION, - }, - ); - assert_eq!( - dispatch.response, - BrokerResponse::Negotiated { - broker_protocol_version: HOST_PROTOCOL_VERSION - } - ); - assert_eq!( - *state, - ConnectionState::Active { - negotiated_protocol_version: HOST_PROTOCOL_VERSION - } - ); - } - const fn event_request(request: EventRequest) -> BrokerRequest { BrokerRequest::Core(CoreRequest::Event(request)) } diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index 8b40d1623..ea849f4d1 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -164,12 +164,11 @@ impl BrokerLocal { mod tests { use super::*; use core::convert::Infallible; - use litebox_broker_protocol::{ErrorCode, ProtocolVersion}; + use litebox_broker_protocol::ProtocolVersion; #[test] fn event_operations_require_negotiation_without_sending() { - let channel = - FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::ProtocolState))); + let channel = FakeControlChannel::new(None); let mut local = BrokerLocal::new(channel); assert!(matches!( @@ -179,26 +178,6 @@ mod tests { assert_eq!(local.channel.sent_request, None); } - #[test] - fn event_operations_require_event_protocol_version_without_sending() { - let channel = - FakeControlChannel::new(Some(BrokerResponse::Error(ErrorCode::UnsupportedOperation))); - let mut local = BrokerLocal::new(channel); - local.state = ConnectionState::Active { - negotiated_protocol_version: ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1), - }; - - assert!(matches!( - local.create_event(), - Err(BrokerLocalError::UnsupportedNegotiatedVersion { - required, - negotiated_protocol_version - }) if required == LOCAL_PROTOCOL_VERSION - && negotiated_protocol_version == ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1) - )); - assert_eq!(local.channel.sent_request, None); - } - #[test] fn negotiate_version_sends_requested_version_and_activates_local_connection() { let requested = ProtocolVersion::new( @@ -220,34 +199,13 @@ mod tests { assert_eq!(local.negotiated_protocol_version(), Some(requested)); } - #[test] - fn negotiate_version_rejects_incompatible_broker_response() { - let requested = LOCAL_PROTOCOL_VERSION; - let broker_version = ProtocolVersion::new(LOCAL_PROTOCOL_VERSION.major, 1); - let channel = FakeControlChannel::new(Some(BrokerResponse::Negotiated { - broker_protocol_version: broker_version, - })); - let mut local = BrokerLocal::new(channel); - - assert!(matches!( - local.negotiate_version(requested), - Err(BrokerLocalError::IncompatibleNegotiation { - requested: actual_requested, - broker_protocol_version - }) if actual_requested == requested && broker_protocol_version == broker_version - )); - assert_eq!(local.negotiated_protocol_version(), None); - } - #[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(Some(BrokerResponse::VersionMismatch { - broker_protocol_version: LOCAL_PROTOCOL_VERSION, - })); + let channel = FakeControlChannel::new(None); let mut local = BrokerLocal::new(channel); assert!(matches!( @@ -261,41 +219,6 @@ mod tests { assert_eq!(local.channel.sent_request, None); } - #[test] - fn negotiate_version_reports_broker_supported_version_and_allows_retry() { - let requested = LOCAL_PROTOCOL_VERSION; - let fallback = ProtocolVersion::new( - LOCAL_PROTOCOL_VERSION.major, - LOCAL_PROTOCOL_VERSION.minor - 1, - ); - let channel = FakeControlChannel::new(Some(BrokerResponse::VersionMismatch { - broker_protocol_version: fallback, - })); - let mut local = BrokerLocal::new(channel); - - assert!(matches!( - local.negotiate_version(requested), - Err(BrokerLocalError::UnsupportedVersion { - requested: actual_requested, - broker_protocol_version - }) if actual_requested == requested && broker_protocol_version == fallback - )); - assert_eq!(local.negotiated_protocol_version(), None); - assert_eq!( - local.channel.sent_request, - Some(BrokerRequest::Negotiate { - protocol_version: requested - }) - ); - - local.channel.response = Some(BrokerResponse::Negotiated { - broker_protocol_version: fallback, - }); - - assert_eq!(local.negotiate_version(fallback).unwrap(), fallback); - assert_eq!(local.negotiated_protocol_version(), Some(fallback)); - } - struct FakeControlChannel { sent_request: Option, response: Option, From d5b0f49a656613746d4d363a54062dd447f900ae Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 13:42:26 -0700 Subject: [PATCH 56/66] Centralize initial broker protocol version Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 2 +- litebox_broker_host/src/lib.rs | 5 +++-- litebox_broker_local/src/event.rs | 6 +++--- litebox_broker_local/src/lib.rs | 14 ++++++-------- litebox_broker_protocol/src/lib.rs | 3 +++ 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/broker-design.md b/docs/broker-design.md index c1d4ef76a..93123e160 100644 --- a/docs/broker-design.md +++ b/docs/broker-design.md @@ -128,7 +128,7 @@ The broker architecture should use crate names that make the authority boundary | Crate | Initial role | |---|---| -| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, local/host control-channel traits, and request/response message-body encoding. | +| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type and initial version constant, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, local/host control-channel traits, and request/response message-body encoding. | | `litebox_broker_core` | Protocol- and channel-independent authority logic: the single broker core constructed for the broker process/kernel lifetime, broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | | `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic host control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | | `litebox_broker_transport` | Hosted concrete broker transport implementations. The current implementation is a Unix-domain-socket control channel under `unix_socket`. The crate owns stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/host crates. | diff --git a/litebox_broker_host/src/lib.rs b/litebox_broker_host/src/lib.rs index 8f62feb3a..e0cc97d7f 100644 --- a/litebox_broker_host/src/lib.rs +++ b/litebox_broker_host/src/lib.rs @@ -18,7 +18,8 @@ use litebox_broker_core::{BrokerAssociation, BrokerCore, BrokerError, CallerCred use litebox_broker_protocol::{ AddEventResponse, BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, CreateEventResponse, ErrorCode, EventRequest, EventResponse, HostControlChannel, - PeerCredential, ProtocolVersion, ReceivedBrokerRequest, WaitEventResponse, + INITIAL_PROTOCOL_VERSION, PeerCredential, ProtocolVersion, ReceivedBrokerRequest, + WaitEventResponse, }; mod error; @@ -26,7 +27,7 @@ mod error; pub use error::{BrokerHostError, Result}; /// Protocol version this broker host implementation supports. -pub const HOST_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); +pub const HOST_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; /// Serves one broker connection over the provided connected control channel. pub fn serve_connection( diff --git a/litebox_broker_local/src/event.rs b/litebox_broker_local/src/event.rs index e26ed4807..7c07501c5 100644 --- a/litebox_broker_local/src/event.rs +++ b/litebox_broker_local/src/event.rs @@ -4,13 +4,13 @@ use litebox_broker_protocol::{ AddEventRequest, BrokerRequest, BrokerResponse, ConsumeEventRequest, ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, EventConsumeMode, EventRequest, EventResponse, - LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, WaitEventRequest, - WaitOutcome, + INITIAL_PROTOCOL_VERSION, LocalControlChannel, ObjectHandle, ProtocolVersion, ReadinessState, + WaitEventRequest, WaitOutcome, }; use crate::{BrokerLocal, BrokerLocalError, Result}; -const EVENT_PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::new(0, 2); +const EVENT_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; impl BrokerLocal { /// Creates a broker-owned event object. diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index ea849f4d1..ab73e93c8 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -16,13 +16,14 @@ mod error; mod event; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, + BrokerRequest, BrokerResponse, 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 = ProtocolVersion::new(0, 2); +pub const LOCAL_PROTOCOL_VERSION: ProtocolVersion = INITIAL_PROTOCOL_VERSION; /// Typed broker-local control adapter for broker operations. pub struct BrokerLocal { @@ -179,17 +180,14 @@ mod tests { } #[test] - fn negotiate_version_sends_requested_version_and_activates_local_connection() { - let requested = ProtocolVersion::new( - LOCAL_PROTOCOL_VERSION.major, - LOCAL_PROTOCOL_VERSION.minor - 1, - ); + 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_version(requested).unwrap(), requested); + assert_eq!(local.negotiate().unwrap(), requested); assert_eq!( local.channel.sent_request, Some(BrokerRequest::Negotiate { diff --git a/litebox_broker_protocol/src/lib.rs b/litebox_broker_protocol/src/lib.rs index 641028fa0..0f081bec3 100644 --- a/litebox_broker_protocol/src/lib.rs +++ b/litebox_broker_protocol/src/lib.rs @@ -58,3 +58,6 @@ impl ProtocolVersion { 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); From 2620a48c05d5a4df98467c4124c0961f83555613 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 14:15:07 -0700 Subject: [PATCH 57/66] Organize broker wire codec by protocol layer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_protocol/src/wire.rs | 357 +++--------------- .../src/wire/core_message.rs | 59 +++ litebox_broker_protocol/src/wire/event.rs | 177 +++++++++ litebox_broker_protocol/src/wire/primitive.rs | 113 ++++++ 4 files changed, 392 insertions(+), 314 deletions(-) create mode 100644 litebox_broker_protocol/src/wire/core_message.rs create mode 100644 litebox_broker_protocol/src/wire/event.rs create mode 100644 litebox_broker_protocol/src/wire/primitive.rs diff --git a/litebox_broker_protocol/src/wire.rs b/litebox_broker_protocol/src/wire.rs index df47ac34d..af5a3a171 100644 --- a/litebox_broker_protocol/src/wire.rs +++ b/litebox_broker_protocol/src/wire.rs @@ -2,42 +2,41 @@ // 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::{ - AddEventRequest, AddEventResponse, BrokerRequest, BrokerResponse, ConsumeEventRequest, - ConsumeEventResponse, CoreRequest, CoreResponse, CreateEventRequest, CreateEventResponse, - ErrorCode, EventConsumeMode, EventRequest, EventResponse, ObjectHandle, - ObjectReferenceGeneration, ObjectReferenceId, ProtocolVersion, ReadinessState, - ReceivedBrokerRequest, ReceivedBrokerResponse, WaitEventRequest, WaitEventResponse, - WaitOutcome, + 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 CORE_REQUEST_TAG_EVENT: u8 = 0; -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 RESPONSE_TAG_NEGOTIATED: u8 = 0; const RESPONSE_TAG_CORE: u8 = 1; const RESPONSE_TAG_ERROR: u8 = 2; const RESPONSE_TAG_VERSION_MISMATCH: u8 = 3; -const CORE_RESPONSE_TAG_EVENT: u8 = 0; -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; /// Error produced while encoding or decoding a broker wire message. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -75,45 +74,14 @@ pub fn encode_request(request: BrokerRequest) -> Vec { match request { BrokerRequest::Negotiate { protocol_version } => { encoder.u8(REQUEST_TAG_NEGOTIATE); - encoder.u16(protocol_version.major); - encoder.u16(protocol_version.minor); - } - BrokerRequest::Core(request) => encode_core_request(&mut encoder, request), - } - encoder.finish() -} - -fn encode_core_request(encoder: &mut Encoder, request: CoreRequest) { - encoder.u8(REQUEST_TAG_CORE); - match request { - CoreRequest::Event(request) => { - encoder.u8(CORE_REQUEST_TAG_EVENT); - encode_event_request(encoder, request); - } - } -} - -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); + encoder.protocol_version(protocol_version); } - 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_event_consume_mode(encoder, request.mode); + BrokerRequest::Core(request) => { + encoder.u8(REQUEST_TAG_CORE); + core_message::encode_core_request(&mut encoder, request); } } + encoder.finish() } /// Decodes a broker request body. @@ -122,9 +90,9 @@ pub fn decode_request(frame: &[u8]) -> Result let tag = decoder.u8()?; let request = match tag { REQUEST_TAG_NEGOTIATE => BrokerRequest::Negotiate { - protocol_version: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + protocol_version: decoder.protocol_version()?, }, - REQUEST_TAG_CORE => match decode_core_request(&mut decoder)? { + REQUEST_TAG_CORE => match core_message::decode_core_request(&mut decoder)? { Some(request) => BrokerRequest::Core(request), None => return Ok(ReceivedBrokerRequest::Unknown), }, @@ -134,38 +102,6 @@ pub fn decode_request(frame: &[u8]) -> Result Ok(ReceivedBrokerRequest::Request(request)) } -fn decode_core_request(decoder: &mut Decoder<'_>) -> Result, WireError> { - let request = match decoder.u8()? { - CORE_REQUEST_TAG_EVENT => match decode_event_request(decoder)? { - Some(request) => CoreRequest::Event(request), - None => return Ok(None), - }, - _ => return Ok(None), - }; - - Ok(Some(request)) -} - -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_event_consume_mode(decoder)? { - Some(mode) => mode, - None => return Ok(None), - }, - )), - _ => return Ok(None), - }; - - Ok(Some(request)) -} - /// Encodes a broker response body. /// /// Successful encodings are always non-empty because the first byte is the @@ -177,17 +113,18 @@ pub fn encode_response(response: BrokerResponse) -> Vec { broker_protocol_version, } => { encoder.u8(RESPONSE_TAG_NEGOTIATED); - encoder.u16(broker_protocol_version.major); - encoder.u16(broker_protocol_version.minor); + encoder.protocol_version(broker_protocol_version); } BrokerResponse::VersionMismatch { broker_protocol_version, } => { encoder.u8(RESPONSE_TAG_VERSION_MISMATCH); - encoder.u16(broker_protocol_version.major); - encoder.u16(broker_protocol_version.minor); + encoder.protocol_version(broker_protocol_version); + } + BrokerResponse::Core(response) => { + encoder.u8(RESPONSE_TAG_CORE); + core_message::encode_core_response(&mut encoder, response); } - BrokerResponse::Core(response) => encode_core_response(&mut encoder, response), BrokerResponse::Error(error) => { encoder.u8(RESPONSE_TAG_ERROR); encoder.u16(error.as_raw()); @@ -196,63 +133,18 @@ pub fn encode_response(response: BrokerResponse) -> Vec { encoder.finish() } -fn encode_core_response(encoder: &mut Encoder, response: CoreResponse) { - encoder.u8(RESPONSE_TAG_CORE); - match response { - CoreResponse::Event(response) => { - encoder.u8(CORE_RESPONSE_TAG_EVENT); - encode_event_response(encoder, response); - } - } -} - -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); - encoder.readiness(response.readiness); - } - EventResponse::Consume(response) => { - encoder.u8(EVENT_RESPONSE_TAG_CONSUMED); - encoder.u64(response.value); - encoder.readiness(response.readiness); - } - } -} - -fn encode_wait_outcome(encoder: &mut Encoder, outcome: WaitOutcome) { - match outcome { - WaitOutcome::Ready(readiness) => { - encoder.u8(WAIT_OUTCOME_TAG_READY); - encoder.readiness(readiness); - } - WaitOutcome::WouldBlock(readiness) => { - encoder.u8(WAIT_OUTCOME_TAG_WOULD_BLOCK); - encoder.readiness(readiness); - } - } -} - /// 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: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + broker_protocol_version: decoder.protocol_version()?, }, RESPONSE_TAG_VERSION_MISMATCH => BrokerResponse::VersionMismatch { - broker_protocol_version: ProtocolVersion::new(decoder.u16()?, decoder.u16()?), + broker_protocol_version: decoder.protocol_version()?, }, - RESPONSE_TAG_CORE => match decode_core_response(&mut decoder)? { + RESPONSE_TAG_CORE => match core_message::decode_core_response(&mut decoder)? { Some(response) => BrokerResponse::Core(response), None => return Ok(ReceivedBrokerResponse::Unknown), }, @@ -266,178 +158,15 @@ pub fn decode_response(frame: &[u8]) -> Result) -> Result, WireError> { - let response = match decoder.u8()? { - CORE_RESPONSE_TAG_EVENT => match decode_event_response(decoder)? { - Some(response) => CoreResponse::Event(response), - None => return Ok(None), - }, - _ => return Ok(None), - }; - - Ok(Some(response)) -} - -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(decoder.readiness()?)), - EVENT_RESPONSE_TAG_CONSUMED => EventResponse::Consume(ConsumeEventResponse::new( - decoder.u64()?, - decoder.readiness()?, - )), - _ => return Ok(None), - }; - - Ok(Some(response)) -} - -fn decode_wait_outcome(decoder: &mut Decoder<'_>) -> Result, WireError> { - match decoder.u8()? { - WAIT_OUTCOME_TAG_READY => Ok(Some(WaitOutcome::Ready(decoder.readiness()?))), - WAIT_OUTCOME_TAG_WOULD_BLOCK => Ok(Some(WaitOutcome::WouldBlock(decoder.readiness()?))), - _ => Ok(None), - } -} - -fn encode_event_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_event_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), - } -} - -#[derive(Default)] -struct Encoder { - bytes: Vec, -} - -impl Encoder { - fn finish(self) -> Vec { - self.bytes - } - - fn bool(&mut self, value: bool) { - self.u8(u8::from(value)); - } - - fn u8(&mut self, value: u8) { - self.bytes.push(value); - } - - fn u16(&mut self, value: u16) { - self.bytes.extend_from_slice(&value.to_le_bytes()); - } - - fn u64(&mut self, value: u64) { - self.bytes.extend_from_slice(&value.to_le_bytes()); - } - - fn handle(&mut self, handle: ObjectHandle) { - self.u64(handle.reference_id.get()); - self.u64(handle.reference_generation.get()); - } - - fn readiness(&mut self, readiness: ReadinessState) { - self.bool(readiness.read_ready); - self.bool(readiness.write_ready); - self.u64(readiness.generation); - } -} - -struct Decoder<'a> { - bytes: &'a [u8], - offset: usize, -} - -impl<'a> Decoder<'a> { - const fn new(bytes: &'a [u8]) -> Self { - Self { bytes, offset: 0 } - } - - fn finish(&self) -> Result<(), WireError> { - if self.offset == self.bytes.len() { - Ok(()) - } else { - Err(WireError::TrailingBytes) - } - } - - fn bool(&mut self) -> Result { - match self.u8()? { - 0 => Ok(false), - 1 => Ok(true), - _ => Err(WireError::InvalidBoolean), - } - } - - fn u8(&mut self) -> Result { - let bytes = self.take(1)?; - Ok(bytes[0]) - } - - fn u16(&mut self) -> Result { - let bytes = self.take(2)?; - Ok(u16::from_le_bytes([bytes[0], bytes[1]])) - } - - 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], - ])) - } - - 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 readiness(&mut self) -> Result { - Ok(ReadinessState::new(self.bool()?, self.bool()?, self.u64()?)) - } - - 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) - } -} - #[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() { @@ -559,7 +288,7 @@ mod tests { } #[test] - fn readiness_response_wire_shape_is_pinned() { + 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) 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) + } +} From 4063b187e4f22100dc12c7158e1e0f92e93bfe30 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 14:22:41 -0700 Subject: [PATCH 58/66] Prune Unix socket transport tests Remove redundant timeout and deadline transport tests while keeping framing coverage and a single wall-clock partial-frame timeout case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_broker_transport/src/unix_socket.rs | 66 --------------------- 1 file changed, 66 deletions(-) diff --git a/litebox_broker_transport/src/unix_socket.rs b/litebox_broker_transport/src/unix_socket.rs index 4271bd955..766896ba9 100644 --- a/litebox_broker_transport/src/unix_socket.rs +++ b/litebox_broker_transport/src/unix_socket.rs @@ -349,24 +349,6 @@ mod tests { ); } - #[test] - fn local_response_read_honors_io_timeout() { - let (local_stream, _host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - channel - .set_io_timeout(Some(Duration::from_millis(10))) - .unwrap(); - - let error = channel.recv_response().unwrap_err(); - assert!( - matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ), - "unexpected timeout error kind: {error:?}" - ); - } - #[test] fn local_response_read_io_timeout_is_wall_clock() { let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); @@ -393,52 +375,4 @@ mod tests { "unexpected timeout error kind: {error:?}" ); } - - #[test] - fn local_response_read_honors_io_deadline() { - let (mut host_stream, local_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamLocalControlChannel::from_connected(local_stream); - channel - .set_io_deadline(Some(Instant::now() + Duration::from_millis(20))) - .unwrap(); - - let reader = std::thread::spawn(move || channel.recv_response().unwrap_err()); - host_stream.write_all(&8u32.to_le_bytes()).unwrap(); - - let error = reader.join().expect("deadline reader panicked"); - assert!( - matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ), - "unexpected deadline error kind: {error:?}" - ); - } - - #[test] - fn host_request_read_io_deadline_is_wall_clock() { - let (mut local_stream, host_stream) = UnixStream::pair().unwrap(); - let mut channel = UnixStreamHostControlChannel::from_accepted(host_stream); - channel - .set_io_deadline(Some(Instant::now() + Duration::from_millis(50))) - .unwrap(); - - let reader = std::thread::spawn(move || channel.recv_request().unwrap_err()); - local_stream.write_all(&8u32.to_le_bytes()).unwrap(); - for _ in 0..8 { - std::thread::sleep(Duration::from_millis(20)); - if local_stream.write_all(&[0]).is_err() { - break; - } - } - - let error = reader.join().expect("deadline reader panicked"); - assert!( - matches!( - error.kind(), - io::ErrorKind::WouldBlock | io::ErrorKind::TimedOut - ), - "unexpected deadline error kind: {error:?}" - ); - } } From 84e8588e65ba6c380cce1c1ee1549b4d8f01d1ac Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 14:53:06 -0700 Subject: [PATCH 59/66] Clean up broker local integration layering Simplify the userland broker entry point, move core-request envelope handling into broker-local/LiteBox layers, and keep the Linux runner focused on endpoint setup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 2 + litebox/Cargo.toml | 1 + litebox/src/broker/mod.rs | 52 ++++++++- litebox/src/litebox.rs | 19 +++- litebox_broker_local/src/lib.rs | 46 ++++++-- litebox_broker_userland/Cargo.toml | 1 + litebox_broker_userland/src/main.rs | 120 +++----------------- litebox_runner_linux_userland/src/broker.rs | 45 +------- litebox_runner_linux_userland/src/lib.rs | 10 +- 9 files changed, 135 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5333ec06c..01fde3376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1460,6 +1460,7 @@ dependencies = [ "buddy_system_allocator", "either", "hashbrown", + "litebox_broker_local", "litebox_broker_protocol", "litebox_util_log", "rangemap", @@ -1512,6 +1513,7 @@ dependencies = [ name = "litebox_broker_userland" version = "0.1.0" dependencies = [ + "clap", "litebox_broker_core", "litebox_broker_host", "litebox_broker_local", diff --git a/litebox/Cargo.toml b/litebox/Cargo.toml index 945b6f447..6e8c3ecfe 100644 --- a/litebox/Cargo.toml +++ b/litebox/Cargo.toml @@ -20,6 +20,7 @@ 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] diff --git a/litebox/src/broker/mod.rs b/litebox/src/broker/mod.rs index ad2e5b43c..e3edc541f 100644 --- a/litebox/src/broker/mod.rs +++ b/litebox/src/broker/mod.rs @@ -3,9 +3,10 @@ use alloc::sync::Arc; -use litebox_broker_protocol::{CoreRequest, CoreResponse}; +use litebox_broker_local::{BrokerLocal, BrokerLocalError}; +use litebox_broker_protocol::{CoreRequest, CoreResponse, LocalControlChannel}; -use crate::sync::RawSyncPrimitivesProvider; +use crate::sync::{Mutex, RawSyncPrimitivesProvider}; pub(crate) mod error; pub use error::BrokerControlError; @@ -23,6 +24,53 @@ pub trait BrokerControl: Send + Sync { ) -> 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, diff --git a/litebox/src/litebox.rs b/litebox/src/litebox.rs index 05a2a3f33..35cca9239 100644 --- a/litebox/src/litebox.rs +++ b/litebox/src/litebox.rs @@ -5,8 +5,11 @@ use alloc::sync::Arc; +use litebox_broker_local::BrokerLocal; +use litebox_broker_protocol::LocalControlChannel; + use crate::{ - broker::{BrokerControl, BrokerState}, + broker::{self, BrokerControl, BrokerState}, fd::Descriptors, sync::{RawSyncPrimitivesProvider, RwLock}, }; @@ -42,6 +45,20 @@ impl LiteBox { 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>, diff --git a/litebox_broker_local/src/lib.rs b/litebox_broker_local/src/lib.rs index ab73e93c8..610cd6512 100644 --- a/litebox_broker_local/src/lib.rs +++ b/litebox_broker_local/src/lib.rs @@ -16,8 +16,8 @@ mod error; mod event; use litebox_broker_protocol::{ - BrokerRequest, BrokerResponse, INITIAL_PROTOCOL_VERSION, LocalControlChannel, ProtocolVersion, - ReceivedBrokerResponse, + BrokerRequest, BrokerResponse, CoreRequest, CoreResponse, INITIAL_PROTOCOL_VERSION, + LocalControlChannel, ProtocolVersion, ReceivedBrokerResponse, }; pub use error::{BrokerLocalError, Result}; @@ -151,13 +151,13 @@ impl BrokerLocal { } } - /// Sends one request on an active connection and returns the raw protocol response. - pub fn active_raw_request( - &mut self, - request: BrokerRequest, - ) -> Result { + /// Sends one BrokerCore request on an active connection. + pub fn active_core_request(&mut self, request: CoreRequest) -> Result { self.ensure_negotiated()?; - self.raw_request(request) + match self.request(BrokerRequest::Core(request))? { + BrokerResponse::Core(response) => Ok(response), + response => Err(BrokerLocalError::UnexpectedResponse(response)), + } } } @@ -217,6 +217,36 @@ mod tests { 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, diff --git a/litebox_broker_userland/Cargo.toml b/litebox_broker_userland/Cargo.toml index cad04fa92..8e3502f1c 100644 --- a/litebox_broker_userland/Cargo.toml +++ b/litebox_broker_userland/Cargo.toml @@ -4,6 +4,7 @@ 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" } diff --git a/litebox_broker_userland/src/main.rs b/litebox_broker_userland/src/main.rs index 5ee05f6ed..d3c91f578 100644 --- a/litebox_broker_userland/src/main.rs +++ b/litebox_broker_userland/src/main.rs @@ -1,118 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use std::env; -use std::fs; -use std::io; -use std::os::unix::fs::FileTypeExt; +use std::error::Error; use std::os::unix::net::UnixListener; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::{Duration, Instant}; +use clap::Parser; use litebox_broker_core::{BrokerCore, PolicyEngine}; -use litebox_broker_host::{BrokerHostError, serve_connection}; +use litebox_broker_host::serve_connection; use litebox_broker_transport::unix_socket::UnixStreamHostControlChannel; const SESSION_TIMEOUT: Duration = Duration::from_secs(5); -fn main() -> io::Result<()> { - let args = Args::parse(env::args().skip(1))?; - let listener = bind_listener(&args.socket_path)?; - let _socket_cleanup = SocketPathCleanup::new(args.socket_path.clone()); +#[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()).map_err(io::Error::other)?; - serve_connection(&mut broker, &mut channel) - .map(|_| ()) - .map_err(broker_error) -} - -fn bind_listener(socket_path: &Path) -> io::Result { - match UnixListener::bind(socket_path) { - Ok(listener) => Ok(listener), - Err(error) if error.kind() == io::ErrorKind::AddrInUse => { - remove_stale_socket(socket_path)?; - UnixListener::bind(socket_path) - } - Err(error) => Err(error), - } -} - -fn remove_stale_socket(socket_path: &Path) -> io::Result<()> { - let metadata = fs::symlink_metadata(socket_path)?; - if !metadata.file_type().is_socket() { - return Err(io::Error::new( - io::ErrorKind::AlreadyExists, - "broker socket path exists and is not a socket", - )); - } - - match std::os::unix::net::UnixStream::connect(socket_path) { - Ok(_stream) => Err(io::Error::new( - io::ErrorKind::AddrInUse, - "broker socket path is already accepting connections", - )), - Err(error) if error.kind() == io::ErrorKind::ConnectionRefused => { - fs::remove_file(socket_path) - } - Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), - Err(error) => Err(error), - } -} - -struct SocketPathCleanup { - socket_path: PathBuf, -} - -impl SocketPathCleanup { - fn new(socket_path: PathBuf) -> Self { - Self { socket_path } - } -} - -impl Drop for SocketPathCleanup { - fn drop(&mut self) { - let _ = fs::remove_file(&self.socket_path); - } -} - -fn broker_error(error: BrokerHostError) -> io::Error { - match error { - BrokerHostError::AssociationSetup => io::Error::other("broker association setup failed"), - BrokerHostError::Channel(error) => error, - error => io::Error::other(error.to_string()), - } -} - -struct Args { - socket_path: PathBuf, -} - -impl Args { - fn parse(args: impl IntoIterator) -> io::Result { - let mut socket_path = None; - let mut args = args.into_iter(); - - while let Some(arg) = args.next() { - match arg.as_str() { - "--socket" => { - let path = args.next().ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidInput, "--socket requires a path") - })?; - socket_path = Some(PathBuf::from(path)); - } - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "unknown command-line argument", - )); - } - } - } - - let socket_path = socket_path - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "--socket is required"))?; - Ok(Self { socket_path }) - } + let mut broker = BrokerCore::new(PolicyEngine::event_only())?; + serve_connection(&mut broker, &mut channel)?; + Ok(()) } diff --git a/litebox_runner_linux_userland/src/broker.rs b/litebox_runner_linux_userland/src/broker.rs index 4586bbd58..7634d861a 100644 --- a/litebox_runner_linux_userland/src/broker.rs +++ b/litebox_runner_linux_userland/src/broker.rs @@ -3,16 +3,12 @@ use std::{ path::Path, - sync::Mutex, thread, time::{Duration, Instant}, }; -use alloc::sync::Arc; use anyhow::{Context as _, Result}; -use litebox::{BrokerControl, BrokerControlError}; use litebox_broker_local::BrokerLocal; -use litebox_broker_protocol::{BrokerRequest, BrokerResponse, CoreRequest, CoreResponse}; use litebox_broker_transport::unix_socket::UnixStreamLocalControlChannel; const SETUP_TIMEOUT: Duration = Duration::from_secs(5); @@ -21,11 +17,7 @@ const RETRY_DELAY: Duration = Duration::from_millis(20); type Local = BrokerLocal; pub(crate) struct BrokerConnection { - control: Arc, -} - -struct BrokerLocalControl { - local: Mutex, + local: Local, } pub(crate) fn connect(socket_path: Option<&Path>) -> Result> { @@ -36,35 +28,8 @@ pub(crate) fn connect(socket_path: Option<&Path>) -> Result Arc { - self.control.clone() - } -} - -impl BrokerLocalControl { - fn new(local: Local) -> Self { - Self { - local: Mutex::new(local), - } - } -} - -impl BrokerControl for BrokerLocalControl { - fn request( - &self, - request: CoreRequest, - ) -> core::result::Result { - match self - .local - .lock() - .map_err(|_| BrokerControlError::Transport)? - .active_raw_request(BrokerRequest::Core(request)) - .map_err(|_| BrokerControlError::Transport)? - { - BrokerResponse::Core(response) => Ok(response), - BrokerResponse::Error(error) => Err(BrokerControlError::Broker(error)), - _ => Err(BrokerControlError::UnexpectedResponse), - } + pub(crate) fn into_local(self) -> Local { + self.local } } @@ -76,9 +41,7 @@ fn connect_to_endpoint(socket_path: &Path) -> Result { .control_channel_mut() .set_io_timeout(Some(ACTIVE_REQUEST_TIMEOUT)) .context("failed to configure broker active request timeout")?; - Ok(BrokerConnection { - control: Arc::new(BrokerLocalControl::new(local)), - }) + Ok(BrokerConnection { local }) } fn connect_with_retry(socket_path: &Path, setup_deadline: Instant) -> Result { diff --git a/litebox_runner_linux_userland/src/lib.rs b/litebox_runner_linux_userland/src/lib.rs index 40be86e5e..23e866c58 100644 --- a/litebox_runner_linux_userland/src/lib.rs +++ b/litebox_runner_linux_userland/src/lib.rs @@ -214,11 +214,11 @@ pub fn run(cli_args: CliArgs) -> Result<()> { litebox_platform_multiplex::set_platform(platform); let broker_connection = broker::connect(cli_args.broker_socket.as_deref())?; - let shim_builder = if let Some(broker_connection) = &broker_connection { + let shim_builder = if let Some(broker_connection) = broker_connection { litebox_shim_linux::LinuxShimBuilder::new_with_litebox( - litebox::LiteBox::new_with_broker_control( + litebox::LiteBox::new_with_broker_local( litebox_platform_multiplex::platform(), - broker_connection.control(), + broker_connection.into_local(), ), ) } else { @@ -436,9 +436,7 @@ pub fn run(cli_args: CliArgs) -> Result<()> { shutdown.store(true, core::sync::atomic::Ordering::Relaxed); net_worker.join().unwrap(); } - let exit_status = program.process.wait(); - drop(broker_connection); - std::process::exit(exit_status) + std::process::exit(program.process.wait()) } /// Pin the current thread to a specific CPU core From 928acfa760d2c8cc9f31ee7fcd2743a0653ce47e Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 14:59:04 -0700 Subject: [PATCH 60/66] Expand broker eventfd fixture coverage Cover poll readiness transitions and nonblocking counter saturation in the broker-backed eventfd runner fixture. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/eventfd.c | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 7194e28f6..702032486 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -2,6 +2,7 @@ // Licensed under the MIT license. #include +#include #include #include #include @@ -29,6 +30,22 @@ static int read_value(int fd, uint64_t expected) { 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 writev_values(int fd, uint64_t first, uint64_t second) { struct iovec iov[2] = { {&first, sizeof(first)}, @@ -75,72 +92,116 @@ static int expect_einval_split_writev(int fd) { return errno == EINVAL ? 0 : 2; } +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; +} + int main(void) { int fd = eventfd(0, EFD_NONBLOCK); if (fd < 0) { return 10; } - if (expect_eagain_read(fd) != 0) { + if (expect_poll_events(fd, POLLOUT) != 0) { return 11; } - if (write_value(fd, 3) != 0) { + if (expect_eagain_read(fd) != 0) { return 12; } - if (read_value(fd, 3) != 0) { + if (write_value(fd, 3) != 0) { return 13; } - if (writev_values(fd, 2, 5) != 0) { + if (expect_poll_events(fd, POLLIN | POLLOUT) != 0) { return 14; } - if (read_value(fd, 7) != 0) { + if (read_value(fd, 3) != 0) { return 15; } - if (write_value(fd, 9) != 0) { + if (expect_poll_events(fd, POLLOUT) != 0) { return 16; } - if (readv_split_value(fd, 9) != 0) { + if (writev_values(fd, 2, 5) != 0) { return 17; } - if (expect_einval_split_writev(fd) != 0) { + if (read_value(fd, 7) != 0) { return 18; } - if (write_value(fd, 11) != 0) { + if (write_value(fd, 9) != 0) { return 19; } - if (expect_einval_short_readv(fd) != 0) { + if (readv_split_value(fd, 9) != 0) { return 20; } - if (read_value(fd, 11) != 0) { + if (expect_einval_split_writev(fd) != 0) { return 21; } - if (expect_eagain_read(fd) != 0) { + if (write_value(fd, 11) != 0) { return 22; } + if (expect_einval_short_readv(fd) != 0) { + return 23; + } + if (read_value(fd, 11) != 0) { + return 24; + } + if (expect_eagain_read(fd) != 0) { + return 25; + } uint64_t invalid = UINT64_MAX; errno = 0; if (write(fd, &invalid, sizeof(invalid)) != -1 || errno != EINVAL) { - return 23; + return 26; + } + if (write_value(fd, UINT64_MAX - 1) != 0) { + return 27; + } + if (expect_poll_events(fd, POLLIN) != 0) { + return 28; + } + if (expect_eagain_write(fd, 1) != 0) { + return 29; + } + if (read_value(fd, UINT64_MAX - 1) != 0) { + return 30; + } + if (expect_poll_events(fd, POLLOUT) != 0) { + return 31; } close(fd); int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); if (semaphore_fd < 0) { - return 30; + return 40; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 41; } if (write_value(semaphore_fd, 3) != 0) { - return 31; + return 42; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 43; } if (read_value(semaphore_fd, 1) != 0) { - return 32; + return 44; + } + if (expect_poll_events(semaphore_fd, POLLIN | POLLOUT) != 0) { + return 45; } if (read_value(semaphore_fd, 1) != 0) { - return 33; + return 46; } if (read_value(semaphore_fd, 1) != 0) { - return 34; + return 47; + } + if (expect_poll_events(semaphore_fd, POLLOUT) != 0) { + return 48; } if (expect_eagain_read(semaphore_fd) != 0) { - return 35; + return 49; } close(semaphore_fd); From fe921838a5c09e4ccdadcdd6853b513825c9b433 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 15:13:17 -0700 Subject: [PATCH 61/66] Assert broker eventfd traffic in runner test Count event requests observed by the test broker so the broker-backed eventfd integration test proves eventfd operations used the broker. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/run.rs | 75 +++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 344ff5ec8..912a523f0 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -263,11 +263,18 @@ fn unique_test_socket_path(name: &str) -> PathBuf { 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) @@ -298,6 +305,7 @@ fn spawn_test_broker( 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 || { @@ -318,14 +326,18 @@ fn spawn_test_broker( stream .set_write_timeout(Some(BROKER_HELPER_TIMEOUT)) .expect("failed to configure broker test write timeout"); - let mut channel = - litebox_broker_transport::unix_socket::UnixStreamHostControlChannel::from_accepted(stream); + 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); @@ -341,10 +353,67 @@ fn spawn_test_broker( 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() { @@ -360,10 +429,12 @@ fn test_runner_broker_integration_with_rewriter() { 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(); } From 756edcdeeb4867ee7ca57539c1e521f4b2c7fc64 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Fri, 5 Jun 2026 16:21:28 -0700 Subject: [PATCH 62/66] Fix shim eventfd and vectored IO semantics Enable broker-backed event counters to support blocking waits so eventfd status flags can clear nonblocking mode. Special-case pipe and socket readv to perform one backend read, and coalesce small pipe writev calls to preserve PIPE_BUF atomicity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox/src/event/counter.rs | 34 ++- litebox_runner_linux_userland/tests/eventfd.c | 39 +++ litebox_shim_linux/src/syscalls/eventfd.rs | 7 +- litebox_shim_linux/src/syscalls/file.rs | 287 ++++++++++++++++-- litebox_shim_linux/src/syscalls/net.rs | 51 +++- litebox_shim_linux/src/syscalls/pipe.rs | 5 +- 6 files changed, 382 insertions(+), 41 deletions(-) diff --git a/litebox/src/event/counter.rs b/litebox/src/event/counter.rs index 3d522f417..c6bbfe447 100644 --- a/litebox/src/event/counter.rs +++ b/litebox/src/event/counter.rs @@ -73,7 +73,7 @@ where broker, handle: response.handle, pollee: Pollee::new(), - blocking_operations_supported: false, + blocking_operations_supported: true, }) } @@ -85,32 +85,36 @@ where /// Reads the event counter. pub fn read( &self, - _cx: &WaitContext<'_, Platform>, - _nonblock: bool, + cx: &WaitContext<'_, Platform>, + nonblock: bool, mode: EventCounterReadMode, ) -> Result> { - let response = map_broker_object_result(self.consume(mode))?; - if response.readiness.write_ready { - self.pollee.notify_observers(Events::OUT); - } - Ok(response.value) + 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, + cx: &WaitContext<'_, Platform>, + nonblock: bool, value: u64, ) -> Result> { if value == u64::MAX { return Err(TryOpError::Other(EventCounterError::InvalidInput)); } - 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::()) + 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( diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index 702032486..e243f54b1 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -2,10 +2,12 @@ // Licensed under the MIT license. #include +#include #include #include #include #include +#include #include #include @@ -100,6 +102,19 @@ static int expect_eagain_write(int fd, uint64_t value) { return errno == EAGAIN ? 0 : 2; } +static int clear_nonblock_with_fcntl(int fd) { + int flags = fcntl(fd, F_GETFL); + if (flags < 0) { + return 1; + } + return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) == 0 ? 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) { @@ -172,6 +187,30 @@ int main(void) { } close(fd); + int fcntl_toggle_fd = eventfd(1, EFD_NONBLOCK); + if (fcntl_toggle_fd < 0) { + return 32; + } + if (clear_nonblock_with_fcntl(fcntl_toggle_fd) != 0) { + return 33; + } + if (read_value(fcntl_toggle_fd, 1) != 0) { + return 34; + } + close(fcntl_toggle_fd); + + int ioctl_toggle_fd = eventfd(1, EFD_NONBLOCK); + if (ioctl_toggle_fd < 0) { + return 35; + } + if (clear_nonblock_with_ioctl(ioctl_toggle_fd) != 0) { + return 36; + } + if (read_value(ioctl_toggle_fd, 1) != 0) { + return 37; + } + close(ioctl_toggle_fd); + int semaphore_fd = eventfd(0, EFD_NONBLOCK | EFD_SEMAPHORE); if (semaphore_fd < 0) { return 40; diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 78f8dc531..5bd466d63 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -30,9 +30,10 @@ impl FdEnabledSubsystemEntry for EventFile {} /// Backing counter for a Linux eventfd file description. /// -/// Blocking eventfd still uses the shim-local implementation because the -/// local-core event counter does not support blocking operations yet. Once it -/// does, this split can collapse to the local-core counter. +/// 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, diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index f8346cef5..427a2fb7f 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -7,6 +7,7 @@ use alloc::{ ffi::CString, string::{String, ToString as _}, vec, + vec::Vec, }; use litebox::{ event::{Events, wait::WaitError}, @@ -25,7 +26,7 @@ use litebox_common_linux::{ use litebox_platform_multiplex::Platform; use thiserror::Error; -use crate::{ConstPtr, GlobalState, MutPtr, ShimFS, Task, syscalls::signal}; +use crate::{ConstPtr, GlobalState, MAX_KERNEL_BUF_SIZE, MutPtr, ShimFS, Task, syscalls::signal}; use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Clone, Copy)] @@ -901,8 +902,23 @@ impl Task { .run_on_raw_fd( raw_fd, |_fd| Ok(None), - |_fd| Ok(None), - |_fd| Ok(None), + |fd| { + let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; + read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { + self.global.receive( + &self.wait_cx(), + fd, + buf, + litebox_common_linux::ReceiveFlags::empty(), + None, + ) + }) + .map(Some) + }, + |fd| { + read_linux_pipe_into_iovec(&self.global, &self.wait_cx(), fd, iovs) + .map(Some) + }, |fd| { let total_len = eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; @@ -918,12 +934,32 @@ impl Task { .ok_or(Errno::EBADF)?; handle.with_entry(|file| { let value = file.read(&self.wait_cx())?; - copy_eventfd_value_to_iovec(iovs, value)?; + read_eventfd_value_to_iovec(iovs, value)?; Ok(Some(8)) }) }, |_fd| Ok(None), - |_fd| Ok(None), + |fd| { + let handle = self + .global + .litebox + .descriptor_table() + .entry_handle(fd) + .ok_or(Errno::EBADF)?; + handle + .with_entry(|file| { + let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; + read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { + file.recvfrom( + &self.wait_cx(), + buf, + litebox_common_linux::ReceiveFlags::empty(), + None, + ) + }) + }) + .map(Some) + }, ) .flatten()? { @@ -972,6 +1008,10 @@ fn check_iovcnt(iovcnt: usize) -> Result<(), Errno> { } fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno> { + iovec_total_len(iov_lens).map(|_| ()) +} + +fn iovec_total_len(iov_lens: impl IntoIterator) -> Result { let mut total = 0usize; for iov_len in iov_lens { total = total.checked_add(iov_len).ok_or(Errno::EINVAL)?; @@ -979,18 +1019,11 @@ fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno return Err(Errno::EINVAL); } } - Ok(()) + Ok(total) } fn eventfd_iovec_total_len(iov_lens: impl IntoIterator) -> Result { - let mut total_len = 0usize; - for iov_len in iov_lens { - total_len = total_len.checked_add(iov_len).ok_or(Errno::EINVAL)?; - if total_len > SSIZE_MAX { - return Err(Errno::EINVAL); - } - } - Ok(total_len) + iovec_total_len(iov_lens) } fn validate_eventfd_iovec_len(total_len: usize) -> Result<(), Errno> { @@ -1000,7 +1033,7 @@ fn validate_eventfd_iovec_len(total_len: usize) -> Result<(), Errno> { Ok(()) } -fn copy_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Result<(), Errno> { +fn read_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Result<(), Errno> { validate_eventfd_iovec_len(eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?)?; let bytes = value.to_ne_bytes(); @@ -1021,7 +1054,7 @@ fn copy_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Re Err(Errno::EINVAL) } -fn write_eventfd_iovec( +fn write_eventfd_from_iovec( iovs: &[IoWriteVec>], mut write_value: F, ) -> Result @@ -1112,6 +1145,82 @@ where Ok(total_read) } +/// Performs one backend read and scatters the returned bytes into user iovecs. +fn read_once_into_iovec( + iovs: &[IoReadVec

], + kernel_buffer: &mut [u8], + mut read_fn: F, +) -> Result +where + P: RawMutPointer, + F: FnMut(&mut [u8]) -> Result, +{ + let total_len = iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; + if total_len == 0 { + return Ok(0); + } + + let read_len = total_len.min(kernel_buffer.len()); + let size = read_fn(&mut kernel_buffer[..read_len])?.min(read_len); + scatter_buffer_to_iovec(iovs, &kernel_buffer[..size]) +} + +fn scatter_buffer_to_iovec

(iovs: &[IoReadVec

], data: &[u8]) -> Result +where + P: RawMutPointer, +{ + let mut copied = 0; + for iov in iovs { + if copied == data.len() { + return Ok(copied); + } + if iov.iov_len == 0 { + continue; + } + let size = (data.len() - copied).min(iov.iov_len); + if iov + .iov_base + .copy_from_slice(0, &data[copied..copied + size]) + .is_none() + { + return if copied > 0 { + Ok(copied) + } else { + Err(Errno::EFAULT) + }; + } + copied += size; + } + if copied == data.len() || copied > 0 { + Ok(copied) + } else { + Err(Errno::EFAULT) + } +} + +fn read_iovec_buffer_len

(iovs: &[IoReadVec

]) -> Result +where + P: RawMutPointer, +{ + Ok(iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?.min(MAX_KERNEL_BUF_SIZE)) +} + +fn read_linux_pipe_into_iovec( + global: &GlobalState, + cx: &litebox::event::wait::WaitContext<'_, Platform>, + fd: &TypedFd>, + iovs: &[IoReadVec

], +) -> Result +where + P: RawMutPointer, + FS: ShimFS, +{ + let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; + read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { + global.read_linux_pipe(cx, fd, buf) + }) +} + /// Drain writes from a sequence of user iovecs. /// /// `write_fn` receives the contents of each iovec along with the total number of @@ -1162,6 +1271,46 @@ where Ok(total_written) } +fn write_linux_pipe_from_iovec( + global: &GlobalState, + cx: &litebox::event::wait::WaitContext<'_, Platform>, + fd: &TypedFd>, + iovs: &[IoWriteVec

], +) -> Result +where + P: RawConstPointer, + FS: ShimFS, +{ + let total_len = iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; + if total_len == 0 { + return Ok(0); + } + if total_len <= super::pipe::LINUX_PIPE_BUF { + let buffer = gather_iovec_to_buffer(iovs, total_len)?; + return global.write_linux_pipe(cx, fd, &buffer); + } + + write_to_iovec(iovs, |buf, _total| global.write_linux_pipe(cx, fd, buf)) +} + +fn gather_iovec_to_buffer

(iovs: &[IoWriteVec

], total_len: usize) -> Result, Errno> +where + P: RawConstPointer, +{ + let mut buffer = Vec::with_capacity(total_len); + for iov in iovs { + if iov.iov_len == 0 { + continue; + } + let Some(slice) = iov.iov_base.to_owned_slice(iov.iov_len) else { + return Err(Errno::EFAULT); + }; + buffer.extend_from_slice(slice.as_ref()); + } + debug_assert_eq!(buffer.len(), total_len); + Ok(buffer) +} + impl Task { /// Handle syscall `writev` pub(crate) fn sys_writev( @@ -1185,7 +1334,10 @@ impl Task { raw_fd, |_fd| Ok(None), |_fd| Ok(None), - |_fd| Ok(None), + |fd| { + write_linux_pipe_from_iovec(&self.global, &self.wait_cx(), fd, iovs) + .map(Some) + }, |fd| { let handle = self .global @@ -1193,7 +1345,7 @@ impl Task { .descriptor_table() .entry_handle(fd) .ok_or(Errno::EBADF)?; - write_eventfd_iovec(iovs, |value| { + write_eventfd_from_iovec(iovs, |value| { handle.with_entry(|file| file.write(&self.wait_cx(), value)) }) .map(Some) @@ -2773,6 +2925,105 @@ mod tests { assert_eq!(&output, b"hello pipe"); } + #[test] + fn small_nonblocking_pipe_writev_is_atomic() { + let task = crate::syscalls::tests::init_platform(None); + let (read_fd, write_fd) = task.sys_pipe2(OFlags::NONBLOCK).unwrap(); + let read_fd = i32::try_from(read_fd).unwrap(); + let write_fd = i32::try_from(write_fd).unwrap(); + let fill = vec![0xaa; 8192]; + + loop { + match task.sys_write(write_fd, &fill, None) { + Ok(0) => panic!("pipe write made no progress"), + Ok(_written) => {} + Err(Errno::EAGAIN) => break, + Err(error) => panic!("unexpected pipe fill error: {error:?}"), + } + } + + let mut slack = [0; 3]; + assert_eq!(task.sys_read(read_fd, &mut slack, None), Ok(slack.len())); + + let first = b"ab"; + let second = b"cd"; + let iovs = [ + IoWriteVec { + iov_base: ConstPtr::from_ptr(first.as_ptr()), + iov_len: first.len(), + }, + IoWriteVec { + iov_base: ConstPtr::from_ptr(second.as_ptr()), + iov_len: second.len(), + }, + ]; + + assert_eq!( + task.sys_writev(write_fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Err(Errno::EAGAIN) + ); + } + + #[test] + fn read_once_into_iovec_stops_after_one_backend_read() { + let mut first = [0u8; 4]; + let mut second = [0u8; 4]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), + iov_len: first.len(), + }, + IoReadVec { + iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), + iov_len: second.len(), + }, + ]; + let mut kernel_buffer = [0u8; 8]; + let calls = Cell::new(0); + + let result = read_once_into_iovec(&iovs, &mut kernel_buffer, |buf| { + calls.set(calls.get() + 1); + buf[..6].copy_from_slice(b"abcdef"); + Ok(6) + }); + + assert_eq!(result, Ok(6)); + assert_eq!(calls.get(), 1); + assert_eq!(&first, b"abcd"); + assert_eq!(&second, b"ef\0\0"); + } + + #[test] + fn pipe_readv_can_return_more_than_one_page_without_second_backend_read() { + let task = crate::syscalls::tests::init_platform(None); + let (read_fd, write_fd) = task.sys_pipe2(OFlags::NONBLOCK).unwrap(); + let read_fd = i32::try_from(read_fd).unwrap(); + let write_fd = i32::try_from(write_fd).unwrap(); + let input = vec![0x5a; PAGE_SIZE * 2]; + + assert_eq!(task.sys_write(write_fd, &input, None), Ok(input.len())); + + let mut first = vec![0u8; PAGE_SIZE]; + let mut second = vec![0u8; PAGE_SIZE]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), + iov_len: first.len(), + }, + IoReadVec { + iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), + iov_len: second.len(), + }, + ]; + + assert_eq!( + task.sys_readv(read_fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), + Ok(input.len()) + ); + assert_eq!(first, vec![0x5a; PAGE_SIZE]); + assert_eq!(second, vec![0x5a; PAGE_SIZE]); + } + #[test] fn read_from_iovec_breaks_on_eof() { let mut first = [0u8; 4]; diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 997ccb632..685678ead 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -2811,10 +2811,10 @@ mod unix_tests { use core::time::Duration; use alloc::{string::ToString, vec::Vec}; - use litebox::{event::Events, platform::RawConstPointer}; + use litebox::{event::Events, mm::linux::PAGE_SIZE, platform::RawConstPointer}; use litebox_common_linux::{ - AddressFamily, AtFlags, ReceiveFlags, SendFlags, SockFlags, SockType, SocketOption, - SocketOptionName, TimeParam, errno::Errno, + AddressFamily, AtFlags, IoReadVec, ReceiveFlags, SendFlags, SockFlags, SockType, + SocketOption, SocketOptionName, TimeParam, errno::Errno, }; use crate::{ @@ -3290,6 +3290,51 @@ mod unix_tests { unix_socketpair_bidirectional(SockType::Datagram, true); } + #[test] + fn test_unix_datagram_socketpair_readv_spans_iovecs() { + let task = init_platform(None); + let mut sv_ptr = alloc::vec![0u32; 2]; + let sv_mut_ptr = MutPtr::from_usize(sv_ptr.as_mut_ptr() as usize); + let ty_and_flags = SockType::Datagram as u32 | SockFlags::NONBLOCK.bits(); + task.sys_socketpair(AddressFamily::UNIX as u32, ty_and_flags, 0, sv_mut_ptr) + .unwrap(); + + let sender = sv_ptr[0]; + let receiver = sv_ptr[1]; + let input = alloc::vec![0x7b; PAGE_SIZE * 2]; + assert_eq!( + task.do_sendto(sender, &input, SendFlags::empty(), None), + Ok(input.len()) + ); + + let mut first = alloc::vec![0u8; PAGE_SIZE]; + let mut second = alloc::vec![0u8; PAGE_SIZE]; + let iovs = [ + IoReadVec { + iov_base: MutPtr::from_usize(first.as_mut_ptr() as usize), + iov_len: first.len(), + }, + IoReadVec { + iov_base: MutPtr::from_usize(second.as_mut_ptr() as usize), + iov_len: second.len(), + }, + ]; + + assert_eq!( + task.sys_readv( + i32::try_from(receiver).unwrap(), + ConstPtr::from_ptr(iovs.as_ptr()), + iovs.len() + ), + Ok(input.len()) + ); + assert_eq!(first, alloc::vec![0x7b; PAGE_SIZE]); + assert_eq!(second, alloc::vec![0x7b; PAGE_SIZE]); + + close_socket(&task, sender); + close_socket(&task, receiver); + } + fn unix_socket_recv_timeout(ty: SockType) { let task = init_platform(None); let (sock1, _sock2) = task diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index 90273c164..c9d3ff6b5 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -19,6 +19,7 @@ use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; use crate::{GlobalState, Platform, ShimFS}; +pub(crate) const LINUX_PIPE_BUF: usize = 4096; const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; /// Status flags for Linux pipe file descriptions. @@ -56,8 +57,8 @@ impl GlobalState { let (writer, reader) = self.pipes.create_pipe( DEFAULT_PIPE_BUF_SIZE, pipe_flags, - // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. - NonZero::new(4096), + // See `man 7 pipe` for `PIPE_BUF`. + NonZero::new(LINUX_PIPE_BUF), ); let initial_status = OFlags::from(pipe_flags); From f0d9e07b738027a8793848bc5147817baeb7a03c Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 8 Jun 2026 10:30:12 -0700 Subject: [PATCH 63/66] Join epoll eventfd test writer thread Keep the spawned eventfd writer handle and join it after epoll_wait so Windows test processes do not remain alive after the test body reports success. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/epoll.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/epoll.rs b/litebox_shim_linux/src/syscalls/epoll.rs index 50acc45ae..df1aad275 100644 --- a/litebox_shim_linux/src/syscalls/epoll.rs +++ b/litebox_shim_linux/src/syscalls/epoll.rs @@ -661,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 || { @@ -677,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] From 5dcc8fdaa44e4833bb0123703d511369e7acf679 Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 15 Jun 2026 14:02:50 -0700 Subject: [PATCH 64/66] Remove broker design docs from implementation PR The docs now live on wdcui/ulitebox/broker-design-docs. Drop them from the implementation branch so the PR contains only code changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/broker-design.md | 824 ------------------------------------------ docs/impl-plan.md | 398 -------------------- 2 files changed, 1222 deletions(-) delete mode 100644 docs/broker-design.md delete mode 100644 docs/impl-plan.md diff --git a/docs/broker-design.md b/docs/broker-design.md deleted file mode 100644 index 93123e160..000000000 --- a/docs/broker-design.md +++ /dev/null @@ -1,824 +0,0 @@ -# LiteBox Broker Architecture Design - -## Goal - -Enable true multi-process and multi-session LiteBox support while preserving portability across userland and kernel-backed deployments. - -This design is shim-agnostic. A shim can expose a POSIX-like ABI, an OP-TEE-compatible ABI, a Windows-like ABI, or another guest ABI. The broker architecture should not assume any one shim's syscall set, process model, or resource vocabulary. - -The design has two trust domains: - -```text -User mode: - Shim + local core - optional BrokerService clients - -Authority domain: - broker entry/host + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform - broker-kernel user-mode support, in kernel-backed deployments -``` - -The local core reaches local mechanics and broker channels through deployment support selected by the deployment profile: - -| Deployment | Broker location | Deployment support used by local core | -|---|---|---| -| **Userland broker** | privileged broker process | host OS user-mode ABI plus a broker transport endpoint | -| **Kernel broker** | kernel or equivalent trusted domain | broker-kernel user-mode support plus broker-channel delivery without decoding broker requests | - -## Component model - -```text -User mode: - guest workload - | - v - Shim - | - v - local core + optional BrokerService clients - -- via litebox_broker_local over the selected broker channel --> - broker authority interface - -Authority domain: - broker-kernel user-mode support (kernel-backed deployments) - -- carries/classifies channel traffic --> - broker authority interface -> litebox_broker_host - | - v - BrokerCore + optional BrokerServices - | | - | consult | execute after authorization - v v - PolicyEngine BrokerPlatform -``` - -Deployment support supplies two things to user mode: local mechanics that do not create LiteBox authority, and a broker channel. In hosted userland, those are the host OS user-mode ABI plus a broker transport endpoint. In a broker-kernel deployment, they are calls into broker-kernel user-mode support plus broker-channel delivery. - -### Shim - -Runs in user mode. Owns guest ABI mechanics for a particular shim: entry/trap handling, argument decoding, return-value conventions, exception delivery, frame construction, and guest-visible ABI details. - -### Local core - -Runs in user mode. It is the LiteBox runtime below the shim: the component that presents local APIs to shims, manages non-authoritative local state, and turns broker-backed resources into local objects. - -The local core contains: - -- user-facing core APIs used by shims; -- guest pointer and guest memory marshalling helpers; -- local caches and non-authoritative views of broker state; -- private synchronization and wait helpers; -- broker-owned control/notification/data channel wrappers; -- the `litebox_broker_local` adapter; -- internal deployment-support calls for local mechanics. - -The local core is **not trusted** for security. It executes in the same user-mode context as the guest and shim. It may request operations and cache derived state, but it must never create authority. - -The old distinction between user core and user platform can remain as an internal implementation structure if it is useful for code organization. It is not a security boundary and does not need to be reflected as a top-level architectural component. - -The local core should keep its contracts portable instead of assuming `std` everywhere. Hosted adapters may use `std` for threads, synchronization, IPC clients, async runtimes, richer errors, and broker-channel plumbing when the selected deployment permits those facilities. - -### Broker-local adapter - -Thin in-process adapter used by local-core code to call the broker authority interface. - -This adapter is not a separate trust boundary or authority component. It is bundled with the user-mode side and serializes typed calls into the explicit broker protocol over the selected broker channel. - -### Broker-kernel user-mode support - -Future kernel-side support for running the user-mode local core in broker-kernel deployments. - -This code runs in the kernel or equivalent trusted domain. It is part of the broker-kernel deployment, not part of the local core. It provides the execution support that the user-mode local core expects: trap/syscall/upcall entry, private synchronization primitives, anonymous local memory mechanics, thread/process setup mechanics, broker channel endpoints, and any compatibility ABI that the selected local-core profile requires. - -Broker-kernel user-mode support is separate from BrokerCore, PolicyEngine, and BrokerPlatform. It is not the platform implementation used by BrokerCore; it is trusted-domain support that lets user-mode LiteBox processes run and reach the broker. It should not create LiteBox authority by itself. Security-relevant operations must still route through BrokerCore, optional BrokerServices, PolicyEngine, and BrokerPlatform. - -Broker-kernel user-mode support may carry broker authority traffic, but decoding, validation, and dispatch of `BrokerRequest` belong to `litebox_broker_host`, not this support code or BrokerCore. - -In broker-kernel deployments, this support code shares the trusted-domain TCB with BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform. The "no LiteBox authority" rule is a code-organization invariant enforced by review, not a sandboxing boundary; this code is in the TCB and must be audited accordingly. - -### BrokerCore - -Required, shim-neutral trusted core. BrokerCore owns global and shared state: workload identity, process/session/thread identity, handle/resource capabilities, shared object lifetime, wait queues, namespace state, signal/event routing, shared synchronization, and readiness. - -BrokerCore should not bake in every shim's ABI semantics. It provides the authority primitives that shims, broker services, and PolicyEngine build on. BrokerCore enforces structural invariants, such as capability validity and object lifetime, and supplies state/context to PolicyEngine for authorization. - -### BrokerServices - -Optional trusted extensions hosted inside the broker authority domain. - -A BrokerService is useful when a shim or domain has security-relevant semantics that are too specific to belong in BrokerCore. Examples include OP-TEE TA/session authority, secure-storage semantics, POSIX process/signal semantics, filesystem semantics, socket semantics, or another guest ABI's domain-specific resource model. - -BrokerServices should not reinvent authority. They should use BrokerCore capabilities, identities, memory grants, wait queues, lifecycle state, and accounting wherever possible. A BrokerService must not grant or exercise authority over a resource without going through BrokerCore's capability/lifecycle primitives and PolicyEngine authorization, even when its own protocol is shim-specific. - -### PolicyEngine - -Trusted policy decision and audit component inside the broker authority domain. - -PolicyEngine is the broker's reference-monitor component. BrokerCore and BrokerServices gather context, validate structural invariants, and ask PolicyEngine to authorize authority-changing or host-effecting operations before BrokerCore mutates authoritative state, a BrokerService grants domain authority, or BrokerPlatform performs backend execution. - -PolicyEngine should not own all broker state. It consumes facts from BrokerCore and BrokerServices, returns allow/deny decisions plus constraints, and emits audit records. Keeping policy decisions here prevents firewall, filesystem, device, storage, and domain-specific access checks from being hidden in BrokerPlatform or duplicated across BrokerServices. - -### BrokerPlatform - -Trusted backend for privileged operations: address-space control, host I/O, filesystem/device/network access, randomness/secrets, timers, scheduling hooks, host-side execution of PolicyEngine-authorized operations, and platform-specific primitives. - -## Crate layout and naming - -The broker architecture should use crate names that make the authority boundary visible. Start with standalone crates rather than moving the existing `litebox` crate wholesale into the broker. - -| Crate | Initial role | -|---|---| -| `litebox_broker_protocol` | Shared `no_std + alloc` protocol crate for broker-visible DTOs, transport-neutral control-channel contracts, and the current reusable byte codec under `litebox_broker_protocol::wire`: protocol version type and initial version constant, opaque event reference handles, event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, local/host control-channel traits, and request/response message-body encoding. | -| `litebox_broker_core` | Protocol- and channel-independent authority logic: the single broker core constructed for the broker process/kernel lifetime, broker-owned caller associations, object/reference registry, object type and rights authority, reference generations, policy hooks, wait/readiness state, association cleanup, and the first broker-owned event object. It exposes direct domain methods and domain types; it does not decode broker protocol requests or know concrete IPC. | -| `litebox_broker_host` | Shared `no_std` broker-side protocol/core adapter for hosted and kernel broker deployments: free receive/send loop over a caller-owned `BrokerCore` and generic host control channel. It owns protocol negotiation, request sequencing, unknown-tag handling, protocol/core type conversion, peer-credential-to-caller-credential mapping, and typed broker-close reasons. | -| `litebox_broker_transport` | Hosted concrete broker transport implementations. The current implementation is a Unix-domain-socket control channel under `unix_socket`. The crate owns stream framing and channel trait adaptation; it does not assemble a broker deployment or depend on broker core/host crates. | -| `litebox_broker_userland` | Hosted `std` broker executable. This deployment crate wires `BrokerCore`, the current policy, the generic broker host loop, and the Unix-socket transport together. | -| `litebox_broker_local` | `no_std` channel-neutral local-side adapter linked into local-core deployments and runners: negotiate broker protocol, track local negotiation state, sequence request/response pairs, map broker errors, and expose typed broker calls. | -| `litebox` | Local core crate: guest fd table view, syscall/resource routing, local-private mechanics, broker-backed object wrappers, and compatibility-profile glue. | - -`litebox_broker_local` should stay narrow. It is not the syscall classifier and does not implement non-delegable syscall handling. Shim dispatch and the local core decide whether an operation is local-private, broker-delegated, or host-arbitrated; the local adapter only carries broker-delegated operations over the selected control channel. - -Channel delivery must remain replaceable. `litebox_broker_protocol` and `litebox_broker_core` must not depend on Unix-domain sockets, shared-memory rings, traps, hypercalls, or any specific IPC implementation. BrokerCore may reuse shared value DTOs from `litebox_broker_protocol`, but it must not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC. The broker host adapts protocol and channel concepts into direct BrokerCore domain calls. Shared control-channel traits live in `litebox_broker_protocol::channel`; concrete IPC implementations, such as `litebox_broker_transport` or a later shared-memory ring crate, live outside the broker deployment crate without changing broker object semantics. - -The current control-channel contract is deliberately serial: one broker authority request is in flight on a connection, and the next request waits for the matching response. A future shared-memory ring or multiplexed transport can either keep that semantic contract behind a blocking adapter, or introduce protocol correlation IDs in a later extension if concurrent in-flight control operations become necessary. - -Shared broker DTOs and the current wire codec live in `litebox_broker_protocol` to avoid repeating request/response, handle/readiness, and message-body encoding shapes across protocol, core, local, transport, and host code. BrokerCore still keeps authority-domain internals private: object IDs, reference storage, rights, policy decisions, and associations remain core-only, while `litebox_broker_host` is the sanctioned mapping boundary for protocol envelopes and channel outcomes. - -Control-channel traits model only the paired broker authority request/response lane. Broker-initiated notifications such as readiness changes, interrupts, faults, revocations, or session failure should use a separately named notification channel/message family rather than arriving as unsolicited `BrokerResponse` values on the control channel. The notification lane should mirror the layered protocol style, for example `BrokerNotification::Core(CoreNotification::Object(...))` or a session-level notification family, but it should not reuse response enums because notifications are not replies to local requests. - -Forward-compatible protocol probing is explicit: unknown requests decoded from the wire stay in channel-level receive wrappers rather than entering the known protocol enums, so the generic host can return `UnsupportedOperation` without closing the connection or exposing wire tag width through the channel interface. Structurally malformed frames remain channel/wire errors. BrokerCore only sees supported, already-adapted domain operations. Core errors or wait outcomes that are newer than the host adapter can represent map to the neutral protocol `Internal` error rather than being reported as unsupported operations. - -Negotiation separates the broker's max-supported protocol version from the effective version spoken on a connection. The broker response advertises the max-supported version after accepting a compatible request, while local and host connection state retain the requested effective version; all future feature gating should use the effective negotiated version. Version mismatches report the broker-supported version without closing the connection, so local peers can retry with a compatible version on expensive or credentialed channels instead of reconnecting and guessing. - -The known broker protocol keeps the outer envelope intentionally small. Connection-level messages such as negotiation and common errors stay at the broker layer; BrokerCore/object operations are grouped below that layer by authority domain and object family, for example `BrokerRequest::Core(CoreRequest::Event(EventRequest::Wait { .. }))`. New object families should add a nested request/response family instead of growing a flat top-level `BrokerRequest`/`BrokerResponse` operation list. The wire codec may encode those nested families as layered tags, but tag widths and unknown-tag handling remain private to the codec. - -Kernel/trusted deployments will likely link broker-kernel user-mode support and broker-authority pieces into one binary or image, but the code should still preserve their logical separation: - -| Future crate/layer | Role | -|---|---| -| broker-kernel user-mode support | Trusted-domain support for user-mode execution, trap/upcall/channel delivery, process/thread setup, broker-channel endpoints, and the kernel support ABI used by local core. It supplies or adapts a host control channel, but `litebox_broker_host` still owns broker-side protocol/channel adaptation into BrokerCore. | -| `litebox_broker_kernel` | Kernel/trusted-domain deployment wiring for `litebox_broker_core`, `litebox_broker_host`, BrokerServices, PolicyEngine, BrokerPlatform, and broker-kernel user-mode support. | - -These names do not require separate runtime processes. In a kernel-broker deployment, broker-kernel user-mode support, `litebox_broker_host`, BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform can be compiled into one trusted binary. The code boundary still matters: broker-kernel user-mode support carries or classifies traffic, `litebox_broker_host` decodes and sequences broker protocol requests, and BrokerCore validates domain invariants and authorizes domain operations with PolicyEngine. - -## Runtime interfaces - -There are three logical interfaces. In a broker-kernel deployment, the local-core deployment-support interface and the broker authority interface may use the same trap instruction or kernel entry path, but they must remain separate contracts with separate authority rules. When they share a path, broker-kernel user-mode support should only classify and deliver traffic to the host control channel; `litebox_broker_host` decodes and sequences `BrokerRequest` values, while BrokerCore/BrokerServices/PolicyEngine remain responsible for domain authority. - -| Interface | Userland deployment | Kernel deployment | Purpose | -|---|---|---|---| -| **Shim <-> local core** | same address space | same address space | ergonomic user-mode guest ABI implementation | -| **local core <-> deployment support** | host OS user-mode ABI | broker-kernel support ABI | local non-authoritative mechanics | -| **local core / BrokerService client <-> broker** | IPC | user/kernel boundary | trusted authority and shared state | - -The interfaces have different stability and trust requirements: - -| Interface | Shape | Authority | -|---|---|---| -| `Shim <-> local core` | ergonomic in-process API | no authority; user-mode compatibility layer | -| `local core <-> deployment support` | deployment-specific host ABI | local mechanics only; must not create LiteBox authority | -| `local core / BrokerService client <-> broker` | explicit broker protocol | authority boundary; broker validates every security-relevant operation | - -The broker authority interface should use one shared envelope and dispatch to either BrokerCore or an optional BrokerService. Caller identity is bound to the authenticated channel by broker entry code; it is not supplied as a user-controlled request field. - -```text -BrokerRequest { - target: BrokerCore | BrokerService(service_id), - operation, - handles, - memory_grants, - payload, -} -``` - -PolicyEngine is not a user-callable broker target. BrokerCore and BrokerServices call it inside the authority domain before granting authority, mutating protected state, or invoking BrokerPlatform for host-visible effects. - -The negotiated policy profile is bound to the authenticated broker session or deployment profile, not chosen by each request. It only needs an explicit request field if a future design supports multiple simultaneous policy profiles on one authenticated channel. - -External broker authority APIs: - -| API | Shape | -|---|---| -| `local core -> broker host/entry -> BrokerCore` | generic capability/resource protocol adapted into direct BrokerCore domain calls | -| `BrokerService client -> BrokerService` | service-specific protocol for an optional BrokerService | - -Internal broker-authority APIs: - -| API | Shape | -|---|---| -| `BrokerCore / BrokerService -> PolicyEngine` | in-domain authorization and audit | -| `BrokerService -> BrokerCore` | trusted in-domain API | -| `BrokerCore / BrokerService -> BrokerPlatform` | backend execution after PolicyEngine authorization | - -A BrokerService client is not a new authority layer. It is optional user-mode typed client code for a matching BrokerService, used only when a service-specific protocol is clearer than routing through generic local-core APIs. - -## Broker protocol and channels - -The broker protocol follows the stricter durable-unicorn shape: a custom, versioned, ABI-neutral object-operation protocol, not a host syscall proxy. - -Each sandboxed process has exactly one authenticated broker association. That association may contain multiple logical traffic classes: - -| Channel | Direction | Purpose | -|---|---|---| -| control | bidirectional | handshake, object operations, operation responses | -| notification | broker to process | lifecycle, readiness, interrupt-like notifications, broker/session failure | -| data | bidirectional | bulk payload bytes associated with authorized object operations | - -The broker creates or authorizes all channels and binds them to the host-authenticated peer identity of the guest process. The local core does not prove identity by filling in request fields. - -Use "notification channel" for broker-initiated asynchronous traffic. Avoid "event channel" because event already names a broker-owned object family and guest-visible eventfd-like behavior. - -The protocol exposes broker-owned objects through opaque per-association reference handles: a reference identifier plus a reference generation. Object identifiers stay broker-internal. Shims may map those handles to guest-visible integers, but the broker remains authoritative for object type, object lifetime, reference lifetime, reference generations, rights, and policy. Object types and rights are broker-internal for authorization; the local core cannot amplify authority by editing request fields. - -The protocol never passes host file descriptors, handles, sockets, directory handles, or other host-native resources to untrusted code in the baseline design. The local core receives only broker reference handles, response data, notification payloads, and broker-created channel endpoints whose authority is already bound by the broker. - -Bulk I/O uses broker-owned data channels or broker-owned shared memory rings. The control channel authorizes an operation and binds it to an object and request identifier; the data channel carries bytes for that authorized operation. Shared memory is an optimization, not an authority transfer, and all shared-memory contents remain untrusted. - -## Rust `std` and runtime strategy - -The new architecture should not force one Rust runtime model everywhere. - -| Component | Recommended baseline | -|---|---| -| local core | portable core APIs; `std` adapters only where the selected deployment permits them | -| userland broker | `std` | -| broker-kernel deployment | start with `no_std + alloc + broker-kernel support/BrokerPlatform traits`; consider a custom `std` target only if broker-kernel support grows rich enough | -| shared protocol/types | `no_std + alloc` where feasible, so they can cross user/kernel and userland/kernel-broker deployments | - -Using `std` in hosted local-core adapters is a deployment convenience, not a security decision. The local core is still untrusted. `std` can be used for normal collections, `std::sync`, broker object wrappers, IPC clients, threads, and async/data-channel libraries when the deployment supports those APIs, but cross-deployment contracts should not require a normal host OS runtime. - -Strict host-syscall profiles constrain how much hosted `std` functionality can be used after lockdown. Any `std` functionality that may issue disallowed host syscalls must either run only during bootstrap, be avoided in strict mode, or be implemented on top of the approved deployment-support ABI. - -For a broker kernel or trusted host, a custom Rust `std` target may eventually be useful if broker-kernel user-mode support can provide enough primitives: allocator, blocking/scheduling, synchronization, time, I/O, panic policy, and TLS. It should not be the first requirement. A staged design is safer: - -1. Define a small broker-kernel support/BrokerPlatform trait surface. -2. Implement it for a `std` userland broker. -3. Implement it for LVBS/SNP/kernel-backed brokers with `no_std + alloc`. -4. Only introduce a custom `std` target if the trait surface naturally becomes "basically std." - -## Local core and deployment-support calls - -The local core may use deployment-provided host/kernel calls, but only for local mechanics that do not create LiteBox authority. - -| local-core operation | Allowed? | Requirement | -|---|---|---| -| private memory allocation, TLS, logging, local scratch mappings | yes | must not grant guest-visible authority | -| private locks/futex-like synchronization | yes | must not represent broker-owned shared state | -| broker notification channel | yes | broker validates every request | -| broker-owned shared-ring data movement | yes | ring ownership, cursor movement, and frames are validated by the broker | -| direct host file, network, or device access for guest-visible resources | no | must be mediated by broker-owned objects and PolicyEngine-authorized broker policy | -| guest-visible mappings or executable/shared memory | only through broker/local-core mediation | BrokerCore validates the object, PolicyEngine authorizes, and BrokerPlatform or broker-kernel user-mode support applies | -| trusted randomness, secrets, or security-sensitive time | no | must come from broker authority | - -If the local core uses a Linux-like syscall ABI, it only works in deployments that provide that ABI. In a broker-kernel deployment, the kernel must either expose the required ABI through broker-kernel user-mode support or the runner must select a different local-core build/profile. The stable portability target is the shim/local-core/broker contract, not a single universal local-core binary. - -### Host syscall profiles - -The strict design still needs a small host-kernel interface for local mechanics, but the interface must be explicitly profiled and locked down. - -| Phase/profile | Allowed host-kernel access | -|---|---| -| bootstrap | setup-only calls such as mapping broker-created shared memory, installing signal handlers, setting TLS, creating local scratch mappings, and preparing syscall-capture/trampoline state | -| fast local mode | a small allowlist for performance, such as futex waits/wakes on private locks or broker-ring cursors | -| strict mode | post-lockdown calls only; on Linux this can target `SECCOMP_MODE_STRICT`-like behavior where only `read`, `write`, `_exit`, and `sigreturn` remain available | -| arbitrated mode | selected non-delegable syscalls are trapped/validated before execution in the user process context | - -Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, `fcntl`, and similar authority-bearing or mapping-changing calls must not reach the host unrestricted after lockdown. Guest-visible mapping operations must enter the shim/local-core path and then either be emulated from pre-reserved local memory or mediated by the broker. - -If unrestricted `mmap`/`mprotect` remain available to the sandbox, broker mapping policy is bypassable. The design must either block those syscalls after bootstrap, constrain them to anonymous private local mechanics with a host-enforced profile, or route them through local-core/broker-kernel support mediation. - -LITESHIELD's useful distinction is between delegable and non-delegable syscalls: - -| Class | Meaning for LiteBox | -|---|---| -| delegable | translate into BrokerCore/BrokerService operations | -| local-private | allow directly under the host syscall profile because no LiteBox authority is created | -| non-delegable/arbitrated | must execute in the process context, but only after trap/validation by the local-core/broker-kernel support arbitration path | -| blocked | never allowed after lockdown | - -Memory-management and process-management calls are the hard cases. If they cannot be safely delegated to the broker, deployment support needs an arbitration mechanism that can validate address ranges, mapping types, permissions, and target objects before allowing the host syscall to complete. - -### Linux userland bootstrap profile - -The durable-unicorn Linux experiment provides a future hosted-userland profile where the broker owns runner launch and ring setup: - -- the broker creates one anonymous `memfd` per spawned runner; -- the `memfd` is inherited by the runner and identified by an environment/argument convention; -- the `memfd` contains shared metadata and broker-created rings; -- the broker binds the mapped ring set to the host-authenticated spawned runner identity; -- the runner maps the `memfd`, initializes shim/local-core state, installs sandbox restrictions, then enters guest code. - -This is intentionally different from the current Unix-socket path. Today, `litebox_runner_linux_userland` does not spawn or supervise the broker. It consumes an already-running broker endpoint via unstable `--broker-socket`, negotiates the broker protocol through `litebox_broker_local`, and then starts the guest. A startup smoke test exercises only that connection/negotiation path, while broker-backed nonblocking Linux `eventfd` exercises the implemented guest-visible broker object path through the local-core event counter. Broker lifecycle ownership stays outside the runner until a later deployment profile explicitly defines broker-owned launch. - -The initial Linux ring set can use five unidirectional rings: - -| Ring | Direction | Purpose | -|---|---|---| -| control | broker to runner | broker responses, setup, control messages | -| control | runner to broker | broker requests and responses | -| notification | broker to runner | asynchronous notifications and fail-closed notices | -| data | broker to runner | bulk response/notification payload bytes | -| data | runner to broker | bulk request payload bytes | - -Broker-to-runner rings can be SPSC because there is one broker-side producer and one runner-side consumer. Runner-to-broker rings may need MPSC in fast local mode, where multiple host threads can produce directly. Strict mode may route all production through a local core scheduler, but keeping the MPSC layout preserves one ring format. - -Shared-memory rings are not trusted. The broker validates header magic/version, ring offsets/capacities, producer/consumer roles, cursor movement, frame bounds, and frame contents before acting. Impossible cursor movement, malformed frames, or writes inconsistent with ring ownership are protocol failures. - -Linux can expose two protection modes: - -| Mode | Shape | -|---|---| -| fast-futex mode | allows a small syscall allowlist, including futex wait/wake on ring cursors or private locks | -| strict-seccomp mode | installs mappings, fds, signal handlers, and trampoline state before lockdown; after lockdown, only a strict syscall set remains available | - -In strict-seccomp mode, guest host-thread parallelism may need to become a shim/local-core scheduling illusion rather than real host threads. This is a compatibility/performance tradeoff, not a broker policy bypass. - -## Deployment contract and negotiation - -The user-side runner and global broker should match through a shared deployment contract, not ad hoc discovery. - -Shared spec crates should define: - -- the broker envelope and handle/memory-grant formats; -- broker channel authentication and peer identity binding; -- broker authority protocol versions; -- BrokerService IDs, protocol versions, request/response types, and feature requirements; -- PolicyEngine policy versions, policy profile IDs, and audit requirements; -- broker capability names and profiles; -- deployment-support ABI names and versions; -- control/notification/data channel formats; -- shared-memory/ring layout versions and validation rules; -- host syscall profiles for bootstrap, fast local mode, and strict mode; -- deployment profiles that bind a shim, local-core profile, broker channel, required services, and required broker features. - -The eventual deployment contract should fail closed: - -1. The runner selects a deployment profile, such as `optee-on-lvbs` or `optee-on-userland`. -2. The runner selects a local-core profile that matches the deployment's host ABI. -3. The user side establishes an authenticated broker channel. In userland, this can use OS IPC peer credentials; in broker-kernel deployments, this comes from the trusted entry path. The current Unix-socket channel returns an explicit unauthenticated placeholder credential through the same `HostControlChannel` API that later authenticated channels will implement. -4. The broker binds the caller identity used for dispatch to the authenticated peer credential. User mode does not choose its own authority identity. -5. The user side sends required BrokerCore, BrokerService, PolicyEngine policy profile, broker capability, local-core profile, deployment-support, channel/ring, and host-syscall-profile versions. -6. The broker replies with supported services and capabilities. -7. The user side starts only if the required versions and features match. - -The current hosted userland path implements the first control-channel subset of that contract: an externally supplied Unix socket, protocol negotiation, unauthenticated placeholder peer credentials, and a broker setup deadline that bounds connection and negotiation. It also routes the migrated nonblocking `eventfd` object family through broker-backed local-core event counters. Full deployment-profile negotiation, host syscall profile matching, channel/ring negotiation, authenticated identity binding, and broader guest-visible broker object routing remain future work. - -The local core should not depend on BrokerPlatform internals. It should depend on the host ABI selected by the deployment profile and the negotiated broker contract: memory-grant format, trap/upcall mechanism, shared-page support, direct fast-path permissions, broker-mediated network/storage requirements, timer behavior, and similar features. Enforcement happens broker-side through PolicyEngine; user-mode code only adapts to supported capabilities. - -## Security invariant - -The core security rule is: - -> User-mode Shim and local core may request operations and cache derived state, but they must never create authority. - -Therefore BrokerCore, BrokerServices, PolicyEngine, and BrokerPlatform must be authoritative for: - -- workload, process, session, thread, and namespace identity; -- handle/resource capabilities; -- handle passing, duplication, revocation, and cleanup; -- shared memory and mapping permissions; -- filesystem, network, device, and host I/O policy; -- signal, event, wait, readiness, and lifecycle state; -- randomness, secrets, and trusted time; -- timers and scheduling-visible state; -- quotas and revocation; -- network and application-level firewall policy; -- shim/domain-specific trusted semantics when a BrokerService is present. - -A compromised user-mode shim/local-core path should not be able to escape the broker-granted authority. - -All authority-changing or host-effecting operations must be authorized by PolicyEngine before BrokerCore state mutation, BrokerService authority grants, or BrokerPlatform backend execution. BrokerCore remains responsible for structural capability/lifecycle validity; BrokerServices provide domain-specific context; PolicyEngine makes the policy decision. - -Any broker-side validation of data sourced from user-controlled memory, including shared rings, memory grants, and scatter/gather descriptors, must operate on a private snapshot or otherwise revalidate before use. The local core must be assumed hostile for the duration of an in-flight request. - -## State ownership - -| State | Owner | -|---|---| -| guest ABI decoding state | Shim | -| per-workload cache/view | local core | -| BrokerService client state | optional BrokerService client | -| guest memory marshalling | local core + broker revalidation | -| private user-mode mechanics | local core via deployment support | -| private synchronization fast paths | local core | -| shared synchronization | BrokerCore | -| guest-visible handle numbers | local core view, BrokerCore authority | -| open/shared resource descriptions | BrokerCore | -| shim/domain-specific authoritative state | optional BrokerService backed by BrokerCore | -| policy decisions, constraints, and audit records | PolicyEngine | -| IPC/event/queue/socket-like resources | BrokerCore-owned resources | -| guest-visible/security-sensitive address-space mappings | BrokerCore + PolicyEngine + BrokerPlatform | -| user-only scratch mappings | local core via deployment support | -| host-visible I/O | BrokerPlatform executes PolicyEngine-authorized policy | -| process/session/workload lifecycle | BrokerCore | - -local-core-owned entries in this table are non-authoritative views, caches, or private fast paths. Broker-visible data and security-relevant state must still be revalidated by BrokerCore, BrokerServices, and PolicyEngine at the authority boundary. - -## Process and session model - -The stricter baseline uses one broker per sandbox session and one sandboxed host process per guest process. - -| Concept | Owner | -|---|---| -| sandbox session | broker | -| guest process identity | BrokerCore | -| authenticated host process association | broker channel plus host OS or broker-kernel user-mode support identity | -| guest-visible process semantics | shim + local core, backed by BrokerCore identity | -| process creation | broker-mediated | -| `exec`-like ABI behavior | shim/local-core within an existing broker association unless policy requires otherwise | - -All guest processes in one sandbox session share one broker. The broker assigns guest process identity, creates or authorizes the private channel set for each process, and binds the channel set to the host-authenticated peer identity. A guest process cannot claim another process's identity by choosing request fields. - -POSIX-like `fork` is broker-mediated at the identity/channel/resource level, while ABI-specific memory and descriptor inheritance semantics remain shim/local-core work. POSIX-like `exec` is preferably a shim/local-core replacement of guest memory and ABI state inside the existing sandboxed host process, so BrokerCore does not become ABI-specific. - -## Local core vs BrokerCore in current `litebox` - -The current `litebox` crate should not be migrated by whole module. Most modules mix ergonomic user-facing logic with authority-bearing state. The useful boundary is by responsibility. - -| Current area | Keep in local core | Move to BrokerCore / broker side | -|---|---|---| -| `LiteBox` object | user-mode facade, std-backed helpers, broker connection/session object | broker session/workload identity | -| `fd::Descriptors` | guest fd number table/cache, typed wrappers, syscall ergonomics | authoritative object IDs, reference IDs, reference generations, rights, dup/pass/close/refcounts | -| `fd::RawDescriptorStorage` | raw-int fd conversion for shim ABI | validation that a handle is live and authorized | -| fd metadata | local ABI metadata and cached hints | shared open-description metadata and metadata inherited/duplicated/passed across processes | -| `path.rs` | string/CStr conversion, cheap normalization helpers | authoritative path lookup, namespace traversal, permission checks | -| `fs::*` | user-facing file API stubs, buffer marshalling, broker data-channel wrappers | filesystem namespace, inode/node identity, cwd/root, permissions, open file descriptions | -| `pipes.rs` | typed pipe fd facade and read/write marshalling | pipe object, ring state, endpoint lifetime, readiness/wakeup | -| `event::wait` | per-thread wait context, blocking current thread, local timeout conversion | shared wait queues, readiness state, cross-process wake routing | -| `event::polling` | readiness cache and ergonomic polling facade | authoritative readiness generations for shared objects | -| `sync::futex` | private futex fast path | shared futex table keyed by shared memory object/address | -| `net::*` | socket API facade and send/recv buffer marshalling | socket objects, local ports, listen backlog, connection state, firewall-visible flow state | -| `mm::PageManager` | loader helpers, guest pointer handling, cached VMA view | authoritative mappings, permissions, memory grants, shared mappings, page-fault decisions | -| `tls.rs` | shim/user-local TLS | none | -| `utils::id_pool` and similar utilities | reusable helper where local-only | broker-owned ID allocation when IDs carry authority | -| current `platform` traits | internal local-core deployment-support adapter where local-only | broker-kernel support/BrokerPlatform traits where trusted-domain or backend effects are required | - -Concrete examples: - -- `fd::Descriptors` currently stores in-process descriptor entries with `Arc>`. The local core can keep the ergonomic raw-fd and typed-fd presentation, but BrokerCore should own the live object, closeable reference, rights, reference generations, refcount, passing, duplication, close, and revoke semantics. -- `fs::in_mem`, `fs::layered`, and `fs::nine_p` currently store namespace-like state in-process. In a multiprocess design, namespace state and open file descriptions must be broker-side. The local core should only marshal paths/buffers and use broker-owned data channels or rings for payloads. -- `net::Network` currently owns smoltcp socket state, local port allocation, close queues, and platform packet I/O. If the broker enforces network/firewall policy, socket and port authority must be broker-side. The local core should keep the socket facade and use broker-owned rings for data movement. -- `mm::PageManager` currently owns VMA state and calls platform page-management operations. The local core can keep loader-side helpers and cached VMA views, but authoritative mapping permissions, shared mappings, and page-fault decisions belong broker-side. - -## Control path and data path separation - -The broker should be on the control path for authority, but it does not need to be on every byte of every data path. - -```text -Control path: - local core -> BrokerCore/BrokerService -> PolicyEngine -> BrokerPlatform - -Data path: - local core moves bytes through broker-owned data channels or rings -``` - -The broker still owns setup, rights, revocation model, object identity, and audit boundaries. Once it creates a constrained data channel or shared ring, the local core can move bytes without a control RPC per byte, but the broker still owns the object and validates frames/cursors before acting. - -Good candidates: - -| Surface | Local data path candidate | Broker-controlled setup | -|---|---|---| -| regular file read/write | broker data channel or broker-owned shared file cache | open/path resolution/permissions/flags | -| read-only file/executable pages | broker-approved mapping/static backing | file open + mapping permission | -| pipe/queue bulk bytes | shared memory ring per pipe endpoint | create pipe, endpoint rights, readiness/wakeup | -| local IPC/domain sockets | shared rings between endpoints | connect/bind/permission/routing | -| network sockets | broker-owned shared TX/RX rings when policy allows | socket/create/connect/bind/listen/firewall | -| 9P filesystem | shared-memory request/data rings | attach/walk/open/auth/policy | -| private futexes | entirely local for private memory | broker only for shared futexes | -| event/poll readiness | local cache for readiness bits | authoritative wait queue/wakeup generation | -| guest memory copy | local copy-in/copy-out | broker validates grants for broker-visible ops | - -Rule: - -> Data path may be local only through broker-owned channels or rings whose use cannot exceed the approved broker object rights. - -If immediate revocation, full audit, or byte-level policy is required, the data path stays broker-mediated. - -## No host-handle delegation in the baseline - -The durable-unicorn experiment chose the stricter rule that the broker never passes host file descriptors, HANDLEs, sockets, directory handles, or similar host-native objects to untrusted code. This design adopts that rule as the baseline. - -Host resources stay broker-owned: - -```text -local core asks broker: open(path, flags) -BrokerCore resolves object/capability -PolicyEngine authorizes path/flags/caller -BrokerPlatform opens host object and stores host handle privately -Broker returns broker reference handle, not host fd/HANDLE -local core uses control/data channels for operations -``` - -This avoids moving enforcement into local core or the runner, avoids cross-OS handle-rights mismatches, and makes revocation/audit simpler. The cost is higher broker involvement on data operations. Performance should first be recovered through broker-owned data rings, batching, and object-specific data channels rather than raw host-handle delegation. - -Reintroducing host-handle delegation would require a separate future design note with object-specific proof obligations. It is not assumed by this architecture. - -## Trusted data-plane services - -The stricter baseline keeps host resources broker-owned and avoids raw host-handle delegation to local core. SKernel suggests a future performance direction that preserves this rule: introduce trusted data-plane services inside the broker authority domain. - -In that model: - -```text -local core: - untrusted ABI compatibility, marshalling, local caches - -BrokerCore / PolicyEngine: - object identity, rights, lifecycle, policy - -Broker data-plane service: - trusted high-performance implementation of an object family - -BrokerPlatform: - authorized host/device/backend effects -``` - -A trusted data-plane service is not local core. It is part of the TCB, like a BrokerService or BrokerPlatform-adjacent component, and can hold backend authority that local core must not receive. - -Potential examples: - -| Service | Inspired by | LiteBox interpretation | -|---|---|---| -| filesystem data-plane service | SKernel-D FD/image-based filesystem, EROFS/TMPFS | broker-owned filesystem cache/ring service that reduces per-operation broker IPC without exposing host fds | -| network data-plane service | SKernel-D high-performance network stack with device passthrough | trusted broker-side network service; local core talks via rings while PolicyEngine keeps firewall authority | -| memory/resource coordination service | SKernel-R/SKernel-V resource calls | broker-kernel support/BrokerPlatform resource-call path for memory, CPU, and device-resource elasticity | - -This is an optimization path, not the first milestone. The initial implementation should still start with simple broker-owned objects and mediated control/data channels. If performance demands it, move hot object-family data paths into trusted broker-side services rather than expanding untrusted local-core authority. - -## Network and firewall enforcement - -The design can support a network or application-level firewall, but only if the broker is the authoritative network path. - -In that configuration, local core must not send or receive guest-visible traffic directly through a platform network device. It may only operate on broker-granted network resources. BrokerCore owns the virtual socket/NIC/resource state, BrokerServices provide domain-specific context when needed, PolicyEngine authorizes policy, and BrokerPlatform executes approved backend operations. - -Possible datapath shapes: - -| Shape | Security property | Performance tradeoff | -|---|---|---| -| broker-mediated control and data | simplest to reason about; every byte is broker-visible | highest IPC/switch overhead | -| broker-approved shared rings | broker still owns state and PolicyEngine authorizes policy | lower overhead, more queue/accounting complexity | -| broker protocol proxy | enables application-level policy when broker sees plaintext/protocol metadata | protocol-specific and more complex | - -Layer-specific implications: - -- Packet and connection policy can be authorized by PolicyEngine from packet headers, connection metadata, endpoint capabilities, and resource labels supplied by BrokerCore/BrokerServices. -- Application-level policy requires PolicyEngine or a broker-side policy helper to see application-level metadata or plaintext. If the guest workload performs end-to-end encryption entirely inside its own address space, the broker can still enforce connection-level policy, but it cannot inspect encrypted payloads unless the design explicitly uses a broker proxy, broker-managed protocol endpoint, or broker-controlled keys. -- Centralizing traffic in the broker creates a throughput and latency bottleneck. The design should plan for batching, shared-memory rings, flow control, per-resource quotas, and efficient policy caching. -- Broker-side policy decisions must be made on stable data. If packet descriptors or payload metadata arrive through shared memory, the broker must snapshot or revalidate them before enforcement. - -## Performance model - -The design should avoid "every operation is remote" while preserving security. - -Use: - -- local guest ABI decoding; -- local cache for non-authoritative handle/process/session views; -- direct local-core fast paths for private state; -- batched broker calls where possible; -- shared-memory rings for bulk IPC, pipe, queue, and network data; -- broker-mediated setup with local data-plane access where security allows; -- cached PolicyEngine decisions when the cache key includes all security-relevant context and supports revocation; -- explicit invalidation/revocation for stale local-core caches. - -The broker path is required for authority changes, cross-workload operations, shared resources, host-visible effects, and firewall-enforced traffic. - -## Why this is better than moving all core into broker - -Moving the whole core into broker would make the trusted boundary too large and too chatty. It would also force guest pointer handling, guest ABI compatibility policy, and shim-specific logic into the trusted domain. - -This separation keeps the trusted computing base smaller: - -```text -User mode: - compatibility, marshalling, caching, broker-owned local data channels - -Authority domain: - validation, capabilities, shared state, policy enforcement, optional domain authority, host effects -``` - -## Main risks - -| Risk | Mitigation | -|---|---| -| local-core cache diverges from BrokerCore | generation-tagged handles, invalidation, broker authority checks | -| user shim bypasses policy | broker validates every security-relevant request and routes policy decisions through PolicyEngine | -| ABI becomes too chatty | batching, shared memory data planes, control/notification/data channel separation, local private fast paths | -| duplicated logic | keep policy in PolicyEngine, authority state in BrokerCore/BrokerServices, and ABI translation in local core | -| handle/resource lifetime bugs | broker-owned object IDs, refcounts, cleanup on lifecycle transitions | -| broker bottleneck from no host-handle delegation | use broker-owned rings, batching, object-specific data channels, and policy caching | -| address-space lifecycle complexity | broker-authoritative mappings, shared object IDs, careful copy-on-write/shared-memory design | -| firewall datapath bottleneck | shared rings, batching, quotas, policy caching, and broker-side flow control | -| encrypted traffic hides application data | enforce metadata/connection policy unless using broker proxying or broker-managed keys/endpoints | -| BrokerServices become a second monolithic core | make them optional, small, versioned, and backed by BrokerCore primitives | -| local core depends on unavailable host ABI | select local-core profile by deployment, or provide the needed ABI through deployment support | -| local core depends on trusted-domain internals | expose negotiated broker/host capability profiles instead of implementation details | -| shared-memory TOCTOU or double-fetch bugs | validate broker requests against private snapshots or revalidate every use of user-controlled fields | -| unauthenticated broker channel | authenticate peers before negotiation and bind broker-assigned caller identity to the authenticated channel endpoint | -| PolicyEngine becomes a second core | keep it decision/audit focused; authoritative object state remains in BrokerCore/BrokerServices | -| custom kernel `std` target becomes a distraction | start with `no_std + alloc + traits`; introduce custom `std` only if the broker-kernel support surface justifies it | -| non-delegable syscalls bypass broker mapping/resource policy | block, constrain, or trap/arbitrate them before host execution | -| trusted data-plane services grow too powerful | keep them broker-side, object-family-specific, and PolicyEngine-authorized | - -## Prior-art positioning - -LiteBox's proposed design combines ideas from several systems rather than copying any one of them. - -| System | Relevant idea | LiteBox lesson | -|---|---|---| -| **Drawbridge** | application + LibOS in a picoprocess over a narrow Host ABI | keep the local core user-mode and keep the broker ABI narrow | -| **Haven** | Drawbridge-style LibOS inside shielded execution | a narrow host interface composes with stronger isolation domains, but LiteBox's broker is trusted TCB rather than untrusted host | -| **Graphene / Gramine** | LibOS plus PAL/host ABI, including SGX deployments | separate compatibility logic from host adaptation; avoid baking one host ABI into core | -| **gVisor** | userspace kernel plus brokered filesystem/gofer model | rich domains like filesystems and sockets need real broker services, not just generic RPC | -| **Chromium sandbox** | sandboxed target, broker/browser process, Mojo IPC, delegated handles | useful contrast: delegated handles can work, but LiteBox's stricter baseline keeps host handles broker-owned | -| **Capsicum** | capability mode, broker opens resources and passes restricted fds | useful contrast: capability fd passing is powerful, but requires OS support for precise rights | -| **Lind / Native Client** | POSIX LibOS inside a restricted sandbox with brokered host operations | local core should not be a generic syscall escape hatch | -| **Arrakis / Exokernel** | OS as control plane, application gets direct data path after safe allocation | broker can be the control plane while local core uses broker-owned rings/data channels for safe data paths | -| **LITESHIELD** | userspace microkernel services, shared-memory IPC, delegable vs non-delegable syscall handling | borrow syscall classification and arbitration; keep LiteBox's explicit broker objects and PolicyEngine | -| **SKernel** | separates guest kernel into resource kernel and data kernel; trusted I/O data plane for performance | consider future trusted broker data-plane services, not untrusted local-core authority expansion | -| **seL4 / capability microkernels** | explicit unforgeable capabilities define authority | BrokerCore should keep object identity internal and expose reference capabilities with generation checks while storing authoritative rights | -| **Qubes OS** | compartmentalized workloads use service VMs for devices/network | isolate rich authority into broker services and policy, especially device/network access | -| **Nabla / Kata / unikernels** | reduce host syscall surface or use VM-backed isolation | narrow the authority interface; choose stronger isolation per deployment when needed | - -The distinctive LiteBox claim is one architecture that supports both: - -```text -userland broker process -kernel broker -``` - -while keeping: - -```text -Shim + local core -``` - -always in user mode. - -## Mapping current components - -The current code does not match the final boundaries exactly. In particular, some current shims and platforms are linked together in the trusted domain for existing deployments. The mapping below describes the intended migration target. - -Where a current crate spans both user-mode and authority responsibilities, the mapping below describes the destination responsibility boundary, not a one-to-one rename. - -### `litebox_shim_optee` - -Today, this crate combines OP-TEE ABI handling, TA loading, per-TA state, page-manager use, syscall dispatch, and some session/object/crypto bookkeeping. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| OP-TEE entry/request decoding, return conventions, TA ABI details | Shim | -| TA-local syscall helpers, guest buffer marshalling, local loader helpers | local core, with broker revalidation for broker-visible data | -| non-authoritative TA/session/object caches | local core | -| authoritative TA/session registry, cross-TA/session state, OP-TEE persistent object semantics, PTA access-control context | OP-TEE BrokerService backed by BrokerCore | -| OP-TEE policy decisions and audit, including PTA and secure-storage authorization | PolicyEngine | -| generic identities, capabilities, memory grants, lifecycle, wait/notify, accounting | BrokerCore | -| trusted secrets, secure storage backend, normal-world shared-memory validation, address-space operations | BrokerPlatform after PolicyEngine authorization | - -This likely means `litebox_shim_optee` should eventually become thinner: the OP-TEE ABI layer remains shim-specific and user-mode, while reusable local core and broker-facing pieces move behind generic interfaces. The kernel/trusted deployment does not need the full OP-TEE shim; it needs only the OP-TEE-aware enforcement that creates authority. - -### `litebox` - -Today, `litebox` is an in-process no_std core library. Its current `LiteBox` object and subsystems assume Rust references, generic platform traits, in-process locks, and in-process descriptor/resource identity. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| ergonomic in-process helpers used by shims | local core | -| guest-visible handle table view | local core backed by BrokerCore | -| private sync, TLS, path conversion, guest marshalling | local core | -| broker-owned data-channel wrappers | local core | -| shared resource identity/lifetime | BrokerCore | -| synchronization/wait/readiness authority for shared objects | BrokerCore | -| final policy decision/audit | PolicyEngine | -| platform trait surface | internal local-core deployment-support adapter, broker-kernel user-mode support, and BrokerPlatform surfaces | -| service-specific authority | optional BrokerServices plus PolicyEngine decisions, not generic BrokerCore | - -The important change is that the current core API should not become the cross-boundary ABI. The local core can keep ergonomic Rust APIs, the broker boundary needs an explicit handle/capability protocol, and broker host/entry code adapts that protocol into BrokerCore domain calls. - -### `litebox_platform_lvbs` - -Today, this crate is effectively a trusted-domain platform: it owns page tables, user-memory validation, VTL switching, syscall/exception entry, host calls, randomness/secrets hooks, and network backend hooks. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| page-table and address-space management | BrokerPlatform | -| VTL/trusted-domain trap and channel mechanism | broker-kernel user-mode support | -| broker request decode and dispatch | `litebox_broker_host` | -| domain validation and authorization | BrokerCore + PolicyEngine | -| normal-world memory mapping and validation | BrokerPlatform | -| host I/O, network backend hooks, root key/secrets | BrokerPlatform executing PolicyEngine-authorized operations | -| user-mode shim/platform helpers | local-core internals, not the current crate wholesale | - -In the new model, LVBS contributes both BrokerPlatform and broker-kernel user-mode support pieces. BrokerPlatform owns privileged backend authority. Broker-kernel user-mode support exposes the host ABI that lets a user-mode local core execute and reach the broker. The LVBS-targeted local-core profile selected by a deployment profile, such as `optee-on-lvbs`, should be smaller than the current LVBS crate. - -### `litebox_runner_lvbs` - -Today, this crate boots the trusted-domain environment, initializes the LVBS platform, dispatches VTL calls, handles OP-TEE messages, creates task page tables, manages sessions, and directly invokes the OP-TEE shim. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| early boot and trusted-domain initialization | broker bootstrap | -| VTL trap/channel dispatch | broker-kernel user-mode support | -| broker request decode and dispatch | `litebox_broker_host` | -| domain validation and authorization | BrokerCore + PolicyEngine | -| session/page-table orchestration | BrokerCore + OP-TEE BrokerService + PolicyEngine + BrokerPlatform | -| shim ABI state and non-authoritative per-task helpers | user-mode OP-TEE Shim + local core | -| authoritative TA/session/object identity currently held by the runner | BrokerCore + OP-TEE BrokerService + PolicyEngine | -| user/broker compatibility checks | deployment profile negotiation | -| local user ABI support | broker-kernel user-mode support | - -The runner becomes less of an application runner and more of a broker bootstrap/entrypoint for the trusted deployment. - -### OP-TEE-on-userland runner - -The current OP-TEE userland runner sets a platform, builds `OpteeShim`, loads binaries, optionally rewrites syscall instructions, and runs the workload in one process. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| command-line harness and binary loading for tests | runner/test harness | -| `OpteeShim` construction | Shim + local-core process setup | -| user-mode local execution | local-core setup | -| broker/service compatibility | deployment profile negotiation | -| shared/security-authoritative state | separate privileged broker process | - -This runner is a good prototype for the userland deployment shape, but it currently lacks a separate broker authority. - -### `litebox_platform_multiplex` - -Today, this crate chooses one monolithic platform type at compile time. - -Future mapping: - -| Current responsibility | Target component | -|---|---| -| selecting a single `Platform` | separate into local-core profile selection, BrokerPlatform selection in the broker, and broker-kernel user-mode support selection only for broker-kernel deployments | -| global platform accessor for shim-side code | local-core deployment-support accessor | -| trusted backend selection | BrokerPlatform accessor inside the broker | -| policy module/profile selection | PolicyEngine configuration inside the broker | - -This separation is needed because the current platform traits mix local execution mechanics with trusted authority. - -### Other shims, platforms, and runners - -The OP-TEE/LVBS sections above are worked examples, not an exhaustive crate-by-crate migration plan. Other existing crates follow the same destination responsibility boundary: - -| Current component | Target shape | -|---|---| -| `litebox_shim_linux`, `litebox_shim_windows` | Shim plus local-core-facing ABI translation; any shared or policy-relevant process, descriptor, filesystem, network, or device authority moves to BrokerCore, optional BrokerServices, and PolicyEngine. | -| `litebox_platform_linux_userland`, `litebox_platform_windows_userland` | hosted local-core deployment-support implementations; native host calls are limited to local non-authoritative mechanics and broker channels/rings. | -| `litebox_platform_linux_kernel` | broker-kernel pieces follow the LVBS boundary: privileged backend operations become BrokerPlatform, while any user-mode support/trap channel becomes broker-kernel user-mode support. | -| `litebox_runner_linux_userland`, `litebox_runner_windows_userland`, `litebox_runner_linux_on_windows_userland` | user-side runners that select local-core profile, create shim/local-core state, authenticate to the broker, and negotiate deployment profile compatibility. | -| `litebox_runner_snp` | trusted-deployment bootstrap analogous to LVBS; move external entry/channel support into broker-kernel user-mode support, authority state into BrokerCore/BrokerServices/PolicyEngine, and backend privileged operations into BrokerPlatform. | - -## Initial implementation direction - -The first milestone should not attempt full multi-process support for every shim. Start with the smallest authority slice: - -The initial slice uses: - -```text -litebox_broker_protocol -litebox_broker_core -litebox_broker_transport -litebox_broker_host -litebox_broker_userland -litebox_broker_local -separate userland broker process -Unix-domain-socket channel implementing neutral control-channel traits -control channel only -minimal PolicyEngine -broker-owned event object -``` - -Early end-to-end shim tests should use a hybrid migration profile: only the migrated object family routes through broker-backed local-core wrappers, while unrelated operations continue through the existing local compatibility path. Shims should keep calling local-core object interfaces; local-core entries can contain either local compatibility state or broker-backed wrappers with cached rights. This is explicitly a migration profile, not the final security posture for arbitrary workloads. - -Then proceed incrementally: - -1. Define the component boundary: `Shim`, local core, optional BrokerService clients, `BrokerCore`, optional `BrokerServices`, `PolicyEngine`, `BrokerPlatform`, `litebox_broker_host`, and, in broker-kernel deployments, broker-kernel user-mode support. -2. Create `litebox_broker_protocol` with the shared protocol version type, opaque event reference handle format, minimal event request/response messages, readiness/wait outcomes, ABI-neutral errors, known/unknown receive wrappers, peer credentials, and neutral control-channel traits needed for the first event-object path. -3. Create `litebox_broker_core` with broker-owned process association IDs, authority-only object type and rights metadata, caller credentials supplied by broker entry code, an event object registry plus per-association reference IDs with generation checks, a minimal object/reference registry, association cleanup, and policy hooks. BrokerCore must stay protocol-neutral and channel-neutral. -4. Add `litebox_broker_protocol::wire` with reusable message-body encoding, create `litebox_broker_transport` with the concrete Unix-domain-socket implementation, create `litebox_broker_host` with protocol negotiation, request sequencing, unknown-tag handling, and adaptation from channel/protocol requests to direct BrokerCore domain calls, and create `litebox_broker_userland` as the hosted executable that assembles those pieces. -5. Add startup negotiation between the local side and broker. The current path starts with protocol negotiation over `--broker-socket`; later deployment profiles add required BrokerCore, BrokerService, PolicyEngine, broker capability, channel/ring, host syscall profile, local-core profile, and deployment-support feature negotiation. -6. Add a broker-owned event object with the smallest end-to-end surface first: create, wait, add, and consume. BrokerCore/host cleanup releases association-owned references on disconnect; protocol-level duplicate, close, explicit readiness queries, and broader stale-handle tests follow after the initial broker path is proven. -7. Use `litebox_broker_local` for typed end-to-end broker tests, keeping the local adapter independent of Unix sockets so future channel implementations can implement the same neutral channel traits. -8. Continue shaping `litebox` as the untrusted local core for syscall/resource routing, local-private operations, broker-backed object wrappers, and compatibility-profile glue. -9. Define host syscall profiles for bootstrap, fast local mode, and strict mode. -10. Make local-core handle tables broker-backed views for migrated object families. -11. Add a broker wait/wakeup channel. -12. Prototype a broker-owned pipe or queue with shared-ring data path. -13. Prototype a broker-owned file object with mediated control/data-channel I/O, not host-handle delegation. -14. Prototype broker-owned network resources with PolicyEngine firewall policy and BrokerPlatform backend execution. -15. Add a small OP-TEE BrokerService only for authority that cannot be represented by generic BrokerCore primitives. -16. Expand to shared memory, lifecycle transitions, IPC, filesystem policy, network policy, and shim-specific resource models. - -That gives a controlled path from current single-process/single-session assumptions toward true shared-state support without rewriting every shim and platform at once. diff --git a/docs/impl-plan.md b/docs/impl-plan.md deleted file mode 100644 index db52e3686..000000000 --- a/docs/impl-plan.md +++ /dev/null @@ -1,398 +0,0 @@ -# Broker Architecture Implementation Plan - -## Goal - -Implement the broker architecture as incremental vertical slices while keeping the existing LiteBox behavior working. - -The target architecture has two trust domains: - -```text -User mode: - Shim + local core + optional BrokerService clients - -Authority domain: - broker entry/host + litebox_broker_host + BrokerCore + optional BrokerServices + PolicyEngine + BrokerPlatform - broker-kernel user-mode support, in kernel-backed deployments -``` - -The local core reaches local mechanics and broker channels through deployment support: the host OS user-mode ABI plus a broker transport endpoint in hosted userland, or calls into broker-kernel user-mode support plus broker-channel delivery in a broker-kernel deployment. - -The baseline is the stricter durable-unicorn model: no host fd/HANDLE delegation to untrusted code, ABI-neutral broker objects, broker-owned control/notification/data channels, authenticated per-process broker associations, and fail-closed behavior. - -## Implementation principles - -- Build vertical slices, not a big-bang refactor. -- Keep local core untrusted and broker authority explicit. -- Keep BrokerCore shim-neutral. -- Keep BrokerCore protocol-neutral and channel-neutral. BrokerCore exposes in-domain authority methods and domain types; broker entry/host code adapts protocol requests and channel credentials before calling it. -- Keep the broker protocol modular: the outer request/response envelope is for connection-level broker messages and coarse authority routing, while object/domain operations live in nested request/response families such as `CoreRequest::Event` and `EventResponse`. -- Keep the control channel strictly paired request/response; broker-initiated readiness, interrupt, fault, revocation, and session-failure messages should use a separate notification channel/message family. -- Put domain-specific authority in BrokerServices. -- Put final allow/deny/audit decisions in PolicyEngine. -- Keep BrokerPlatform as authorized backend execution, not a policy owner. -- Keep broker-kernel user-mode support separate from broker request decode/authorization; `litebox_broker_host` owns the reusable protocol-to-core adapter for both userland and kernel brokers. -- Start in userland; move to kernel-broker deployment after broker semantics are proven. - -## Phase 0: Boundary freeze - -Define and document the core vocabulary in code and docs: - -- `local core` -- `litebox_broker_local` adapter -- optional BrokerService client -- `BrokerCore` -- `BrokerService` -- `PolicyEngine` -- `BrokerPlatform` -- `litebox_broker_host` -- broker-kernel user-mode support - -Exit criteria: - -- Design doc and code comments use one vocabulary. -- No new code treats local core as trusted. -- No broker API is modeled as host syscall proxying. - -## Phase 1: Shared protocol/types crate - -Create a shared crate for broker protocol types. - -Initial contents: - -- protocol version type; -- broker event reference handles with reference generations; -- small broker request/response envelope with minimal nested event request/response families; -- readiness and wait outcome payloads; -- ABI-neutral error categories; - -Richer request/response envelopes, control/notification/data channel frame headers, policy profile IDs, host syscall profile IDs, and feature negotiation structures are added when later milestones need them. - -Prefer `no_std` for shared types, adding `alloc` only in crates that need owned buffers, so they can be reused by userland and kernel-broker deployments. - -Exit criteria: - -- Shared types compile independently. -- Wire-visible fields are explicit and versioned. -- Caller identity is not caller-chosen inside request payloads. - -## Phase 2: Userland broker skeleton - -Implement a userland broker process first. - -Initial scope: - -- one broker process per sandbox session; -- at least one authenticated process association within the broker session; -- control channel only; -- major-version/minor-compatible protocol negotiation; -- neutral blocking `no_std` control-channel traits with channel-specific error types and explicit clean-close receive semantics; -- channel-produced peer credentials returned through the host control-channel trait and mapped by the broker host into BrokerCore caller credentials; -- reusable `no_std + alloc` request/response wire codec for byte-stream channel implementations; -- Unix-domain-socket framing as the first concrete userland channel implementation; -- a Unix-socket executable that wires the generic channel-neutral host to the concrete Unix control-channel implementation; -- host-owned protocol negotiation, request sequencing, unknown-tag handling, protocol/core type adaptation, and connection-close reasons; -- BrokerCore-owned caller associations, object/reference authority, policy hooks, event behavior, and association cleanup; -- default-deny PolicyEngine; -- fail-closed channel/session behavior. - -Exit criteria: - -- A local-side adapter can connect and negotiate. In the current hosted userland path, `litebox_runner_linux_userland` consumes an externally owned Unix-socket endpoint via `--broker-socket`, uses `litebox_broker_local` to negotiate, constructs the `LiteBox` local core with broker control already present, and the Linux shim reaches migrated event objects through the local-core event domain rather than through broker-specific APIs. -- Broker binds caller identity to the authenticated channel endpoint. The first hosted executable passes the explicit unauthenticated placeholder through the same host API that later deployment-specific authentication will use. -- Userland channel code only receives/sends decoded frames and supplies peer credentials; the generic host owns broker protocol dispatch and reports successful termination as peer-close or broker-close with a reason. -- The Unix-socket channel adapter and hosted broker executable live in separate crates, so local-side code can depend on the channel without pulling in broker core/host deployment code. -- BrokerCore depends only on shared broker value DTOs from `litebox_broker_protocol`; it does not depend on protocol envelopes, channel traits, wire codecs, or concrete IPC crates. -- Local-side code does not need to depend on the userland broker executable or host crate to use the first Unix socket channel. -- The generic broker host library does not depend on concrete Unix socket channel code and remains `no_std`. -- Malformed or unauthorized requests fail closed or return policy-denied according to explicit policy. -- Unsupported future protocol operations return `UnsupportedOperation` without closing the connection so local peers can probe optional features explicitly; newer core error categories or wait outcomes that the host adapter cannot represent return `Internal`. -- Version-mismatch negotiation responses advertise the broker-supported version and keep the connection in negotiation state so local peers can downgrade without reconnecting or guessing. -- BrokerCore/object operations are grouped below the broker envelope instead of added as unrelated top-level `BrokerRequest` and `BrokerResponse` variants. -- Control-channel contracts live in `litebox_broker_protocol::channel`, separate from semantic message DTO modules but in the same shared protocol crate. Future broker-initiated readiness, interrupt, fault, revocation, or session-failure traffic must use a separate notification channel/message family rather than unsolicited control-channel responses. - -## Phase 3: Local core facade - -Introduce the local core without moving every subsystem. - -Initial scope: - -- wrap existing LiteBox ergonomics in the local core; -- add the `litebox_broker_local` adapter; -- keep existing local implementations available behind a profile/feature; -- add a broker-backed handle-table view for experimental objects. - -Exit criteria: - -- Current tests can still use the local profile. -- A broker-backed profile can issue a simple broker request. -- local-core handle entries can store opaque broker reference handles plus local cached rights hints. - -## Phase 4: First broker-owned object - -Start with a small event or pipe-like object, not filesystem or networking. - -Broker owns: - -- broker-internal object ID and lifetime; -- initial reference ID, reference generation, and rights; -- readiness state; -- wait/wakeup state. - -The local core owns: - -- guest-visible handle number; -- typed facade; -- buffer marshalling; -- non-authoritative readiness cache. - -Exit criteria: - -- Create, wait, and signal work through BrokerCore and the separate broker process. -- BrokerCore and the host already release association-owned references on channel disconnect, and BrokerCore has an explicit in-domain `close_object_reference` operation. Protocol-level close, duplicate, explicit readiness queries, and broader stale-handle coverage remain future work after the first end-to-end path is proven. - -## Phase 5: Broker-backed fd semantics - -Move fd authority to BrokerCore while keeping guest fd numbers in local core. - -BrokerCore owns: - -- object refs; -- rights; -- reference generations; -- refcounts; -- dup/pass/close; -- inherited object tables; -- process-exit cleanup. - -The local core owns: - -- guest fd number allocation; -- raw-int fd conversion; -- typed fd wrappers; -- local ABI metadata and cached hints. - -Exit criteria: - -- Double close, stale fd, dup, inherited refs, and process-exit cleanup are tested. -- The local core cannot create a live broker object by editing local fd state. - -## Phase 6: Control/notification/data channels - -Add the durable-unicorn-style control/notification/data channel separation. - -Channels: - -- control: object operations and responses; -- notification: broker-to-process asynchronous notifications; -- data: bulk payload bytes. - -Linux hosted prototype: - -- broker creates inherited private `memfd`; -- ring header has magic/version/layout; -- broker binds ring set to authenticated runner identity; -- broker validates all ring metadata, cursors, frame bounds, and producer roles. - -Exit criteria: - -- Control channel supports concurrent request IDs. -- Event channel reports readiness/lifecycle/fail-closed events. -- Data channel can carry payloads for the first broker-owned object. -- Invalid cursor movement or malformed frames fail closed. - -## Phase 7: Host syscall profiles - -Define host syscall profiles for hosted userland. - -Profiles: - -- bootstrap profile; -- fast local profile; -- strict profile. - -Linux targets: - -- fast-futex mode: small syscall allowlist, including futex for ring/private waits; -- strict-seccomp-like mode: all mappings, fds, signal handlers, and trampoline state installed before lockdown. - -Exit criteria: - -- Direct guest `mmap`, `mprotect`, `munmap`, `mremap`, `memfd_create`, `open/openat`, `ioctl`, and `fcntl` cannot bypass broker policy after lockdown. -- Guest-visible mapping operations enter the shim/local-core path and are emulated or broker-mediated. - -## Phase 8: Filesystem BrokerService - -Add a minimal filesystem BrokerService. - -Start with a restricted virtual or host-backed filesystem. - -Broker side owns: - -- namespace roots; -- directory objects; -- open file descriptions; -- file object IDs; -- path lookup; -- permissions; -- read/write policy. - -The local core owns: - -- path string conversion; -- buffer marshalling; -- guest fd view; -- data-channel wrappers. - -Exit criteria: - -- Filesystem operations are directory-relative. -- No host fd/HANDLE is exposed to local core. -- File data uses mediated control/data channel or broker-owned ring. -- PolicyEngine can deny open/read/write independently. - -## Phase 9: Memory and mapping authority - -Move security-sensitive mapping authority broker-side. - -Broker side owns: - -- mapping object identity; -- memory grants; -- shared mappings; -- executable mapping policy; -- page-fault decisions where applicable. - -The local core owns: - -- loader helpers; -- guest pointer handling; -- cached VMA view; -- local anonymous/private scratch allocation allowed by host profile. - -Exit criteria: - -- Broker-visible mappings require BrokerCore validation and PolicyEngine authorization. -- Executable memory cannot be created without rewrite/validation policy. -- Shared memory grants cannot be forged by local core. - -## Phase 10: Multiprocess - -Implement one guest process as one sandboxed host process. - -Broker owns: - -- session identity; -- guest process identity; -- per-process channel set; -- process lifecycle; -- process-exit cleanup; -- inherited broker object table. - -Shim/local core owns: - -- ABI-specific fork semantics; -- ABI-specific exec semantics; -- guest memory replacement; -- guest fd-number presentation. - -Exit criteria: - -- Broker-mediated process creation works. -- Child process cannot impersonate parent. -- Inherited objects are explicit. -- Process exit releases broker-owned refs. - -## Phase 11: Network BrokerService - -Add broker-owned networking after filesystem and process identity are stable. - -Broker side owns: - -- socket object identity; -- port allocation; -- bind/listen/connect state; -- flow metadata; -- firewall policy context; -- TX/RX rings where enabled. - -The local core owns: - -- socket syscall facade; -- send/recv marshalling; -- local readiness cache. - -Exit criteria: - -- L3/L4 firewall policy is enforced by PolicyEngine. -- The local core cannot send or receive guest-visible network traffic directly through host network devices. -- Data path uses broker-mediated operations or broker-owned rings. - -## Phase 12: Kernel-broker deployment - -Only after userland semantics are stable, implement broker-kernel deployment. - -Separate trusted deployment code into: - -- broker-kernel user-mode support: trusted-domain support for user-mode execution and channel delivery; -- `litebox_broker_host`: broker protocol/core adaptation reused from the userland broker; -- BrokerPlatform: privileged backend execution; -- BrokerCore/BrokerServices/PolicyEngine: shared authority logic. - -Exit criteria: - -- Broker-kernel user-mode support does not decode or authorize BrokerRequest. -- `litebox_broker_host` protocol semantics and BrokerCore object semantics are reused through the same protocol-to-core adapter boundary. -- Kernel-broker deployment passes the same broker-object conformance tests as userland broker. - -## Phase 13: OP-TEE BrokerService - -Add OP-TEE-specific authority only where generic BrokerCore cannot express it. - -BrokerService owns: - -- TA/session authority; -- persistent object semantics; -- PTA access-control context; -- OP-TEE-specific lifecycle semantics. - -PolicyEngine owns: - -- final OP-TEE policy decisions and audit. - -Exit criteria: - -- OP-TEE shim remains user-mode ABI code. -- Trusted deployment does not need the full OP-TEE shim. -- OP-TEE authority cannot be created in local core. - -## Suggested first milestone - -The smallest useful milestone is: - -```text -single process -userland broker -typed broker-local adapter -control channel only -minimal PolicyEngine -broker-owned event object -local-core fd table maps guest fd -> broker reference handle -``` - -This proves the trust boundary before taking on filesystem, networking, mapping, or multiprocess complexity. - -## Validation strategy - -Add conformance tests at each layer: - -- protocol parsing rejects malformed frames; -- policy default-denies unknown operations; -- caller identity is channel-bound; -- stale reference IDs fail; -- wrong reference generation fails; -- process disconnect cleans up refs; -- local-core handle edits cannot create authority; -- shared-memory cursor/frame corruption fails closed; -- broker failure forces session failure. - -Prefer tests that run against both userland broker and later kernel-broker implementations. From ea52efe957eb177fed3307e749dd5478bd28e06f Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 15 Jun 2026 16:37:43 -0700 Subject: [PATCH 65/66] Back out non-eventfd vectored IO changes Keep eventfd-specific readv/writev handling while routing pipes and sockets through the existing generic fallback paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_shim_linux/src/syscalls/file.rs | 264 +----------------------- litebox_shim_linux/src/syscalls/net.rs | 51 +---- litebox_shim_linux/src/syscalls/pipe.rs | 5 +- 3 files changed, 10 insertions(+), 310 deletions(-) diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 427a2fb7f..019a2e880 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -7,7 +7,6 @@ use alloc::{ ffi::CString, string::{String, ToString as _}, vec, - vec::Vec, }; use litebox::{ event::{Events, wait::WaitError}, @@ -26,7 +25,7 @@ use litebox_common_linux::{ use litebox_platform_multiplex::Platform; use thiserror::Error; -use crate::{ConstPtr, GlobalState, MAX_KERNEL_BUF_SIZE, MutPtr, ShimFS, Task, syscalls::signal}; +use crate::{ConstPtr, GlobalState, MutPtr, ShimFS, Task, syscalls::signal}; use core::sync::atomic::{AtomicUsize, Ordering}; #[derive(Clone, Copy)] @@ -902,23 +901,8 @@ impl Task { .run_on_raw_fd( raw_fd, |_fd| Ok(None), - |fd| { - let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; - read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { - self.global.receive( - &self.wait_cx(), - fd, - buf, - litebox_common_linux::ReceiveFlags::empty(), - None, - ) - }) - .map(Some) - }, - |fd| { - read_linux_pipe_into_iovec(&self.global, &self.wait_cx(), fd, iovs) - .map(Some) - }, + |_fd| Ok(None), + |_fd| Ok(None), |fd| { let total_len = eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; @@ -939,27 +923,7 @@ impl Task { }) }, |_fd| Ok(None), - |fd| { - let handle = self - .global - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(Errno::EBADF)?; - handle - .with_entry(|file| { - let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; - read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { - file.recvfrom( - &self.wait_cx(), - buf, - litebox_common_linux::ReceiveFlags::empty(), - None, - ) - }) - }) - .map(Some) - }, + |_fd| Ok(None), ) .flatten()? { @@ -1145,82 +1109,6 @@ where Ok(total_read) } -/// Performs one backend read and scatters the returned bytes into user iovecs. -fn read_once_into_iovec( - iovs: &[IoReadVec

], - kernel_buffer: &mut [u8], - mut read_fn: F, -) -> Result -where - P: RawMutPointer, - F: FnMut(&mut [u8]) -> Result, -{ - let total_len = iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; - if total_len == 0 { - return Ok(0); - } - - let read_len = total_len.min(kernel_buffer.len()); - let size = read_fn(&mut kernel_buffer[..read_len])?.min(read_len); - scatter_buffer_to_iovec(iovs, &kernel_buffer[..size]) -} - -fn scatter_buffer_to_iovec

(iovs: &[IoReadVec

], data: &[u8]) -> Result -where - P: RawMutPointer, -{ - let mut copied = 0; - for iov in iovs { - if copied == data.len() { - return Ok(copied); - } - if iov.iov_len == 0 { - continue; - } - let size = (data.len() - copied).min(iov.iov_len); - if iov - .iov_base - .copy_from_slice(0, &data[copied..copied + size]) - .is_none() - { - return if copied > 0 { - Ok(copied) - } else { - Err(Errno::EFAULT) - }; - } - copied += size; - } - if copied == data.len() || copied > 0 { - Ok(copied) - } else { - Err(Errno::EFAULT) - } -} - -fn read_iovec_buffer_len

(iovs: &[IoReadVec

]) -> Result -where - P: RawMutPointer, -{ - Ok(iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?.min(MAX_KERNEL_BUF_SIZE)) -} - -fn read_linux_pipe_into_iovec( - global: &GlobalState, - cx: &litebox::event::wait::WaitContext<'_, Platform>, - fd: &TypedFd>, - iovs: &[IoReadVec

], -) -> Result -where - P: RawMutPointer, - FS: ShimFS, -{ - let mut kernel_buffer = vec![0u8; read_iovec_buffer_len(iovs)?]; - read_once_into_iovec(iovs, &mut kernel_buffer, |buf| { - global.read_linux_pipe(cx, fd, buf) - }) -} - /// Drain writes from a sequence of user iovecs. /// /// `write_fn` receives the contents of each iovec along with the total number of @@ -1271,46 +1159,6 @@ where Ok(total_written) } -fn write_linux_pipe_from_iovec( - global: &GlobalState, - cx: &litebox::event::wait::WaitContext<'_, Platform>, - fd: &TypedFd>, - iovs: &[IoWriteVec

], -) -> Result -where - P: RawConstPointer, - FS: ShimFS, -{ - let total_len = iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; - if total_len == 0 { - return Ok(0); - } - if total_len <= super::pipe::LINUX_PIPE_BUF { - let buffer = gather_iovec_to_buffer(iovs, total_len)?; - return global.write_linux_pipe(cx, fd, &buffer); - } - - write_to_iovec(iovs, |buf, _total| global.write_linux_pipe(cx, fd, buf)) -} - -fn gather_iovec_to_buffer

(iovs: &[IoWriteVec

], total_len: usize) -> Result, Errno> -where - P: RawConstPointer, -{ - let mut buffer = Vec::with_capacity(total_len); - for iov in iovs { - if iov.iov_len == 0 { - continue; - } - let Some(slice) = iov.iov_base.to_owned_slice(iov.iov_len) else { - return Err(Errno::EFAULT); - }; - buffer.extend_from_slice(slice.as_ref()); - } - debug_assert_eq!(buffer.len(), total_len); - Ok(buffer) -} - impl Task { /// Handle syscall `writev` pub(crate) fn sys_writev( @@ -1334,10 +1182,7 @@ impl Task { raw_fd, |_fd| Ok(None), |_fd| Ok(None), - |fd| { - write_linux_pipe_from_iovec(&self.global, &self.wait_cx(), fd, iovs) - .map(Some) - }, + |_fd| Ok(None), |fd| { let handle = self .global @@ -2925,105 +2770,6 @@ mod tests { assert_eq!(&output, b"hello pipe"); } - #[test] - fn small_nonblocking_pipe_writev_is_atomic() { - let task = crate::syscalls::tests::init_platform(None); - let (read_fd, write_fd) = task.sys_pipe2(OFlags::NONBLOCK).unwrap(); - let read_fd = i32::try_from(read_fd).unwrap(); - let write_fd = i32::try_from(write_fd).unwrap(); - let fill = vec![0xaa; 8192]; - - loop { - match task.sys_write(write_fd, &fill, None) { - Ok(0) => panic!("pipe write made no progress"), - Ok(_written) => {} - Err(Errno::EAGAIN) => break, - Err(error) => panic!("unexpected pipe fill error: {error:?}"), - } - } - - let mut slack = [0; 3]; - assert_eq!(task.sys_read(read_fd, &mut slack, None), Ok(slack.len())); - - let first = b"ab"; - let second = b"cd"; - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(first.as_ptr()), - iov_len: first.len(), - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(second.as_ptr()), - iov_len: second.len(), - }, - ]; - - assert_eq!( - task.sys_writev(write_fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Err(Errno::EAGAIN) - ); - } - - #[test] - fn read_once_into_iovec_stops_after_one_backend_read() { - let mut first = [0u8; 4]; - let mut second = [0u8; 4]; - let iovs = [ - IoReadVec { - iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), - iov_len: first.len(), - }, - IoReadVec { - iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), - iov_len: second.len(), - }, - ]; - let mut kernel_buffer = [0u8; 8]; - let calls = Cell::new(0); - - let result = read_once_into_iovec(&iovs, &mut kernel_buffer, |buf| { - calls.set(calls.get() + 1); - buf[..6].copy_from_slice(b"abcdef"); - Ok(6) - }); - - assert_eq!(result, Ok(6)); - assert_eq!(calls.get(), 1); - assert_eq!(&first, b"abcd"); - assert_eq!(&second, b"ef\0\0"); - } - - #[test] - fn pipe_readv_can_return_more_than_one_page_without_second_backend_read() { - let task = crate::syscalls::tests::init_platform(None); - let (read_fd, write_fd) = task.sys_pipe2(OFlags::NONBLOCK).unwrap(); - let read_fd = i32::try_from(read_fd).unwrap(); - let write_fd = i32::try_from(write_fd).unwrap(); - let input = vec![0x5a; PAGE_SIZE * 2]; - - assert_eq!(task.sys_write(write_fd, &input, None), Ok(input.len())); - - let mut first = vec![0u8; PAGE_SIZE]; - let mut second = vec![0u8; PAGE_SIZE]; - let iovs = [ - IoReadVec { - iov_base: MutPtr::from_usize(first.as_mut_ptr().expose_provenance()), - iov_len: first.len(), - }, - IoReadVec { - iov_base: MutPtr::from_usize(second.as_mut_ptr().expose_provenance()), - iov_len: second.len(), - }, - ]; - - assert_eq!( - task.sys_readv(read_fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Ok(input.len()) - ); - assert_eq!(first, vec![0x5a; PAGE_SIZE]); - assert_eq!(second, vec![0x5a; PAGE_SIZE]); - } - #[test] fn read_from_iovec_breaks_on_eof() { let mut first = [0u8; 4]; diff --git a/litebox_shim_linux/src/syscalls/net.rs b/litebox_shim_linux/src/syscalls/net.rs index 685678ead..997ccb632 100644 --- a/litebox_shim_linux/src/syscalls/net.rs +++ b/litebox_shim_linux/src/syscalls/net.rs @@ -2811,10 +2811,10 @@ mod unix_tests { use core::time::Duration; use alloc::{string::ToString, vec::Vec}; - use litebox::{event::Events, mm::linux::PAGE_SIZE, platform::RawConstPointer}; + use litebox::{event::Events, platform::RawConstPointer}; use litebox_common_linux::{ - AddressFamily, AtFlags, IoReadVec, ReceiveFlags, SendFlags, SockFlags, SockType, - SocketOption, SocketOptionName, TimeParam, errno::Errno, + AddressFamily, AtFlags, ReceiveFlags, SendFlags, SockFlags, SockType, SocketOption, + SocketOptionName, TimeParam, errno::Errno, }; use crate::{ @@ -3290,51 +3290,6 @@ mod unix_tests { unix_socketpair_bidirectional(SockType::Datagram, true); } - #[test] - fn test_unix_datagram_socketpair_readv_spans_iovecs() { - let task = init_platform(None); - let mut sv_ptr = alloc::vec![0u32; 2]; - let sv_mut_ptr = MutPtr::from_usize(sv_ptr.as_mut_ptr() as usize); - let ty_and_flags = SockType::Datagram as u32 | SockFlags::NONBLOCK.bits(); - task.sys_socketpair(AddressFamily::UNIX as u32, ty_and_flags, 0, sv_mut_ptr) - .unwrap(); - - let sender = sv_ptr[0]; - let receiver = sv_ptr[1]; - let input = alloc::vec![0x7b; PAGE_SIZE * 2]; - assert_eq!( - task.do_sendto(sender, &input, SendFlags::empty(), None), - Ok(input.len()) - ); - - let mut first = alloc::vec![0u8; PAGE_SIZE]; - let mut second = alloc::vec![0u8; PAGE_SIZE]; - let iovs = [ - IoReadVec { - iov_base: MutPtr::from_usize(first.as_mut_ptr() as usize), - iov_len: first.len(), - }, - IoReadVec { - iov_base: MutPtr::from_usize(second.as_mut_ptr() as usize), - iov_len: second.len(), - }, - ]; - - assert_eq!( - task.sys_readv( - i32::try_from(receiver).unwrap(), - ConstPtr::from_ptr(iovs.as_ptr()), - iovs.len() - ), - Ok(input.len()) - ); - assert_eq!(first, alloc::vec![0x7b; PAGE_SIZE]); - assert_eq!(second, alloc::vec![0x7b; PAGE_SIZE]); - - close_socket(&task, sender); - close_socket(&task, receiver); - } - fn unix_socket_recv_timeout(ty: SockType) { let task = init_platform(None); let (sock1, _sock2) = task diff --git a/litebox_shim_linux/src/syscalls/pipe.rs b/litebox_shim_linux/src/syscalls/pipe.rs index c9d3ff6b5..90273c164 100644 --- a/litebox_shim_linux/src/syscalls/pipe.rs +++ b/litebox_shim_linux/src/syscalls/pipe.rs @@ -19,7 +19,6 @@ use litebox_common_linux::{FileDescriptorFlags, InodeType, errno::Errno}; use crate::{GlobalState, Platform, ShimFS}; -pub(crate) const LINUX_PIPE_BUF: usize = 4096; const DEFAULT_PIPE_BUF_SIZE: usize = 1024 * 1024; /// Status flags for Linux pipe file descriptions. @@ -57,8 +56,8 @@ impl GlobalState { let (writer, reader) = self.pipes.create_pipe( DEFAULT_PIPE_BUF_SIZE, pipe_flags, - // See `man 7 pipe` for `PIPE_BUF`. - NonZero::new(LINUX_PIPE_BUF), + // See `man 7 pipe` for `PIPE_BUF`. On Linux, this is 4096. + NonZero::new(4096), ); let initial_status = OFlags::from(pipe_flags); From e1d740766c71ff5a2ec9763de84e8abac54f57ed Mon Sep 17 00:00:00 2001 From: Weidong Cui Date: Mon, 15 Jun 2026 17:08:45 -0700 Subject: [PATCH 66/66] Simplify eventfd fixture IO paths Use plain read/write in the eventfd fixture and remove eventfd-specific readv/writev shim handling that is not needed for the broker POC. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- litebox_runner_linux_userland/tests/eventfd.c | 104 +---- litebox_shim_linux/src/syscalls/eventfd.rs | 37 +- litebox_shim_linux/src/syscalls/file.rs | 357 +----------------- 3 files changed, 28 insertions(+), 470 deletions(-) diff --git a/litebox_runner_linux_userland/tests/eventfd.c b/litebox_runner_linux_userland/tests/eventfd.c index e243f54b1..5ed53b194 100644 --- a/litebox_runner_linux_userland/tests/eventfd.c +++ b/litebox_runner_linux_userland/tests/eventfd.c @@ -2,13 +2,10 @@ // Licensed under the MIT license. #include -#include #include #include -#include #include #include -#include #include static int expect_eagain_read(int fd) { @@ -48,52 +45,6 @@ static int expect_poll_events(int fd, short expected) { return 0; } -static int writev_values(int fd, uint64_t first, uint64_t second) { - struct iovec iov[2] = { - {&first, sizeof(first)}, - {&second, sizeof(second)}, - }; - return writev(fd, iov, 2) == (ssize_t)(sizeof(first) + sizeof(second)) ? 0 : 1; -} - -static int readv_split_value(int fd, uint64_t expected) { - uint8_t bytes[sizeof(expected)] = {0}; - struct iovec iov[2] = { - {bytes, 4}, - {bytes + 4, 4}, - }; - uint64_t value = 0; - if (readv(fd, iov, 2) != (ssize_t)sizeof(value)) { - return 1; - } - memcpy(&value, bytes, sizeof(value)); - return value == expected ? 0 : 2; -} - -static int expect_einval_short_readv(int fd) { - uint32_t value = 0; - struct iovec iov = {&value, sizeof(value)}; - errno = 0; - if (readv(fd, &iov, 1) != -1) { - return 1; - } - return errno == EINVAL ? 0 : 2; -} - -static int expect_einval_split_writev(int fd) { - uint32_t low = 1; - uint32_t high = 0; - struct iovec iov[2] = { - {&low, sizeof(low)}, - {&high, sizeof(high)}, - }; - errno = 0; - if (writev(fd, iov, 2) != -1) { - return 1; - } - return errno == EINVAL ? 0 : 2; -} - static int expect_eagain_write(int fd, uint64_t value) { errno = 0; if (write(fd, &value, sizeof(value)) != -1) { @@ -102,14 +53,6 @@ static int expect_eagain_write(int fd, uint64_t value) { return errno == EAGAIN ? 0 : 2; } -static int clear_nonblock_with_fcntl(int fd) { - int flags = fcntl(fd, F_GETFL); - if (flags < 0) { - return 1; - } - return fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) == 0 ? 0 : 2; -} - static int clear_nonblock_with_ioctl(int fd) { int nonblock = 0; return ioctl(fd, FIONBIO, &nonblock) == 0 ? 0 : 1; @@ -138,76 +81,61 @@ int main(void) { if (expect_poll_events(fd, POLLOUT) != 0) { return 16; } - if (writev_values(fd, 2, 5) != 0) { + if (write_value(fd, 2) != 0) { return 17; } - if (read_value(fd, 7) != 0) { + if (write_value(fd, 5) != 0) { return 18; } - if (write_value(fd, 9) != 0) { + if (read_value(fd, 7) != 0) { return 19; } - if (readv_split_value(fd, 9) != 0) { + if (write_value(fd, 9) != 0) { return 20; } - if (expect_einval_split_writev(fd) != 0) { + if (read_value(fd, 9) != 0) { return 21; } if (write_value(fd, 11) != 0) { return 22; } - if (expect_einval_short_readv(fd) != 0) { - return 23; - } if (read_value(fd, 11) != 0) { - return 24; + return 23; } if (expect_eagain_read(fd) != 0) { - return 25; + return 24; } uint64_t invalid = UINT64_MAX; errno = 0; if (write(fd, &invalid, sizeof(invalid)) != -1 || errno != EINVAL) { - return 26; + return 25; } if (write_value(fd, UINT64_MAX - 1) != 0) { - return 27; + return 26; } if (expect_poll_events(fd, POLLIN) != 0) { - return 28; + return 27; } if (expect_eagain_write(fd, 1) != 0) { - return 29; + return 28; } if (read_value(fd, UINT64_MAX - 1) != 0) { - return 30; + return 29; } if (expect_poll_events(fd, POLLOUT) != 0) { - return 31; + return 30; } close(fd); - int fcntl_toggle_fd = eventfd(1, EFD_NONBLOCK); - if (fcntl_toggle_fd < 0) { - return 32; - } - if (clear_nonblock_with_fcntl(fcntl_toggle_fd) != 0) { - return 33; - } - if (read_value(fcntl_toggle_fd, 1) != 0) { - return 34; - } - close(fcntl_toggle_fd); - int ioctl_toggle_fd = eventfd(1, EFD_NONBLOCK); if (ioctl_toggle_fd < 0) { - return 35; + return 31; } if (clear_nonblock_with_ioctl(ioctl_toggle_fd) != 0) { - return 36; + return 32; } if (read_value(ioctl_toggle_fd, 1) != 0) { - return 37; + return 33; } close(ioctl_toggle_fd); diff --git a/litebox_shim_linux/src/syscalls/eventfd.rs b/litebox_shim_linux/src/syscalls/eventfd.rs index 5bd466d63..0102a5a8e 100644 --- a/litebox_shim_linux/src/syscalls/eventfd.rs +++ b/litebox_shim_linux/src/syscalls/eventfd.rs @@ -61,13 +61,6 @@ impl EventFileCounter bool { - match self { - Self::ShimLocal { .. } => true, - Self::LocalCore(counter) => counter.supports_blocking_operations(), - } - } - fn read( &self, cx: &WaitContext<'_, Platform>, @@ -188,16 +181,6 @@ impl EventFile { super::common_functions_for_file_status!(); - pub(crate) fn set_status_flags(&self, requested: OFlags, mask: OFlags) -> Result<(), Errno> { - let new_status = (self.get_status() & mask.complement()) | (requested & mask); - if !new_status.contains(OFlags::NONBLOCK) && !self.counter.supports_blocking_operations() { - return Err(Errno::EINVAL); - } - self.set_status(requested & mask, true); - self.set_status(requested.complement() & mask, false); - Ok(()) - } - fn is_nonblocking(&self) -> bool { self.get_status().contains(OFlags::NONBLOCK) } @@ -249,7 +232,7 @@ fn consume_mode(semaphore: bool) -> EventCounterReadMode { #[cfg(test)] mod tests { - use litebox::{event::wait::WaitState, fs::OFlags}; + use litebox::event::wait::WaitState; use litebox_common_linux::{EfdFlags, errno::Errno}; use litebox_platform_multiplex::platform; @@ -356,22 +339,4 @@ mod tests { ); assert_eq!(eventfd.read(&WaitState::new(platform()).context()), Ok(1)); } - - #[test] - fn test_shim_local_eventfd_can_be_made_nonblocking() { - let task = crate::syscalls::tests::init_platform(None); - - let eventfd = task - .global - .create_linux_eventfd(0, EfdFlags::empty()) - .unwrap(); - - eventfd - .set_status_flags(OFlags::NONBLOCK, OFlags::NONBLOCK) - .unwrap(); - assert_eq!( - eventfd.read(&WaitState::new(platform()).context()), - Err(Errno::EAGAIN) - ); - } } diff --git a/litebox_shim_linux/src/syscalls/file.rs b/litebox_shim_linux/src/syscalls/file.rs index 019a2e880..3560d8956 100644 --- a/litebox_shim_linux/src/syscalls/file.rs +++ b/litebox_shim_linux/src/syscalls/file.rs @@ -894,43 +894,6 @@ impl Task { self.check_raw_fd_exists(fd)?; check_iovcnt(iovcnt)?; let iovs: &[IoReadVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; - let raw_fd = usize::try_from(fd).map_err(|_| Errno::EBADF)?; - { - let files = self.files.borrow(); - if let Some(size) = files - .run_on_raw_fd( - raw_fd, - |_fd| Ok(None), - |_fd| Ok(None), - |_fd| Ok(None), - |fd| { - let total_len = - eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?; - if total_len == 0 { - return Ok(Some(0)); - } - validate_eventfd_iovec_len(total_len)?; - let handle = self - .global - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(Errno::EBADF)?; - handle.with_entry(|file| { - let value = file.read(&self.wait_cx())?; - read_eventfd_value_to_iovec(iovs, value)?; - Ok(Some(8)) - }) - }, - |_fd| Ok(None), - |_fd| Ok(None), - ) - .flatten()? - { - return Ok(size); - } - } - let mut kernel_buffer = vec![0u8; PAGE_SIZE]; // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with @@ -972,10 +935,6 @@ fn check_iovcnt(iovcnt: usize) -> Result<(), Errno> { } fn check_iov_lens(iov_lens: impl IntoIterator) -> Result<(), Errno> { - iovec_total_len(iov_lens).map(|_| ()) -} - -fn iovec_total_len(iov_lens: impl IntoIterator) -> Result { let mut total = 0usize; for iov_len in iov_lens { total = total.checked_add(iov_len).ok_or(Errno::EINVAL)?; @@ -983,87 +942,9 @@ fn iovec_total_len(iov_lens: impl IntoIterator) -> Result) -> Result { - iovec_total_len(iov_lens) -} - -fn validate_eventfd_iovec_len(total_len: usize) -> Result<(), Errno> { - if total_len < size_of::() { - return Err(Errno::EINVAL); - } Ok(()) } -fn read_eventfd_value_to_iovec(iovs: &[IoReadVec>], value: u64) -> Result<(), Errno> { - validate_eventfd_iovec_len(eventfd_iovec_total_len(iovs.iter().map(|iov| iov.iov_len))?)?; - - let bytes = value.to_ne_bytes(); - let mut copied = 0; - for iov in iovs { - if iov.iov_len == 0 { - continue; - } - let size = (size_of::() - copied).min(iov.iov_len); - iov.iov_base - .copy_from_slice(0, &bytes[copied..copied + size]) - .ok_or(Errno::EFAULT)?; - copied += size; - if copied == size_of::() { - return Ok(()); - } - } - Err(Errno::EINVAL) -} - -fn write_eventfd_from_iovec( - iovs: &[IoWriteVec>], - mut write_value: F, -) -> Result -where - F: FnMut(u64) -> Result, -{ - check_iov_lens(iovs.iter().map(|iov| iov.iov_len))?; - - let Some(first_non_empty) = iovs.iter().position(|iov| iov.iov_len != 0) else { - return Ok(0); - }; - if first_non_empty != 0 { - return Err(Errno::EINVAL); - } - - let bail = |total: usize, e: Errno| if total > 0 { Ok(total) } else { Err(e) }; - let mut total_written = 0; - for iov in iovs { - if iov.iov_len == 0 { - continue; - } - if iov.iov_len != size_of::() { - return bail(total_written, Errno::EINVAL); - } - let Some(slice) = iov.iov_base.to_owned_slice(iov.iov_len) else { - return bail(total_written, Errno::EFAULT); - }; - let value = u64::from_ne_bytes( - slice - .as_ref() - .try_into() - .expect("eventfd writev validated an eight-byte iovec"), - ); - let size = match write_value(value) { - Ok(size) => size, - Err(err) => return bail(total_written, err), - }; - total_written += size; - if size < iov.iov_len { - break; - } - } - Ok(total_written) -} - /// Drain reads into a sequence of user iovecs. fn read_from_iovec( iovs: &[IoReadVec

], @@ -1171,39 +1052,9 @@ impl Task { check_iovcnt(iovcnt)?; let iovs: &[IoWriteVec>] = &iovec.to_owned_slice(iovcnt).ok_or(Errno::EFAULT)?; - let raw_fd = usize::try_from(fd).map_err(|_| Errno::EBADF)?; // TODO: The data transfers performed by readv() and writev() are atomic: the data // written by writev() is written as a single block that is not intermingled with // output from writes in other processes - { - let files = self.files.borrow(); - if let Some(size) = files - .run_on_raw_fd( - raw_fd, - |_fd| Ok(None), - |_fd| Ok(None), - |_fd| Ok(None), - |fd| { - let handle = self - .global - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(Errno::EBADF)?; - write_eventfd_from_iovec(iovs, |value| { - handle.with_entry(|file| file.write(&self.wait_cx(), value)) - }) - .map(Some) - }, - |_fd| Ok(None), - |_fd| Ok(None), - ) - .flatten()? - { - return Ok(size); - } - } - write_to_iovec(iovs, |buf, _total| self.sys_write(fd, buf, None)) } @@ -1716,19 +1567,8 @@ impl Task { .set_linux_pipe_status_flags(fd, flags, setfl_mask) }, |fd| { - let handle = self - .global - .litebox - .descriptor_table() - .entry_handle(fd) - .ok_or(Errno::EBADF)?; - handle.with_entry(|file| { - let diff = (file.get_status() & setfl_mask) ^ flags; - if diff.intersects(OFlags::APPEND | OFlags::DIRECT | OFlags::NOATIME) { - log_unsupported!("unsupported flags"); - } - file.set_status_flags(flags, setfl_mask) - }) + toggle_flags!(fd); + Ok(()) }, |_fd| todo!("epoll"), |fd| { @@ -1888,6 +1728,12 @@ impl Task { } pub fn sys_eventfd2(&self, initval: u32, flags: EfdFlags) -> Result { + if flags + .intersects((EfdFlags::SEMAPHORE | EfdFlags::CLOEXEC | EfdFlags::NONBLOCK).complement()) + { + return Err(Errno::EINVAL); + } + let eventfd = self.global.create_linux_eventfd(initval, flags)?; let mut dt = self.global.litebox.descriptor_table_mut(); let typed = dt.insert::(eventfd); @@ -2017,14 +1863,10 @@ impl Task { .descriptor_table() .entry_handle(fd) .ok_or(Errno::EBADF)?; - let requested = if val != 0 { - OFlags::NONBLOCK - } else { - OFlags::empty() - }; handle.with_entry(|file| { - file.set_status_flags(requested, OFlags::NONBLOCK) - }) + file.set_status(OFlags::NONBLOCK, val != 0); + }); + Ok(()) }, |fd| { let handle = self @@ -2736,40 +2578,6 @@ mod tests { assert_eq!(calls.get(), 2); } - #[test] - fn writev_uses_generic_write_path_for_pipes() { - let task = crate::syscalls::tests::init_platform(None); - let (read_fd, write_fd) = task.sys_pipe2(OFlags::empty()).unwrap(); - let first = b"hello "; - let second = b"pipe"; - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(first.as_ptr()), - iov_len: first.len(), - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(second.as_ptr()), - iov_len: second.len(), - }, - ]; - - assert_eq!( - task.sys_writev( - i32::try_from(write_fd).unwrap(), - ConstPtr::from_ptr(iovs.as_ptr()), - iovs.len() - ), - Ok(first.len() + second.len()) - ); - - let mut output = [0u8; 10]; - assert_eq!( - task.sys_read(i32::try_from(read_fd).unwrap(), &mut output, None), - Ok(output.len()) - ); - assert_eq!(&output, b"hello pipe"); - } - #[test] fn read_from_iovec_breaks_on_eof() { let mut first = [0u8; 4]; @@ -2863,149 +2671,6 @@ mod tests { assert_eq!(result, Ok(4)); assert_eq!(calls.get(), 2); assert_eq!(&first, b"xxxx"); - assert_eq!(&second, &[0u8; 4]); - } - - #[test] - fn eventfd_writev_rejects_split_value() { - let task = crate::syscalls::tests::init_platform(None); - let fd = task - .sys_eventfd2(0, EfdFlags::empty()) - .expect("eventfd2 failed"); - let fd = i32::try_from(fd).unwrap(); - let value = 0x0102_0304_0506_0708u64; - let bytes = value.to_ne_bytes(); - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(bytes.as_ptr()), - iov_len: 3, - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(bytes[3..].as_ptr()), - iov_len: 5, - }, - ]; - - assert_eq!( - task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Err(Errno::EINVAL) - ); - } - - #[test] - fn eventfd_readv_all_zero_iovecs_is_noop() { - let task = crate::syscalls::tests::init_platform(None); - let fd = task - .sys_eventfd2(5, EfdFlags::empty()) - .expect("eventfd2 failed"); - let fd = i32::try_from(fd).unwrap(); - let mut output = [0u8; 8]; - let iovs = [ - IoReadVec { - iov_base: MutPtr::from_usize(output.as_mut_ptr().expose_provenance()), - iov_len: 0, - }, - IoReadVec { - iov_base: MutPtr::from_usize(output.as_mut_ptr().expose_provenance()), - iov_len: 0, - }, - ]; - - assert_eq!( - task.sys_readv(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Ok(0) - ); - - assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); - assert_eq!(u64::from_ne_bytes(output), 5); - } - - #[test] - fn eventfd_writev_rejects_leading_zero_before_value() { - let task = crate::syscalls::tests::init_platform(None); - let fd = task - .sys_eventfd2(5, EfdFlags::empty()) - .expect("eventfd2 failed"); - let fd = i32::try_from(fd).unwrap(); - let value = 7u64.to_ne_bytes(); - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(value.as_ptr()), - iov_len: 0, - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(value.as_ptr()), - iov_len: value.len(), - }, - ]; - - assert_eq!( - task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Err(Errno::EINVAL) - ); - - let mut output = [0u8; 8]; - assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); - assert_eq!(u64::from_ne_bytes(output), 5); - } - - #[test] - fn eventfd_writev_all_zero_iovecs_is_noop() { - let task = crate::syscalls::tests::init_platform(None); - let fd = task - .sys_eventfd2(5, EfdFlags::empty()) - .expect("eventfd2 failed"); - let fd = i32::try_from(fd).unwrap(); - let value = 7u64.to_ne_bytes(); - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(value.as_ptr()), - iov_len: 0, - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(value.as_ptr()), - iov_len: 0, - }, - ]; - - assert_eq!( - task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Ok(0) - ); - - let mut output = [0u8; 8]; - assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); - assert_eq!(u64::from_ne_bytes(output), 5); - } - - #[test] - fn eventfd_writev_writes_each_full_value() { - let task = crate::syscalls::tests::init_platform(None); - let fd = task - .sys_eventfd2(0, EfdFlags::empty()) - .expect("eventfd2 failed"); - let fd = i32::try_from(fd).unwrap(); - let first = 7u64.to_ne_bytes(); - let second = 11u64.to_ne_bytes(); - let iovs = [ - IoWriteVec { - iov_base: ConstPtr::from_ptr(first.as_ptr()), - iov_len: first.len(), - }, - IoWriteVec { - iov_base: ConstPtr::from_ptr(second.as_ptr()), - iov_len: second.len(), - }, - ]; - - assert_eq!( - task.sys_writev(fd, ConstPtr::from_ptr(iovs.as_ptr()), iovs.len()), - Ok(16) - ); - - let mut output = [0u8; 8]; - assert_eq!(task.sys_read(fd, &mut output, None), Ok(8)); - assert_eq!(u64::from_ne_bytes(output), 18); } #[test]