From 28b5258ea336b0e7c3c15ab985148af925c87a95 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Tue, 22 Sep 2026 15:38:30 +0200 Subject: [PATCH 1/3] pldm: Add IPC API crate (wire format, types, transport seam) Defines the binary protocol the orchestrator uses to talk to the PLDM Firmware Device over IPC. Twelve operations covering the full update lifecycle: offer accept/reject, verify/apply grant/deny, activation, SVN commit, cancel ack, and status query. Wire format: fixed 8-byte headers for both request and response, with manual byte encoding (no zerocopy dep) for no_std compatibility. The doc's response diagram adds to 6 bytes; the prose says "Fixed 8-byte header," so we pad 2 reserved bytes to match the stated intent. The doc field `gen` is renamed to `generation` because `gen` is a reserved keyword in Rust 2024 edition. All four Deny* ops carry a DenyReason byte for uniformity (the doc pins DenySvnCommit to PolicyViolation, but sending the reason on the wire keeps the decode path identical to the other denials). The FdStatus enum encodes the FD's current condition as a QueryStatus response payload, covering all DSP0267 states plus pending decisions. OfferPending carries target, total, transfer mode, and the SVN delayed flag from the UA's UpdateComponent request, so the orchestrator can validate against the floor. The Transport trait is the seam the client crate will be generic over, split into start/poll/cancel rather than one blocking round-trip: the orchestrator client runs in the event loop, which must not block. In production it is backed by util/ipc's AsyncTransaction; host tests use LoopbackTransport. Host-buildable with no kernel dependencies. All encode/decode paths have roundtrip tests. Assisted-by: Claude Code --- services/pldm/ipc-api/BUILD.bazel | 16 + services/pldm/ipc-api/src/error.rs | 138 ++++++ services/pldm/ipc-api/src/lib.rs | 23 + services/pldm/ipc-api/src/status.rs | 342 +++++++++++++ services/pldm/ipc-api/src/transport.rs | 66 +++ services/pldm/ipc-api/src/wire.rs | 639 +++++++++++++++++++++++++ 6 files changed, 1224 insertions(+) create mode 100644 services/pldm/ipc-api/BUILD.bazel create mode 100644 services/pldm/ipc-api/src/error.rs create mode 100644 services/pldm/ipc-api/src/lib.rs create mode 100644 services/pldm/ipc-api/src/status.rs create mode 100644 services/pldm/ipc-api/src/transport.rs create mode 100644 services/pldm/ipc-api/src/wire.rs diff --git a/services/pldm/ipc-api/BUILD.bazel b/services/pldm/ipc-api/BUILD.bazel new file mode 100644 index 00000000..076874b2 --- /dev/null +++ b/services/pldm/ipc-api/BUILD.bazel @@ -0,0 +1,16 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "pldm_ipc_api", + srcs = glob(["src/**/*.rs"]), + edition = "2024", + visibility = ["//visibility:public"], +) + +rust_test( + name = "pldm_ipc_api_test", + crate = ":pldm_ipc_api", +) diff --git a/services/pldm/ipc-api/src/error.rs b/services/pldm/ipc-api/src/error.rs new file mode 100644 index 00000000..50311c56 --- /dev/null +++ b/services/pldm/ipc-api/src/error.rs @@ -0,0 +1,138 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Error types for the PLDM IPC wire protocol. + +use core::fmt; + +/// Wire-level decode/encode error. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WireError { + /// Output buffer too small for the encoded message. + BufferTooSmall, + /// Payload exceeds the maximum allowed size. + PayloadTooLarge, + /// Unrecognized operation code. + InvalidOpcode(u8), + /// Input buffer too short for a complete header or payload. + Truncated, + /// Unrecognized enum value (status discriminant, deny reason, etc). + InvalidValue(u8), +} + +impl fmt::Display for WireError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::BufferTooSmall => f.write_str("buffer too small"), + Self::PayloadTooLarge => f.write_str("payload too large"), + Self::InvalidOpcode(op) => write!(f, "invalid opcode 0x{op:02x}"), + Self::Truncated => f.write_str("truncated"), + Self::InvalidValue(v) => write!(f, "invalid value 0x{v:02x}"), + } + } +} + +/// On-wire response code from the FD. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ResponseCode { + /// Operation completed successfully. + Success = 0, + /// Internal server error. + InternalError = 1, + /// Unrecognized opcode (InvalidOp on the wire). + InvalidOp = 2, + /// Operation not valid in the FD's current phase. + WrongPhase = 3, +} + +impl ResponseCode { + pub const fn is_success(self) -> bool { + matches!(self, Self::Success) + } + + pub const fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::Success), + 1 => Some(Self::InternalError), + 2 => Some(Self::InvalidOp), + 3 => Some(Self::WrongPhase), + _ => None, + } + } +} + +impl fmt::Display for ResponseCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Success => f.write_str("success"), + Self::InternalError => f.write_str("internal error"), + Self::InvalidOp => f.write_str("invalid op"), + Self::WrongPhase => f.write_str("wrong phase"), + } + } +} + +/// Reason the orchestrator denied an operation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum DenyReason { + /// Component is isolated (compromise detected). + Isolated = 0, + /// Update policy violation. + PolicyViolation = 1, + /// Component identifier not recognized. + UnknownTarget = 2, + /// Another operation is in progress. + Busy = 3, +} + +impl DenyReason { + pub const fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::Isolated), + 1 => Some(Self::PolicyViolation), + 2 => Some(Self::UnknownTarget), + 3 => Some(Self::Busy), + _ => None, + } + } +} + +impl fmt::Display for DenyReason { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Isolated => f.write_str("isolated"), + Self::PolicyViolation => f.write_str("policy violation"), + Self::UnknownTarget => f.write_str("unknown target"), + Self::Busy => f.write_str("busy"), + } + } +} + +/// Error returned to the orchestrator's client layer. +/// +/// Wraps the on-wire `ResponseCode`, the way `MctpError` wraps +/// `mctp_api::ResponseCode`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PldmIpcError { + pub code: ResponseCode, +} + +impl PldmIpcError { + pub const fn from_code(code: ResponseCode) -> Self { + Self { code } + } +} + +impl fmt::Display for PldmIpcError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "pldm ipc error: {}", self.code) + } +} + +impl From for PldmIpcError { + fn from(code: ResponseCode) -> Self { + Self::from_code(code) + } +} diff --git a/services/pldm/ipc-api/src/lib.rs b/services/pldm/ipc-api/src/lib.rs new file mode 100644 index 00000000..03a56097 --- /dev/null +++ b/services/pldm/ipc-api/src/lib.rs @@ -0,0 +1,23 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! PLDM IPC API: wire format, types, and transport seam. +//! +//! Defines the binary protocol the orchestrator uses to talk to the +//! PLDM Firmware Device over IPC. Host-buildable, no kernel +//! dependencies. The server and client crates depend on this for +//! shared types; neither re-invents the encoding. + +#![no_std] + +pub mod error; +pub mod status; +pub mod transport; +pub mod wire; + +pub use error::{DenyReason, PldmIpcError, ResponseCode, WireError}; +pub use status::{FdStatus, TransferMode}; +pub use transport::{Transport, TransportError}; +pub use wire::{ + PldmOp, RequestHeader, ResponseHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; diff --git a/services/pldm/ipc-api/src/status.rs b/services/pldm/ipc-api/src/status.rs new file mode 100644 index 00000000..aee92a93 --- /dev/null +++ b/services/pldm/ipc-api/src/status.rs @@ -0,0 +1,342 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! FD status for QueryStatus responses. +//! +//! The status payload follows the response header and carries the FD's +//! current condition. The orchestrator always follows a nudge with +//! QueryStatus to learn what happened, so the status is the primary +//! communication channel from the FD. + +use crate::error::WireError; + +/// In-transport vs out-of-transport image transfer. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum TransferMode { + /// FD pulls firmware chunks from the UA over MCTP. + InTransport = 0, + /// A third party writes the image to staging before verify. + OutOfTransport = 1, +} + +impl TransferMode { + pub const fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::InTransport), + 1 => Some(Self::OutOfTransport), + _ => None, + } + } +} + +/// Current condition of the FD, returned by QueryStatus. +/// +/// Some variants map to DSP0267 states (Idle, ReadyXfer), some to +/// pending decisions the orchestrator owes the FD (OfferPending, +/// VerifyPending, ApplyPending, ActivationPending, SvnCommitPending), +/// and PhaseFailed is a verify/apply failure the UA has not yet +/// cancelled. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FdStatus { + /// No update in progress. `reason` is the DSP0267 + /// GetStatusReasonCode (0 = Initialization, others per spec). + Idle { reason: u8 }, + + /// UA has sent UpdateComponent, FD is ready for transfer. + ReadyXfer, + + /// FD has an offer the orchestrator has not yet accepted or + /// rejected. `target` is the PLDM component identifier, `total` + /// is the image size in bytes. `svn_delayed` is true when the UA + /// requested delayed SVN update (DSP0267 bit 9). + OfferPending { + target: u16, + total: u32, + mode: TransferMode, + svn_delayed: bool, + }, + + /// Transfer complete, FD waiting for GrantVerify. + VerifyPending, + + /// Verify complete, FD waiting for GrantApply. + ApplyPending, + + /// Apply complete, FD waiting for GrantActivate. + ActivationPending, + + /// UA sent UpdateSecurityRevision, FD waiting for + /// GrantSvnCommit. `component` is the target identifier. + SvnCommitPending { component: u16 }, + + /// Verify or apply failed. `phase` and `result_code` are the + /// DSP0267 values the FD already sent the UA. + PhaseFailed { phase: u8, result_code: u8 }, + + /// UA sent CancelUpdate, FD waiting for AckCancel. + Cancelled, +} + +/// Wire tag for [`FdStatus`] variants. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +enum Tag { + Idle = 0, + ReadyXfer = 1, + OfferPending = 2, + VerifyPending = 3, + ApplyPending = 4, + ActivationPending = 5, + SvnCommitPending = 6, + PhaseFailed = 7, + Cancelled = 8, +} + +impl Tag { + const fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::Idle), + 1 => Some(Self::ReadyXfer), + 2 => Some(Self::OfferPending), + 3 => Some(Self::VerifyPending), + 4 => Some(Self::ApplyPending), + 5 => Some(Self::ActivationPending), + 6 => Some(Self::SvnCommitPending), + 7 => Some(Self::PhaseFailed), + 8 => Some(Self::Cancelled), + _ => None, + } + } +} + +/// Encode a single-byte (tag-only) variant. +fn encode_tag(buf: &mut [u8], tag: Tag) -> Result { + if buf.is_empty() { + return Err(WireError::BufferTooSmall); + } + buf[0] = tag as u8; + Ok(1) +} + +impl FdStatus { + /// Maximum encoded size of a status payload (OfferPending: 9 bytes). + pub const MAX_SIZE: usize = 9; + + /// Encode into `buf`, returning the number of bytes written. + pub fn encode(&self, buf: &mut [u8]) -> Result { + match *self { + Self::Idle { reason } => { + if buf.len() < 2 { + return Err(WireError::BufferTooSmall); + } + buf[0] = Tag::Idle as u8; + buf[1] = reason; // DSP0267 GetStatusReasonCode + Ok(2) + } + Self::ReadyXfer => encode_tag(buf, Tag::ReadyXfer), + Self::OfferPending { + target, + total, + mode, + svn_delayed, + } => { + if buf.len() < Self::MAX_SIZE { + return Err(WireError::BufferTooSmall); + } + buf[0] = Tag::OfferPending as u8; + buf[1..3].copy_from_slice(&target.to_le_bytes()); // component id + buf[3..7].copy_from_slice(&total.to_le_bytes()); // image size + buf[7] = mode as u8; + buf[8] = svn_delayed as u8; + Ok(Self::MAX_SIZE) + } + Self::VerifyPending => encode_tag(buf, Tag::VerifyPending), + Self::ApplyPending => encode_tag(buf, Tag::ApplyPending), + Self::ActivationPending => encode_tag(buf, Tag::ActivationPending), + Self::SvnCommitPending { component } => { + if buf.len() < 3 { + return Err(WireError::BufferTooSmall); + } + buf[0] = Tag::SvnCommitPending as u8; + buf[1..3].copy_from_slice(&component.to_le_bytes()); // component id + Ok(3) + } + Self::PhaseFailed { phase, result_code } => { + if buf.len() < 3 { + return Err(WireError::BufferTooSmall); + } + buf[0] = Tag::PhaseFailed as u8; + buf[1] = phase; // DSP0267 phase code + buf[2] = result_code; // DSP0267 result code + Ok(3) + } + Self::Cancelled => encode_tag(buf, Tag::Cancelled), + } + } + + /// Decode from `buf`. + pub fn decode(buf: &[u8]) -> Result { + if buf.is_empty() { + return Err(WireError::Truncated); + } + let tag = Tag::from_u8(buf[0]).ok_or(WireError::InvalidValue(buf[0]))?; + match tag { + Tag::Idle => { + if buf.len() < 2 { + return Err(WireError::Truncated); + } + Ok(Self::Idle { reason: buf[1] }) + } + Tag::ReadyXfer => Ok(Self::ReadyXfer), + Tag::OfferPending => { + if buf.len() < Self::MAX_SIZE { + return Err(WireError::Truncated); + } + let target = u16::from_le_bytes([buf[1], buf[2]]); + let total = u32::from_le_bytes([buf[3], buf[4], buf[5], buf[6]]); + let mode = TransferMode::from_u8(buf[7]).ok_or(WireError::InvalidValue(buf[7]))?; + let svn_delayed = buf[8] != 0; + Ok(Self::OfferPending { + target, + total, + mode, + svn_delayed, + }) + } + Tag::VerifyPending => Ok(Self::VerifyPending), + Tag::ApplyPending => Ok(Self::ApplyPending), + Tag::ActivationPending => Ok(Self::ActivationPending), + Tag::SvnCommitPending => { + if buf.len() < 3 { + return Err(WireError::Truncated); + } + let component = u16::from_le_bytes([buf[1], buf[2]]); + Ok(Self::SvnCommitPending { component }) + } + Tag::PhaseFailed => { + if buf.len() < 3 { + return Err(WireError::Truncated); + } + Ok(Self::PhaseFailed { + phase: buf[1], + result_code: buf[2], + }) + } + Tag::Cancelled => Ok(Self::Cancelled), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idle_roundtrip() { + // reason is a DSP0267 GetStatusReasonCode, arbitrary nonzero value + let s = FdStatus::Idle { reason: 3 }; + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(len, 2); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + + #[test] + fn offer_pending_roundtrip() { + let s = FdStatus::OfferPending { + target: 0x1234, // component identifier + total: 0x0010_0000, // 1 MiB image + mode: TransferMode::InTransport, + svn_delayed: false, + }; + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(len, FdStatus::MAX_SIZE); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + + #[test] + fn offer_pending_svn_delayed() { + let s = FdStatus::OfferPending { + target: 1, + total: 4096, + mode: TransferMode::OutOfTransport, + svn_delayed: true, + }; + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + + #[test] + fn simple_variants_roundtrip() { + for s in [ + FdStatus::ReadyXfer, + FdStatus::VerifyPending, + FdStatus::ApplyPending, + FdStatus::ActivationPending, + FdStatus::Cancelled, + ] { + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(len, 1); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + } + + #[test] + fn svn_commit_pending_roundtrip() { + let s = FdStatus::SvnCommitPending { component: 255 }; + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(len, 3); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + + #[test] + fn phase_failed_roundtrip() { + // phase and result_code are DSP0267 values, arbitrary here + let s = FdStatus::PhaseFailed { + phase: 2, + result_code: 10, + }; + let mut buf = [0u8; 16]; + let len = s.encode(&mut buf).unwrap(); + assert_eq!(len, 3); + assert_eq!(FdStatus::decode(&buf[..len]), Ok(s)); + } + + #[test] + fn decode_empty_is_truncated() { + assert_eq!(FdStatus::decode(&[]), Err(WireError::Truncated)); + } + + #[test] + fn decode_unknown_discriminant() { + assert_eq!( + FdStatus::decode(&[0xFF]), + Err(WireError::InvalidValue(0xFF)) + ); + } + + #[test] + fn decode_offer_pending_truncated() { + assert_eq!( + FdStatus::decode(&[Tag::OfferPending as u8, 0, 0]), + Err(WireError::Truncated) + ); + } + + #[test] + fn encode_offer_pending_buffer_too_small() { + let s = FdStatus::OfferPending { + target: 1, + total: 1, + mode: TransferMode::InTransport, + svn_delayed: false, + }; + let mut buf = [0u8; 4]; + assert_eq!(s.encode(&mut buf), Err(WireError::BufferTooSmall)); + } +} diff --git a/services/pldm/ipc-api/src/transport.rs b/services/pldm/ipc-api/src/transport.rs new file mode 100644 index 00000000..d2917e97 --- /dev/null +++ b/services/pldm/ipc-api/src/transport.rs @@ -0,0 +1,66 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! The transport seam. +//! +//! Bytes in, bytes out, one round-trip, split into a start and a poll so +//! the caller never blocks. The orchestrator's client layer encodes a +//! request, starts it, and polls for the response from its event loop. +//! Swapping the transport is a wiring choice: +//! +//! - `IpcTransport` (in `orchestrator-pldm-client-ipc`): production path +//! over a kernel channel, backed by `util/ipc`'s `AsyncTransaction`. +//! That wrapper lends the kernel `'static` buffers, so the impl copies +//! the request in and the response out of buffers it owns. +//! - `LoopbackTransport` (in `pldm-ipc-server`): calls dispatch directly +//! in-process and has the response ready on the first poll. +//! Host-buildable, so the same client encoders/decoders exercise the +//! real dispatch with no kernel. + +/// Why a transport round-trip failed. Small and transport-neutral; +/// PLDM-level status travels inside the response payload, not here. +#[non_exhaustive] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TransportError { + /// The underlying channel or loopback call failed. + Failed, + /// `start` was called while a round-trip was still in flight, or + /// `poll`/`cancel` was called with nothing in flight. + WrongState, + /// The request does not fit the transport's request buffer, or the + /// response does not fit the caller's. + TooLarge, +} + +impl core::fmt::Display for TransportError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Failed => f.write_str("pldm ipc transport round-trip failed"), + Self::WrongState => f.write_str("pldm ipc transport is in the wrong state"), + Self::TooLarge => f.write_str("pldm ipc message does not fit the buffer"), + } + } +} + +impl core::error::Error for TransportError {} + +/// Bytes-in, bytes-out, exactly one round-trip at a time. +/// +/// `start` takes one fully serialized `pldm_ipc_api` request and returns +/// immediately; the transport copies what it needs, so `req` is free +/// afterwards. `poll` returns `Ok(None)` while the response is still +/// outstanding and `Ok(Some(len))` once `resp[..len]` holds one fully +/// serialized reply. No fragmentation, no state between round-trips. +/// +/// A transport carries one round-trip at a time: `start` while another +/// is in flight, or `poll`/`cancel` with none, is `WrongState`. Any +/// error from `poll` ends the round-trip, so the next call is `start`. +pub trait Transport { + fn start(&mut self, req: &[u8]) -> Result<(), TransportError>; + + fn poll(&mut self, resp: &mut [u8]) -> Result, TransportError>; + + /// Abandon the round-trip in flight. The response, if one arrives, is + /// discarded. + fn cancel(&mut self) -> Result<(), TransportError>; +} diff --git a/services/pldm/ipc-api/src/wire.rs b/services/pldm/ipc-api/src/wire.rs new file mode 100644 index 00000000..8ab51358 --- /dev/null +++ b/services/pldm/ipc-api/src/wire.rs @@ -0,0 +1,639 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! PLDM IPC wire protocol. +//! +//! Binary wire protocol for orchestrator-to-FD operations over IPC +//! channels. Uses manual byte encoding for `no_std` compatibility. +//! +//! ```text +//! Request (8-byte header + optional args): +//! +----+-------+-----+----------+ +//! | op | flags | gen | reserved | + [args] +//! | 1B | 1B | 2B | 4B | +//! +----+-------+-----+----------+ +//! +//! Response (8-byte header + optional payload): +//! +------+-------+-----+-------------+----------+ +//! | code | flags | gen | payload_len | reserved | + [payload] +//! | 1B | 1B | 2B | 2B LE | 2B | +//! +------+-------+-----+-------------+----------+ +//! ``` + +use crate::error::{DenyReason, ResponseCode, WireError}; +use crate::status::FdStatus; + +// ============================================================================ +// Operation codes +// ============================================================================ + +/// Orchestrator-to-FD IPC operations. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum PldmOp { + /// Approve the pending offer, provide staging flash base address. + AcceptOffer = 0, + /// Reject the pending offer, FD sends TransferComplete. + RejectOffer = 1, + /// Authorize the FD to run FdOps::verify. + GrantVerify = 2, + /// Block verify (e.g. isolated component). + DenyVerify = 3, + /// Authorize the FD to run FdOps::apply. + GrantApply = 4, + /// Block apply. + DenyApply = 5, + /// Read the FD's current state (phase, result, error). + QueryStatus = 6, + /// Authorize activation ahead of the UA's request. + GrantActivate = 7, + /// Refuse activation; FD answers the UA with INCOMPLETE_UPDATE. + DenyActivate = 8, + /// Acknowledge cancel, release orchestrator-side resources. + AckCancel = 9, + /// Tell the FD the SVN floor is raised so it can answer the UA. + GrantSvnCommit = 10, + /// Block the SVN commit. + DenySvnCommit = 11, +} + +impl PldmOp { + pub fn from_u8(val: u8) -> Option { + match val { + 0 => Some(Self::AcceptOffer), + 1 => Some(Self::RejectOffer), + 2 => Some(Self::GrantVerify), + 3 => Some(Self::DenyVerify), + 4 => Some(Self::GrantApply), + 5 => Some(Self::DenyApply), + 6 => Some(Self::QueryStatus), + 7 => Some(Self::GrantActivate), + 8 => Some(Self::DenyActivate), + 9 => Some(Self::AckCancel), + 10 => Some(Self::GrantSvnCommit), + 11 => Some(Self::DenySvnCommit), + _ => None, + } + } +} + +// ============================================================================ +// Request header +// ============================================================================ + +/// Request header (8 bytes). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RequestHeader { + pub op: u8, + pub flags: u8, + /// Phase generation. Reserved, set to 0. + pub generation: u16, +} + +impl RequestHeader { + pub const SIZE: usize = 8; + + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + let g = self.generation.to_le_bytes(); + [self.op, self.flags, g[0], g[1], 0, 0, 0, 0] + } + + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() < Self::SIZE { + return None; + } + Some(Self { + op: bytes[0], + flags: bytes[1], + generation: u16::from_le_bytes([bytes[2], bytes[3]]), + }) + } + + pub fn operation(&self) -> Option { + PldmOp::from_u8(self.op) + } +} + +// ============================================================================ +// Response header +// ============================================================================ + +/// Response header (8 bytes). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ResponseHeader { + pub code: u8, + pub flags: u8, + /// Phase generation. Reserved, set to 0. + pub generation: u16, + pub payload_len: u16, +} + +impl ResponseHeader { + pub const SIZE: usize = 8; + + pub const fn success() -> Self { + Self { + code: ResponseCode::Success as u8, + flags: 0, + generation: 0, + payload_len: 0, + } + } + + pub const fn error(code: ResponseCode) -> Self { + Self { + code: code as u8, + flags: 0, + generation: 0, + payload_len: 0, + } + } + + pub fn is_success(&self) -> bool { + self.code == ResponseCode::Success as u8 + } + + pub fn response_code(&self) -> ResponseCode { + ResponseCode::from_u8(self.code).unwrap_or(ResponseCode::InternalError) + } + + pub fn to_bytes(&self) -> [u8; Self::SIZE] { + let g = self.generation.to_le_bytes(); + let pl = self.payload_len.to_le_bytes(); + [self.code, self.flags, g[0], g[1], pl[0], pl[1], 0, 0] + } + + pub fn from_bytes(bytes: &[u8]) -> Option { + if bytes.len() < Self::SIZE { + return None; + } + Some(Self { + code: bytes[0], + flags: bytes[1], + generation: u16::from_le_bytes([bytes[2], bytes[3]]), + payload_len: u16::from_le_bytes([bytes[4], bytes[5]]), + }) + } +} + +// ============================================================================ +// Constants +// ============================================================================ + +/// Maximum status payload (OfferPending: 9 bytes). +pub const MAX_PAYLOAD_SIZE: usize = FdStatus::MAX_SIZE; + +/// Maximum total request size (header + AcceptOffer args). +pub const MAX_REQUEST_SIZE: usize = RequestHeader::SIZE + 4; + +/// How long a well-formed request is for a given operation. The decoder +/// reads a fixed header and ignores anything past it, so a frame longer +/// than expected would dispatch as if the extra bytes were not there. +pub const fn expected_request_len(op: PldmOp) -> usize { + match op { + PldmOp::AcceptOffer => RequestHeader::SIZE + 4, + PldmOp::DenyVerify | PldmOp::DenyApply | PldmOp::DenyActivate | PldmOp::DenySvnCommit => { + RequestHeader::SIZE + 1 + } + PldmOp::RejectOffer + | PldmOp::GrantVerify + | PldmOp::GrantApply + | PldmOp::QueryStatus + | PldmOp::GrantActivate + | PldmOp::AckCancel + | PldmOp::GrantSvnCommit => RequestHeader::SIZE, + } +} + +/// Maximum total response size (header + QueryStatus payload). +pub const MAX_RESPONSE_SIZE: usize = ResponseHeader::SIZE + MAX_PAYLOAD_SIZE; + +// ============================================================================ +// Request encoding +// ============================================================================ + +fn encode_header_only(buf: &mut [u8], op: PldmOp) -> Result { + if buf.len() < RequestHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: op as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + Ok(RequestHeader::SIZE) +} + +/// Encode AcceptOffer. `staging_base` is the flash address the FD +/// should stage firmware to. +pub fn encode_accept_offer(buf: &mut [u8], staging_base: u32) -> Result { + let total = RequestHeader::SIZE + 4; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: PldmOp::AcceptOffer as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[RequestHeader::SIZE..total].copy_from_slice(&staging_base.to_le_bytes()); + Ok(total) +} + +pub fn encode_reject_offer(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::RejectOffer) +} + +pub fn encode_grant_verify(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::GrantVerify) +} + +/// Encode DenyVerify with the reason the orchestrator is blocking. +pub fn encode_deny_verify(buf: &mut [u8], reason: DenyReason) -> Result { + let total = RequestHeader::SIZE + 1; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: PldmOp::DenyVerify as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[RequestHeader::SIZE] = reason as u8; + Ok(total) +} + +pub fn encode_grant_apply(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::GrantApply) +} + +/// Encode DenyApply with the reason the orchestrator is blocking. +pub fn encode_deny_apply(buf: &mut [u8], reason: DenyReason) -> Result { + let total = RequestHeader::SIZE + 1; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: PldmOp::DenyApply as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[RequestHeader::SIZE] = reason as u8; + Ok(total) +} + +pub fn encode_query_status(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::QueryStatus) +} + +pub fn encode_grant_activate(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::GrantActivate) +} + +pub fn encode_deny_activate(buf: &mut [u8], reason: DenyReason) -> Result { + let total = RequestHeader::SIZE + 1; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: PldmOp::DenyActivate as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[RequestHeader::SIZE] = reason as u8; + Ok(total) +} + +pub fn encode_ack_cancel(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::AckCancel) +} + +pub fn encode_grant_svn_commit(buf: &mut [u8]) -> Result { + encode_header_only(buf, PldmOp::GrantSvnCommit) +} + +/// Encode DenySvnCommit with the reason the orchestrator is blocking. +pub fn encode_deny_svn_commit(buf: &mut [u8], reason: DenyReason) -> Result { + let total = RequestHeader::SIZE + 1; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let h = RequestHeader { + op: PldmOp::DenySvnCommit as u8, + flags: 0, + generation: 0, + }; + buf[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[RequestHeader::SIZE] = reason as u8; + Ok(total) +} + +// ============================================================================ +// Response encoding (server side) +// ============================================================================ + +/// Encode a success response with no payload. +pub fn encode_success_response(buf: &mut [u8]) -> Result { + if buf.len() < ResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + buf[..ResponseHeader::SIZE].copy_from_slice(&ResponseHeader::success().to_bytes()); + Ok(ResponseHeader::SIZE) +} + +/// Encode an error response. +pub fn encode_error_response(buf: &mut [u8], code: ResponseCode) -> Result { + if buf.len() < ResponseHeader::SIZE { + return Err(WireError::BufferTooSmall); + } + buf[..ResponseHeader::SIZE].copy_from_slice(&ResponseHeader::error(code).to_bytes()); + Ok(ResponseHeader::SIZE) +} + +/// Encode a QueryStatus success response with the FD's current status. +pub fn encode_status_response(buf: &mut [u8], status: &FdStatus) -> Result { + let mut payload_buf = [0u8; FdStatus::MAX_SIZE]; + let payload_len = status.encode(&mut payload_buf)?; + let total = ResponseHeader::SIZE + payload_len; + if buf.len() < total { + return Err(WireError::BufferTooSmall); + } + let mut h = ResponseHeader::success(); + h.payload_len = payload_len as u16; + buf[..ResponseHeader::SIZE].copy_from_slice(&h.to_bytes()); + buf[ResponseHeader::SIZE..total].copy_from_slice(&payload_buf[..payload_len]); + Ok(total) +} + +// ============================================================================ +// Response decoding (client side) +// ============================================================================ + +/// Decode a response header. +pub fn decode_response_header(buf: &[u8]) -> Result { + ResponseHeader::from_bytes(buf).ok_or(WireError::Truncated) +} + +/// Extract the response payload bytes (after the header). +pub fn get_response_payload<'a>( + buf: &'a [u8], + header: &ResponseHeader, +) -> Result<&'a [u8], WireError> { + let end = ResponseHeader::SIZE + header.payload_len as usize; + if buf.len() < end { + return Err(WireError::Truncated); + } + Ok(&buf[ResponseHeader::SIZE..end]) +} + +/// Decode a request header. +pub fn decode_request_header(buf: &[u8]) -> Result { + RequestHeader::from_bytes(buf).ok_or(WireError::Truncated) +} + +/// Get the request args (bytes after the header). +pub fn get_request_args(buf: &[u8]) -> &[u8] { + if buf.len() > RequestHeader::SIZE { + &buf[RequestHeader::SIZE..] + } else { + &[] + } +} + +/// Extract the staging base address from an AcceptOffer request's args. +pub fn get_accept_offer_base(args: &[u8]) -> Result { + if args.len() < 4 { + return Err(WireError::Truncated); + } + Ok(u32::from_le_bytes([args[0], args[1], args[2], args[3]])) +} + +/// Extract the deny reason from a Deny* request's args. +pub fn get_deny_reason(args: &[u8]) -> Result { + if args.is_empty() { + return Err(WireError::Truncated); + } + DenyReason::from_u8(args[0]).ok_or(WireError::InvalidValue(args[0])) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_header_roundtrip() { + let h = RequestHeader { + op: PldmOp::QueryStatus as u8, + flags: 0, + generation: 0x1234, + }; + let bytes = h.to_bytes(); + let decoded = RequestHeader::from_bytes(&bytes).unwrap(); + assert_eq!(decoded.op, PldmOp::QueryStatus as u8); + assert_eq!(decoded.generation, 0x1234); + } + + #[test] + fn response_header_roundtrip() { + let h = ResponseHeader { + code: ResponseCode::Success as u8, + flags: 0, + generation: 0, + payload_len: 8, + }; + let bytes = h.to_bytes(); + let decoded = ResponseHeader::from_bytes(&bytes).unwrap(); + assert!(decoded.is_success()); + assert_eq!(decoded.payload_len, 8); + } + + #[test] + fn encode_accept_offer_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_accept_offer(&mut buf, 0x2000_0000).unwrap(); + assert_eq!(len, 12); + let h = decode_request_header(&buf).unwrap(); + assert_eq!(h.operation(), Some(PldmOp::AcceptOffer)); + let args = get_request_args(&buf[..len]); + assert_eq!(get_accept_offer_base(args).unwrap(), 0x2000_0000); + } + + #[test] + fn encode_deny_verify_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_deny_verify(&mut buf, DenyReason::Isolated).unwrap(); + assert_eq!(len, 9); + let h = decode_request_header(&buf).unwrap(); + assert_eq!(h.operation(), Some(PldmOp::DenyVerify)); + let args = get_request_args(&buf[..len]); + assert_eq!(get_deny_reason(args).unwrap(), DenyReason::Isolated); + } + + #[test] + fn encode_deny_apply_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_deny_apply(&mut buf, DenyReason::PolicyViolation).unwrap(); + let args = get_request_args(&buf[..len]); + assert_eq!(get_deny_reason(args).unwrap(), DenyReason::PolicyViolation); + } + + #[test] + fn encode_deny_svn_commit_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_deny_svn_commit(&mut buf, DenyReason::PolicyViolation).unwrap(); + assert_eq!(len, 9); + let h = decode_request_header(&buf).unwrap(); + assert_eq!(h.operation(), Some(PldmOp::DenySvnCommit)); + let args = get_request_args(&buf[..len]); + assert_eq!(get_deny_reason(args).unwrap(), DenyReason::PolicyViolation); + } + + #[test] + fn encode_deny_activate_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_deny_activate(&mut buf, DenyReason::UnknownTarget).unwrap(); + let h = decode_request_header(&buf).unwrap(); + assert_eq!(h.operation(), Some(PldmOp::DenyActivate)); + let args = get_request_args(&buf[..len]); + assert_eq!(get_deny_reason(args).unwrap(), DenyReason::UnknownTarget); + } + + #[test] + fn header_only_ops_roundtrip() { + let ops = [ + ( + encode_reject_offer as fn(&mut [u8]) -> _, + PldmOp::RejectOffer, + ), + (encode_grant_verify, PldmOp::GrantVerify), + (encode_grant_apply, PldmOp::GrantApply), + (encode_query_status, PldmOp::QueryStatus), + (encode_grant_activate, PldmOp::GrantActivate), + (encode_ack_cancel, PldmOp::AckCancel), + (encode_grant_svn_commit, PldmOp::GrantSvnCommit), + ]; + for (encode_fn, expected_op) in ops { + let mut buf = [0u8; 16]; + let len = encode_fn(&mut buf).unwrap(); + assert_eq!(len, RequestHeader::SIZE); + let h = decode_request_header(&buf).unwrap(); + assert_eq!(h.operation(), Some(expected_op)); + } + } + + #[test] + fn status_response_roundtrip() { + let status = FdStatus::OfferPending { + target: 0x0001, + total: 0x0008_0000, + mode: crate::status::TransferMode::InTransport, + svn_delayed: true, + }; + let mut buf = [0u8; 32]; + let len = encode_status_response(&mut buf, &status).unwrap(); + let h = decode_response_header(&buf).unwrap(); + assert!(h.is_success()); + assert_eq!(h.payload_len, 9); + let payload = get_response_payload(&buf[..len], &h).unwrap(); + let decoded = FdStatus::decode(payload).unwrap(); + assert_eq!(decoded, status); + } + + #[test] + fn error_response_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_error_response(&mut buf, ResponseCode::WrongPhase).unwrap(); + assert_eq!(len, ResponseHeader::SIZE); + let h = decode_response_header(&buf).unwrap(); + assert!(!h.is_success()); + assert_eq!(h.response_code(), ResponseCode::WrongPhase); + } + + #[test] + fn success_response_roundtrip() { + let mut buf = [0u8; 16]; + let len = encode_success_response(&mut buf).unwrap(); + assert_eq!(len, ResponseHeader::SIZE); + let h = decode_response_header(&buf).unwrap(); + assert!(h.is_success()); + assert_eq!(h.payload_len, 0); + } + + #[test] + fn unknown_opcode() { + assert_eq!(PldmOp::from_u8(0xFF), None); + } + + #[test] + fn decode_request_truncated() { + assert_eq!(decode_request_header(&[0u8; 4]), Err(WireError::Truncated)); + } + + #[test] + fn decode_response_truncated() { + assert_eq!(decode_response_header(&[0u8; 4]), Err(WireError::Truncated)); + } + + #[test] + fn get_response_payload_truncated() { + let mut h = ResponseHeader::success(); + h.payload_len = 100; + let mut buf = [0u8; 16]; + buf[..ResponseHeader::SIZE].copy_from_slice(&h.to_bytes()); + assert_eq!( + get_response_payload(&buf[..ResponseHeader::SIZE], &h), + Err(WireError::Truncated) + ); + } + + #[test] + fn buffer_too_small_errors() { + let mut buf = [0u8; 4]; + assert_eq!( + encode_accept_offer(&mut buf, 0), + Err(WireError::BufferTooSmall) + ); + assert_eq!( + encode_reject_offer(&mut buf), + Err(WireError::BufferTooSmall) + ); + assert_eq!( + encode_deny_verify(&mut buf, DenyReason::Busy), + Err(WireError::BufferTooSmall) + ); + assert_eq!( + encode_success_response(&mut buf), + Err(WireError::BufferTooSmall) + ); + assert_eq!( + encode_error_response(&mut buf, ResponseCode::InternalError), + Err(WireError::BufferTooSmall) + ); + } + + #[test] + fn get_request_args_empty_for_header_only() { + let mut buf = [0u8; 16]; + encode_query_status(&mut buf).unwrap(); + assert_eq!(get_request_args(&buf[..RequestHeader::SIZE]), &[]); + } + + #[test] + fn get_accept_offer_base_truncated() { + assert_eq!(get_accept_offer_base(&[0, 0]), Err(WireError::Truncated)); + } + + #[test] + fn get_deny_reason_truncated() { + assert_eq!(get_deny_reason(&[]), Err(WireError::Truncated)); + } +} From 79248873a5c3548de2a06b17a91916fb312120a9 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Tue, 22 Sep 2026 16:17:37 +0200 Subject: [PATCH 2/3] pldm: Add IPC server crate with dispatch and loopback transport The FD side of the IPC channel. dispatch() decodes the orchestrator's request header, matches on PldmOp, calls the corresponding FdHandler trait method, and encodes the response. FdHandler has one method per opcode so the implementation never touches wire bytes. LoopbackTransport implements pldm_ipc_api::Transport by calling dispatch directly in-process. Dispatch runs inside start(), so the response is ready on the first poll(); the split-phase shape is kept so client code written against a real channel runs unchanged here. The loopback tests show a multi-step sequence (accept offer, then query status shows ReadyXfer), proving the wire format, dispatch, and status encoding work together, plus the state rules: start while pending, poll with nothing in flight, cancel, and a short response buffer. Same pattern as services/i2c/server: dispatch generic over a trait, loopback transport in the server crate, host-buildable with no kernel dependencies. Assisted-by: Claude Code --- services/pldm/ipc-server/BUILD.bazel | 19 ++ services/pldm/ipc-server/src/lib.rs | 394 +++++++++++++++++++++++ services/pldm/ipc-server/src/loopback.rs | 274 ++++++++++++++++ 3 files changed, 687 insertions(+) create mode 100644 services/pldm/ipc-server/BUILD.bazel create mode 100644 services/pldm/ipc-server/src/lib.rs create mode 100644 services/pldm/ipc-server/src/loopback.rs diff --git a/services/pldm/ipc-server/BUILD.bazel b/services/pldm/ipc-server/BUILD.bazel new file mode 100644 index 00000000..8b893946 --- /dev/null +++ b/services/pldm/ipc-server/BUILD.bazel @@ -0,0 +1,19 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "pldm_ipc_server", + srcs = glob(["src/**/*.rs"]), + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//services/pldm/ipc-api:pldm_ipc_api", + ], +) + +rust_test( + name = "pldm_ipc_server_test", + crate = ":pldm_ipc_server", +) diff --git a/services/pldm/ipc-server/src/lib.rs b/services/pldm/ipc-server/src/lib.rs new file mode 100644 index 00000000..d7529a58 --- /dev/null +++ b/services/pldm/ipc-server/src/lib.rs @@ -0,0 +1,394 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! PLDM IPC server: dispatch and loopback. +//! +//! The FD side of the IPC channel. Decodes orchestrator requests, +//! dispatches to an `FdHandler` implementation, and encodes responses. +//! The loopback transport calls dispatch directly in-process so the +//! same encode/decode paths exercise real dispatch with no kernel. + +#![no_std] + +mod loopback; + +pub use loopback::LoopbackTransport; +use pldm_ipc_api::wire::{self, PldmOp}; +use pldm_ipc_api::{DenyReason, FdStatus, ResponseCode, WireError}; + +/// What the FD does in response to each orchestrator operation. +/// +/// One method per opcode. All return `Result<(), ResponseCode>` except +/// `query_status`, which returns the FD's current condition. The server +/// encodes the result into the response buffer; the handler never +/// touches wire bytes. +/// +/// Every method must return immediately. Record the decision, flip +/// state, return. The FD shares its loop with MCTP traffic from the +/// UA, so a handler that blocks stalls UA traffic. +pub trait FdHandler { + fn accept_offer(&mut self, staging_base: u32) -> Result<(), ResponseCode>; + fn reject_offer(&mut self) -> Result<(), ResponseCode>; + fn grant_verify(&mut self) -> Result<(), ResponseCode>; + fn deny_verify(&mut self, reason: DenyReason) -> Result<(), ResponseCode>; + fn grant_apply(&mut self) -> Result<(), ResponseCode>; + fn deny_apply(&mut self, reason: DenyReason) -> Result<(), ResponseCode>; + fn query_status(&mut self) -> Result; + fn grant_activate(&mut self) -> Result<(), ResponseCode>; + fn deny_activate(&mut self, reason: DenyReason) -> Result<(), ResponseCode>; + fn ack_cancel(&mut self) -> Result<(), ResponseCode>; + fn grant_svn_commit(&mut self) -> Result<(), ResponseCode>; + fn deny_svn_commit(&mut self, reason: DenyReason) -> Result<(), ResponseCode>; +} + +/// Decode one request, call the handler, encode the response. +/// +/// Returns the number of bytes written to `response`. On any wire +/// error the response is an `InternalError`; on an unknown opcode it +/// is `InvalidOp`. The caller does not need to inspect the return +/// value beyond passing `&response[..n]` to the transport. +pub fn dispatch(handler: &mut F, request: &[u8], response: &mut [u8]) -> usize { + match dispatch_inner(handler, request, response) { + Ok(n) => n, + Err(WireError::InvalidOpcode(_)) => { + wire::encode_error_response(response, ResponseCode::InvalidOp).unwrap_or(0) + } + Err(_) => wire::encode_error_response(response, ResponseCode::InternalError).unwrap_or(0), + } +} + +fn dispatch_inner( + handler: &mut F, + request: &[u8], + response: &mut [u8], +) -> Result { + let header = wire::decode_request_header(request)?; + let args = wire::get_request_args(request); + + let op = header + .operation() + .ok_or(WireError::InvalidOpcode(header.op))?; + + match op { + PldmOp::AcceptOffer => { + let base = wire::get_accept_offer_base(args)?; + encode_unit_result(response, handler.accept_offer(base)) + } + PldmOp::RejectOffer => encode_unit_result(response, handler.reject_offer()), + PldmOp::GrantVerify => encode_unit_result(response, handler.grant_verify()), + PldmOp::DenyVerify => { + let reason = wire::get_deny_reason(args)?; + encode_unit_result(response, handler.deny_verify(reason)) + } + PldmOp::GrantApply => encode_unit_result(response, handler.grant_apply()), + PldmOp::DenyApply => { + let reason = wire::get_deny_reason(args)?; + encode_unit_result(response, handler.deny_apply(reason)) + } + PldmOp::QueryStatus => match handler.query_status() { + Ok(status) => wire::encode_status_response(response, &status), + Err(code) => wire::encode_error_response(response, code), + }, + PldmOp::GrantActivate => encode_unit_result(response, handler.grant_activate()), + PldmOp::DenyActivate => { + let reason = wire::get_deny_reason(args)?; + encode_unit_result(response, handler.deny_activate(reason)) + } + PldmOp::AckCancel => encode_unit_result(response, handler.ack_cancel()), + PldmOp::GrantSvnCommit => encode_unit_result(response, handler.grant_svn_commit()), + PldmOp::DenySvnCommit => { + let reason = wire::get_deny_reason(args)?; + encode_unit_result(response, handler.deny_svn_commit(reason)) + } + } +} + +fn encode_unit_result( + response: &mut [u8], + result: Result<(), ResponseCode>, +) -> Result { + match result { + Ok(()) => wire::encode_success_response(response), + Err(code) => wire::encode_error_response(response, code), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pldm_ipc_api::status::TransferMode; + use pldm_ipc_api::wire::{RequestHeader, MAX_RESPONSE_SIZE}; + + /// Mock handler that records calls and returns canned responses. + struct MockFd { + last_op: Option<&'static str>, + status: FdStatus, + next_error: Option, + } + + impl MockFd { + fn new() -> Self { + Self { + last_op: None, + status: FdStatus::Idle { reason: 0 }, + next_error: None, + } + } + + fn returning_error(code: ResponseCode) -> Self { + Self { + last_op: None, + status: FdStatus::Idle { reason: 0 }, + next_error: Some(code), + } + } + + fn check(&mut self, name: &'static str) -> Result<(), ResponseCode> { + self.last_op = Some(name); + match self.next_error.take() { + Some(code) => Err(code), + None => Ok(()), + } + } + } + + impl FdHandler for MockFd { + fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { + self.check("accept_offer") + } + fn reject_offer(&mut self) -> Result<(), ResponseCode> { + self.check("reject_offer") + } + fn grant_verify(&mut self) -> Result<(), ResponseCode> { + self.check("grant_verify") + } + fn deny_verify(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + self.check("deny_verify") + } + fn grant_apply(&mut self) -> Result<(), ResponseCode> { + self.check("grant_apply") + } + fn deny_apply(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + self.check("deny_apply") + } + fn query_status(&mut self) -> Result { + self.last_op = Some("query_status"); + match self.next_error.take() { + Some(code) => Err(code), + None => Ok(self.status), + } + } + fn grant_activate(&mut self) -> Result<(), ResponseCode> { + self.check("grant_activate") + } + fn deny_activate(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + self.check("deny_activate") + } + fn ack_cancel(&mut self) -> Result<(), ResponseCode> { + self.check("ack_cancel") + } + fn grant_svn_commit(&mut self) -> Result<(), ResponseCode> { + self.check("grant_svn_commit") + } + fn deny_svn_commit(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + self.check("deny_svn_commit") + } + } + + fn roundtrip_success( + encode: impl FnOnce(&mut [u8]) -> Result, + expected_op: &'static str, + ) { + let mut req = [0u8; 16]; + let req_len = encode(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + assert_eq!(fd.last_op, Some(expected_op)); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success(), "expected success for {expected_op}"); + } + + #[test] + fn accept_offer_dispatches() { + roundtrip_success( + |buf| wire::encode_accept_offer(buf, 0x2000_0000), + "accept_offer", + ); + } + + #[test] + fn reject_offer_dispatches() { + roundtrip_success(|buf| wire::encode_reject_offer(buf), "reject_offer"); + } + + #[test] + fn grant_verify_dispatches() { + roundtrip_success(|buf| wire::encode_grant_verify(buf), "grant_verify"); + } + + #[test] + fn deny_verify_dispatches() { + roundtrip_success( + |buf| wire::encode_deny_verify(buf, DenyReason::Isolated), + "deny_verify", + ); + } + + #[test] + fn grant_apply_dispatches() { + roundtrip_success(|buf| wire::encode_grant_apply(buf), "grant_apply"); + } + + #[test] + fn deny_apply_dispatches() { + roundtrip_success( + |buf| wire::encode_deny_apply(buf, DenyReason::PolicyViolation), + "deny_apply", + ); + } + + #[test] + fn grant_activate_dispatches() { + roundtrip_success(|buf| wire::encode_grant_activate(buf), "grant_activate"); + } + + #[test] + fn deny_activate_dispatches() { + roundtrip_success( + |buf| wire::encode_deny_activate(buf, DenyReason::Busy), + "deny_activate", + ); + } + + #[test] + fn ack_cancel_dispatches() { + roundtrip_success(|buf| wire::encode_ack_cancel(buf), "ack_cancel"); + } + + #[test] + fn grant_svn_commit_dispatches() { + roundtrip_success(|buf| wire::encode_grant_svn_commit(buf), "grant_svn_commit"); + } + + #[test] + fn deny_svn_commit_dispatches() { + roundtrip_success( + |buf| wire::encode_deny_svn_commit(buf, DenyReason::PolicyViolation), + "deny_svn_commit", + ); + } + + #[test] + fn query_status_returns_fd_state() { + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + fd.status = FdStatus::OfferPending { + target: 0x0001, + total: 0x0010_0000, + mode: TransferMode::InTransport, + svn_delayed: true, + }; + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, fd.status); + } + + #[test] + fn handler_error_becomes_error_response() { + let mut req = [0u8; 16]; + let req_len = wire::encode_grant_verify(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::returning_error(ResponseCode::WrongPhase); + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(!h.is_success()); + assert_eq!(h.response_code(), ResponseCode::WrongPhase); + } + + #[test] + fn unknown_opcode_returns_invalid_op() { + let mut req = [0u8; 16]; + let h = RequestHeader { + op: 0xFF, + flags: 0, + generation: 0, + }; + req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp); + let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(!rh.is_success()); + assert_eq!(rh.response_code(), ResponseCode::InvalidOp); + assert_eq!(fd.last_op, None); + } + + #[test] + fn truncated_request_returns_internal_error() { + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &[0u8; 2], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert_eq!(h.response_code(), ResponseCode::InternalError); + } + + #[test] + fn deny_verify_missing_reason_returns_internal_error() { + let mut req = [0u8; 16]; + let h = RequestHeader { + op: PldmOp::DenyVerify as u8, + flags: 0, + generation: 0, + }; + req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp); + let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert_eq!(rh.response_code(), ResponseCode::InternalError); + assert_eq!(fd.last_op, None); + } + + #[test] + fn deny_verify_bad_reason_returns_internal_error() { + let mut req = [0u8; 16]; + let h = RequestHeader { + op: PldmOp::DenyVerify as u8, + flags: 0, + generation: 0, + }; + req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + req[RequestHeader::SIZE] = 0xFF; + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp); + let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert_eq!(rh.response_code(), ResponseCode::InternalError); + assert_eq!(fd.last_op, None); + } + + #[test] + fn accept_offer_truncated_args_returns_internal_error() { + let mut req = [0u8; 16]; + let h = RequestHeader { + op: PldmOp::AcceptOffer as u8, + flags: 0, + generation: 0, + }; + req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); + // Only 1 byte of args instead of the 4 needed for the base address. + req[RequestHeader::SIZE] = 0x20; + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp); + let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert_eq!(rh.response_code(), ResponseCode::InternalError); + assert_eq!(fd.last_op, None); + } +} diff --git a/services/pldm/ipc-server/src/loopback.rs b/services/pldm/ipc-server/src/loopback.rs new file mode 100644 index 00000000..157b0ece --- /dev/null +++ b/services/pldm/ipc-server/src/loopback.rs @@ -0,0 +1,274 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! In-process loopback transport for host testing. +//! +//! Calls `dispatch` directly, no kernel, no IPC channel. The client +//! crate's encoders and decoders exercise the real server dispatch +//! path through this transport. + +use pldm_ipc_api::transport::{Transport, TransportError}; +use pldm_ipc_api::wire::{MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE}; + +use crate::{dispatch, FdHandler}; + +/// Loopback transport that dispatches in-process. +/// +/// Owns the `FdHandler` so the client and server share a single +/// process with no concurrency. Useful for host-side integration +/// tests and for validating wire format round-trips. +/// +/// Dispatch runs inside `start`, so the response is ready on the first +/// `poll`. The split-phase shape is kept so client code written against +/// a real channel runs unchanged here. +pub struct LoopbackTransport { + handler: F, + response: [u8; MAX_RESPONSE_SIZE], + /// Response length, set while a round-trip is in flight. + pending: Option, +} + +impl LoopbackTransport { + pub fn new(handler: F) -> Self { + Self { + handler, + response: [0u8; MAX_RESPONSE_SIZE], + pending: None, + } + } +} + +impl Transport for LoopbackTransport { + fn start(&mut self, req: &[u8]) -> Result<(), TransportError> { + if self.pending.is_some() { + return Err(TransportError::WrongState); + } + if req.len() > MAX_REQUEST_SIZE { + return Err(TransportError::TooLarge); + } + + let n = dispatch(&mut self.handler, req, &mut self.response); + if n == 0 { + return Err(TransportError::Failed); + } + self.pending = Some(n); + Ok(()) + } + + fn poll(&mut self, resp: &mut [u8]) -> Result, TransportError> { + let n = self.pending.ok_or(TransportError::WrongState)?; + if resp.len() < n { + self.pending = None; + return Err(TransportError::TooLarge); + } + resp[..n].copy_from_slice(&self.response[..n]); + self.pending = None; + Ok(Some(n)) + } + + fn cancel(&mut self) -> Result<(), TransportError> { + if self.pending.take().is_none() { + return Err(TransportError::WrongState); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Start one request and poll the response out, as the client layer + /// does; the loopback always has it ready on the first poll. + fn round_trip( + transport: &mut LoopbackTransport, + req: &[u8], + resp: &mut [u8], + ) -> usize { + transport.start(req).unwrap(); + transport.poll(resp).unwrap().unwrap() + } + use pldm_ipc_api::status::TransferMode; + use pldm_ipc_api::wire::{self, MAX_RESPONSE_SIZE}; + use pldm_ipc_api::{DenyReason, FdStatus, ResponseCode}; + + /// Minimal handler for loopback tests. + struct StubFd { + status: FdStatus, + } + + impl StubFd { + fn idle() -> Self { + Self { + status: FdStatus::Idle { reason: 0 }, + } + } + + fn with_offer() -> Self { + Self { + status: FdStatus::OfferPending { + target: 0x0001, + total: 0x0010_0000, + mode: TransferMode::InTransport, + svn_delayed: false, + }, + } + } + } + + impl FdHandler for StubFd { + fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { + self.status = FdStatus::ReadyXfer; + Ok(()) + } + fn reject_offer(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::Idle { reason: 0 }; + Ok(()) + } + fn grant_verify(&mut self) -> Result<(), ResponseCode> { + Ok(()) + } + fn deny_verify(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn grant_apply(&mut self) -> Result<(), ResponseCode> { + Ok(()) + } + fn deny_apply(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn query_status(&mut self) -> Result { + Ok(self.status) + } + fn grant_activate(&mut self) -> Result<(), ResponseCode> { + Ok(()) + } + fn deny_activate(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn ack_cancel(&mut self) -> Result<(), ResponseCode> { + Ok(()) + } + fn grant_svn_commit(&mut self) -> Result<(), ResponseCode> { + Ok(()) + } + fn deny_svn_commit(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + } + + #[test] + fn query_status_through_loopback() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::Idle { reason: 0 }); + } + + #[test] + fn accept_offer_then_query_shows_ready_xfer() { + let mut transport = LoopbackTransport::new(StubFd::with_offer()); + + // Accept the offer. + let mut req = [0u8; 16]; + let req_len = wire::encode_accept_offer(&mut req, 0x2000_0000).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + + // Query status: should now be ReadyXfer. + let req_len = wire::encode_query_status(&mut req).unwrap(); + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::ReadyXfer); + } + + #[test] + fn reject_offer_then_query_shows_idle() { + let mut transport = LoopbackTransport::new(StubFd::with_offer()); + + let mut req = [0u8; 16]; + let req_len = wire::encode_reject_offer(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + + let req_len = wire::encode_query_status(&mut req).unwrap(); + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::Idle { reason: 0 }); + } + + #[test] + fn start_while_pending_is_wrong_state() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + assert_eq!( + transport.start(&req[..req_len]), + Err(TransportError::WrongState) + ); + } + + #[test] + fn poll_without_start_is_wrong_state() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); + } + + #[test] + fn cancel_releases_the_round_trip() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + transport.cancel().unwrap(); + assert_eq!(transport.cancel(), Err(TransportError::WrongState)); + + // The transport is usable again after a cancel. + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + assert!(wire::decode_response_header(&resp[..resp_len]) + .unwrap() + .is_success()); + } + + #[test] + fn poll_into_a_short_buffer_is_too_large() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + let mut resp = [0u8; 1]; + assert_eq!(transport.poll(&mut resp), Err(TransportError::TooLarge)); + // The failed poll ended the round-trip. + assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); + } + + #[test] + fn start_with_an_oversized_request_is_too_large() { + let mut transport = LoopbackTransport::new(StubFd::idle()); + let req = [0u8; MAX_REQUEST_SIZE + 1]; + assert_eq!(transport.start(&req), Err(TransportError::TooLarge)); + // Nothing was started, so there is nothing to poll. + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); + } +} From caeabc64e632e4d0b855d532f9760d78a9e89d8c Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Fri, 25 Sep 2026 15:18:04 +0200 Subject: [PATCH 3/3] pldm: Move the IPC server onto the shared service seam Deletes the two local copies: ipc-api's Transport trait, which was character-identical to util_service::AsyncTransport, and ipc-server's LoopbackTransport, which was util_service::Loopback around dispatch. The api crate is now wire format only, and the transport that carries its frames is the one every IPC service shares. FdServer is the Dispatch impl. The newtype exists because Dispatch is a foreign trait and cannot be implemented for every F: FdHandler directly. dispatch() returns Result: a response buffer too small for even an error frame is the one case with nothing to send back, where it used to return a bare 0. The request length check is now per opcode. expected_request_len(op) returns the exact frame size for each operation, so a frame with trailing slack is rejected before dispatch. The match has no catch-all, so adding an opcode without choosing its length fails to compile. Assisted-by: Claude --- services/pldm/ipc-api/src/lib.rs | 8 +- services/pldm/ipc-api/src/transport.rs | 66 ---- services/pldm/ipc-server/BUILD.bazel | 1 + services/pldm/ipc-server/src/lib.rs | 402 +++++++++++++++++++++-- services/pldm/ipc-server/src/loopback.rs | 274 --------------- 5 files changed, 380 insertions(+), 371 deletions(-) delete mode 100644 services/pldm/ipc-api/src/transport.rs delete mode 100644 services/pldm/ipc-server/src/loopback.rs diff --git a/services/pldm/ipc-api/src/lib.rs b/services/pldm/ipc-api/src/lib.rs index 03a56097..39d51067 100644 --- a/services/pldm/ipc-api/src/lib.rs +++ b/services/pldm/ipc-api/src/lib.rs @@ -1,23 +1,23 @@ // Licensed under the Apache-2.0 license // SPDX-License-Identifier: Apache-2.0 -//! PLDM IPC API: wire format, types, and transport seam. +//! PLDM IPC API: wire format and types. //! //! Defines the binary protocol the orchestrator uses to talk to the //! PLDM Firmware Device over IPC. Host-buildable, no kernel //! dependencies. The server and client crates depend on this for -//! shared types; neither re-invents the encoding. +//! shared types; neither re-invents the encoding. The transport that +//! carries these frames is `util_service`, shared with every other +//! IPC service. #![no_std] pub mod error; pub mod status; -pub mod transport; pub mod wire; pub use error::{DenyReason, PldmIpcError, ResponseCode, WireError}; pub use status::{FdStatus, TransferMode}; -pub use transport::{Transport, TransportError}; pub use wire::{ PldmOp, RequestHeader, ResponseHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, }; diff --git a/services/pldm/ipc-api/src/transport.rs b/services/pldm/ipc-api/src/transport.rs deleted file mode 100644 index d2917e97..00000000 --- a/services/pldm/ipc-api/src/transport.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Licensed under the Apache-2.0 license -// SPDX-License-Identifier: Apache-2.0 - -//! The transport seam. -//! -//! Bytes in, bytes out, one round-trip, split into a start and a poll so -//! the caller never blocks. The orchestrator's client layer encodes a -//! request, starts it, and polls for the response from its event loop. -//! Swapping the transport is a wiring choice: -//! -//! - `IpcTransport` (in `orchestrator-pldm-client-ipc`): production path -//! over a kernel channel, backed by `util/ipc`'s `AsyncTransaction`. -//! That wrapper lends the kernel `'static` buffers, so the impl copies -//! the request in and the response out of buffers it owns. -//! - `LoopbackTransport` (in `pldm-ipc-server`): calls dispatch directly -//! in-process and has the response ready on the first poll. -//! Host-buildable, so the same client encoders/decoders exercise the -//! real dispatch with no kernel. - -/// Why a transport round-trip failed. Small and transport-neutral; -/// PLDM-level status travels inside the response payload, not here. -#[non_exhaustive] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TransportError { - /// The underlying channel or loopback call failed. - Failed, - /// `start` was called while a round-trip was still in flight, or - /// `poll`/`cancel` was called with nothing in flight. - WrongState, - /// The request does not fit the transport's request buffer, or the - /// response does not fit the caller's. - TooLarge, -} - -impl core::fmt::Display for TransportError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Failed => f.write_str("pldm ipc transport round-trip failed"), - Self::WrongState => f.write_str("pldm ipc transport is in the wrong state"), - Self::TooLarge => f.write_str("pldm ipc message does not fit the buffer"), - } - } -} - -impl core::error::Error for TransportError {} - -/// Bytes-in, bytes-out, exactly one round-trip at a time. -/// -/// `start` takes one fully serialized `pldm_ipc_api` request and returns -/// immediately; the transport copies what it needs, so `req` is free -/// afterwards. `poll` returns `Ok(None)` while the response is still -/// outstanding and `Ok(Some(len))` once `resp[..len]` holds one fully -/// serialized reply. No fragmentation, no state between round-trips. -/// -/// A transport carries one round-trip at a time: `start` while another -/// is in flight, or `poll`/`cancel` with none, is `WrongState`. Any -/// error from `poll` ends the round-trip, so the next call is `start`. -pub trait Transport { - fn start(&mut self, req: &[u8]) -> Result<(), TransportError>; - - fn poll(&mut self, resp: &mut [u8]) -> Result, TransportError>; - - /// Abandon the round-trip in flight. The response, if one arrives, is - /// discarded. - fn cancel(&mut self) -> Result<(), TransportError>; -} diff --git a/services/pldm/ipc-server/BUILD.bazel b/services/pldm/ipc-server/BUILD.bazel index 8b893946..704f385e 100644 --- a/services/pldm/ipc-server/BUILD.bazel +++ b/services/pldm/ipc-server/BUILD.bazel @@ -10,6 +10,7 @@ rust_library( visibility = ["//visibility:public"], deps = [ "//services/pldm/ipc-api:pldm_ipc_api", + "//util/service", ], ) diff --git a/services/pldm/ipc-server/src/lib.rs b/services/pldm/ipc-server/src/lib.rs index d7529a58..50c246a2 100644 --- a/services/pldm/ipc-server/src/lib.rs +++ b/services/pldm/ipc-server/src/lib.rs @@ -1,20 +1,19 @@ // Licensed under the Apache-2.0 license // SPDX-License-Identifier: Apache-2.0 -//! PLDM IPC server: dispatch and loopback. +//! PLDM IPC server: the FD side of the IPC channel. //! -//! The FD side of the IPC channel. Decodes orchestrator requests, -//! dispatches to an `FdHandler` implementation, and encodes responses. -//! The loopback transport calls dispatch directly in-process so the -//! same encode/decode paths exercise real dispatch with no kernel. +//! Decodes orchestrator requests, dispatches to an `FdHandler` +//! implementation, and encodes responses. `FdServer` is the +//! `util_service::Dispatch` impl, so the same server answers behind a +//! kernel channel in production and inside `util_service::Loopback` in +//! host tests. #![no_std] -mod loopback; - -pub use loopback::LoopbackTransport; use pldm_ipc_api::wire::{self, PldmOp}; use pldm_ipc_api::{DenyReason, FdStatus, ResponseCode, WireError}; +use util_service::{Dispatch, DispatchError}; /// What the FD does in response to each orchestrator operation. /// @@ -45,15 +44,43 @@ pub trait FdHandler { /// /// Returns the number of bytes written to `response`. On any wire /// error the response is an `InternalError`; on an unknown opcode it -/// is `InvalidOp`. The caller does not need to inspect the return -/// value beyond passing `&response[..n]` to the transport. -pub fn dispatch(handler: &mut F, request: &[u8], response: &mut [u8]) -> usize { - match dispatch_inner(handler, request, response) { - Ok(n) => n, - Err(WireError::InvalidOpcode(_)) => { - wire::encode_error_response(response, ResponseCode::InvalidOp).unwrap_or(0) - } - Err(_) => wire::encode_error_response(response, ResponseCode::InternalError).unwrap_or(0), +/// is `InvalidOp`. Only a `response` too small to hold even that error +/// frame has nothing to send back. +pub fn dispatch( + handler: &mut F, + request: &[u8], + response: &mut [u8], +) -> Result { + let code = match dispatch_inner(handler, request, response) { + Ok(n) => return Ok(n), + Err(WireError::InvalidOpcode(_)) => ResponseCode::InvalidOp, + Err(_) => ResponseCode::InternalError, + }; + wire::encode_error_response(response, code).map_err(|_| DispatchError::ResponseTooSmall) +} + +/// An `FdHandler` as a server the shared transports can drive. +/// +/// The newtype exists because `Dispatch` is a foreign trait: it cannot +/// be implemented for every `F: FdHandler` directly. +pub struct FdServer { + handler: F, +} + +impl FdServer { + pub const fn new(handler: F) -> Self { + Self { handler } + } + + /// The handler, to assert on what the requests did to it. + pub fn handler(&self) -> &F { + &self.handler + } +} + +impl Dispatch for FdServer { + fn dispatch(&mut self, request: &[u8], response: &mut [u8]) -> Result { + dispatch(&mut self.handler, request, response) } } @@ -63,11 +90,13 @@ fn dispatch_inner( response: &mut [u8], ) -> Result { let header = wire::decode_request_header(request)?; - let args = wire::get_request_args(request); - let op = header .operation() .ok_or(WireError::InvalidOpcode(header.op))?; + if request.len() > wire::expected_request_len(op) { + return Err(WireError::PayloadTooLarge); + } + let args = wire::get_request_args(request); match op { PldmOp::AcceptOffer => { @@ -203,7 +232,7 @@ mod tests { let req_len = encode(&mut req).unwrap(); let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); assert_eq!(fd.last_op, Some(expected_op)); let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert!(h.is_success(), "expected success for {expected_op}"); @@ -291,7 +320,7 @@ mod tests { mode: TransferMode::InTransport, svn_delayed: true, }; - let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert!(h.is_success()); let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); @@ -305,7 +334,7 @@ mod tests { let req_len = wire::encode_grant_verify(&mut req).unwrap(); let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::returning_error(ResponseCode::WrongPhase); - let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp); + let resp_len = dispatch(&mut fd, &req[..req_len], &mut resp).unwrap(); let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert!(!h.is_success()); assert_eq!(h.response_code(), ResponseCode::WrongPhase); @@ -322,7 +351,7 @@ mod tests { req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp).unwrap(); let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert!(!rh.is_success()); assert_eq!(rh.response_code(), ResponseCode::InvalidOp); @@ -333,7 +362,7 @@ mod tests { fn truncated_request_returns_internal_error() { let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &[0u8; 2], &mut resp); + let resp_len = dispatch(&mut fd, &[0u8; 2], &mut resp).unwrap(); let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert_eq!(h.response_code(), ResponseCode::InternalError); } @@ -349,7 +378,7 @@ mod tests { req[..RequestHeader::SIZE].copy_from_slice(&h.to_bytes()); let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE], &mut resp).unwrap(); let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert_eq!(rh.response_code(), ResponseCode::InternalError); assert_eq!(fd.last_op, None); @@ -367,7 +396,7 @@ mod tests { req[RequestHeader::SIZE] = 0xFF; let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp).unwrap(); let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert_eq!(rh.response_code(), ResponseCode::InternalError); assert_eq!(fd.last_op, None); @@ -386,9 +415,328 @@ mod tests { req[RequestHeader::SIZE] = 0x20; let mut resp = [0u8; MAX_RESPONSE_SIZE]; let mut fd = MockFd::new(); - let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp); + let resp_len = dispatch(&mut fd, &req[..RequestHeader::SIZE + 1], &mut resp).unwrap(); let rh = wire::decode_response_header(&resp[..resp_len]).unwrap(); assert_eq!(rh.response_code(), ResponseCode::InternalError); assert_eq!(fd.last_op, None); } + + #[test] + fn a_query_status_frame_with_trailing_slack_is_rejected() { + // 12 bytes is MAX_REQUEST_SIZE, so the old length gate let this + // through and the decoder ignored the four extra bytes. + let mut req = [0u8; wire::MAX_REQUEST_SIZE]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + assert!(req_len < wire::MAX_REQUEST_SIZE); + + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let mut fd = MockFd::new(); + let resp_len = dispatch(&mut fd, &req, &mut resp).unwrap(); + + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(!h.is_success()); + assert_eq!(h.response_code(), ResponseCode::InternalError); + assert_eq!(fd.last_op, None); + } +} + +#[cfg(test)] +mod loopback_tests { + use super::*; + use pldm_ipc_api::wire::MAX_REQUEST_SIZE; + use util_service::{AsyncTransport, Loopback, TransportError}; + + /// Start one request and poll the response out, as the client layer + /// does; the loopback always has it ready on the first poll. + fn round_trip( + transport: &mut Loopback, MAX_RESPONSE_SIZE>, + req: &[u8], + resp: &mut [u8], + ) -> usize { + transport.start(req).unwrap(); + transport.poll(resp).unwrap().unwrap() + } + use pldm_ipc_api::status::TransferMode; + use pldm_ipc_api::wire::{self, MAX_RESPONSE_SIZE}; + use pldm_ipc_api::{DenyReason, FdStatus, ResponseCode}; + + /// Minimal handler for loopback tests. + struct StubFd { + status: FdStatus, + } + + impl StubFd { + fn idle() -> Self { + Self { + status: FdStatus::Idle { reason: 0 }, + } + } + + fn with_offer() -> Self { + Self { + status: FdStatus::OfferPending { + target: 0x0001, + total: 0x0010_0000, + mode: TransferMode::InTransport, + svn_delayed: false, + }, + } + } + + fn at(status: FdStatus) -> Self { + Self { status } + } + } + + impl FdHandler for StubFd { + fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { + self.status = FdStatus::ReadyXfer; + Ok(()) + } + fn reject_offer(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::Idle { reason: 0 }; + Ok(()) + } + fn grant_verify(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::ApplyPending; + Ok(()) + } + fn deny_verify(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn grant_apply(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::ActivationPending; + Ok(()) + } + fn deny_apply(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn query_status(&mut self) -> Result { + Ok(self.status) + } + fn grant_activate(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::Idle { reason: 0 }; + Ok(()) + } + fn deny_activate(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + fn ack_cancel(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::Idle { reason: 0 }; + Ok(()) + } + fn grant_svn_commit(&mut self) -> Result<(), ResponseCode> { + self.status = FdStatus::Idle { reason: 0 }; + Ok(()) + } + fn deny_svn_commit(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { + Ok(()) + } + } + + #[test] + fn query_status_through_loopback() { + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::Idle { reason: 0 }); + } + + #[test] + fn accept_offer_then_query_shows_ready_xfer() { + let mut transport = + Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::with_offer())); + + // Accept the offer. + let mut req = [0u8; 16]; + let req_len = wire::encode_accept_offer(&mut req, 0x2000_0000).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + + // Query status: should now be ReadyXfer. + let req_len = wire::encode_query_status(&mut req).unwrap(); + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::ReadyXfer); + } + + #[test] + fn reject_offer_then_query_shows_idle() { + let mut transport = + Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::with_offer())); + + let mut req = [0u8; 16]; + let req_len = wire::encode_reject_offer(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + + let req_len = wire::encode_query_status(&mut req).unwrap(); + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + let status = FdStatus::decode(payload).unwrap(); + assert_eq!(status, FdStatus::Idle { reason: 0 }); + } + + #[test] + fn start_while_pending_is_wrong_state() { + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + assert_eq!( + transport.start(&req[..req_len]), + Err(TransportError::WrongState) + ); + } + + #[test] + fn poll_without_start_is_wrong_state() { + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); + } + + #[test] + fn cancel_releases_the_round_trip() { + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + transport.cancel().unwrap(); + assert_eq!(transport.cancel(), Err(TransportError::WrongState)); + + // The transport is usable again after a cancel. + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); + assert!(wire::decode_response_header(&resp[..resp_len]) + .unwrap() + .is_success()); + } + + #[test] + fn poll_into_a_short_buffer_is_too_large() { + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + + transport.start(&req[..req_len]).unwrap(); + let mut resp = [0u8; 1]; + assert_eq!(transport.poll(&mut resp), Err(TransportError::TooLarge)); + // The failed poll ended the round-trip. + assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); + } + + /// Send a request and assert the response is success. + fn send_ok( + transport: &mut Loopback, MAX_RESPONSE_SIZE>, + encode: impl FnOnce(&mut [u8]) -> Result, + ) { + let mut req = [0u8; 16]; + let req_len = encode(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + } + + /// Send QueryStatus and return the decoded FdStatus. + fn query_status( + transport: &mut Loopback, MAX_RESPONSE_SIZE>, + ) -> FdStatus { + let mut req = [0u8; 16]; + let req_len = wire::encode_query_status(&mut req).unwrap(); + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + let resp_len = round_trip(transport, &req[..req_len], &mut resp); + let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); + assert!(h.is_success()); + let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); + FdStatus::decode(payload).unwrap() + } + + #[test] + fn offer_accept_reaches_ready_xfer() { + let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::with_offer())); + + assert_eq!( + query_status(&mut t), + FdStatus::OfferPending { + target: 0x0001, + total: 0x0010_0000, + mode: TransferMode::InTransport, + svn_delayed: false, + } + ); + + send_ok(&mut t, |b| wire::encode_accept_offer(b, 0x2000_0000)); + assert_eq!(query_status(&mut t), FdStatus::ReadyXfer); + } + + // Transfer is UA-driven (no IPC op). The FD enters VerifyPending + // when the transfer completes, so the grant sequence starts there. + #[test] + fn grant_sequence_verify_through_idle() { + let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::at( + FdStatus::VerifyPending, + ))); + + send_ok(&mut t, |b| wire::encode_grant_verify(b)); + assert_eq!(query_status(&mut t), FdStatus::ApplyPending); + + send_ok(&mut t, |b| wire::encode_grant_apply(b)); + assert_eq!(query_status(&mut t), FdStatus::ActivationPending); + + send_ok(&mut t, |b| wire::encode_grant_activate(b)); + assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); + } + + #[test] + fn svn_commit_after_activation() { + let mut t = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::at( + FdStatus::SvnCommitPending { component: 1 }, + ))); + + send_ok(&mut t, |b| wire::encode_grant_svn_commit(b)); + assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); + } + + #[test] + fn ack_cancel_returns_to_idle() { + let mut t = + Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::at(FdStatus::Cancelled))); + + send_ok(&mut t, |b| wire::encode_ack_cancel(b)); + assert_eq!(query_status(&mut t), FdStatus::Idle { reason: 0 }); + } + + #[test] + fn an_oversized_request_is_answered_by_dispatch_not_the_transport() { + // A loopback has no request buffer to overflow, so nothing caps the + // request at the transport. The decoder rejects it instead and the + // caller gets an error frame, the same answer a malformed request of + // any length gets. + let mut transport = Loopback::<_, MAX_RESPONSE_SIZE>::new(FdServer::new(StubFd::idle())); + let req = [0u8; MAX_REQUEST_SIZE + 1]; + let mut resp = [0u8; MAX_RESPONSE_SIZE]; + + transport.start(&req).unwrap(); + let len = transport.poll(&mut resp).unwrap().unwrap(); + + let h = wire::decode_response_header(&resp[..len]).unwrap(); + assert!(!h.is_success()); + } } diff --git a/services/pldm/ipc-server/src/loopback.rs b/services/pldm/ipc-server/src/loopback.rs deleted file mode 100644 index 157b0ece..00000000 --- a/services/pldm/ipc-server/src/loopback.rs +++ /dev/null @@ -1,274 +0,0 @@ -// Licensed under the Apache-2.0 license -// SPDX-License-Identifier: Apache-2.0 - -//! In-process loopback transport for host testing. -//! -//! Calls `dispatch` directly, no kernel, no IPC channel. The client -//! crate's encoders and decoders exercise the real server dispatch -//! path through this transport. - -use pldm_ipc_api::transport::{Transport, TransportError}; -use pldm_ipc_api::wire::{MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE}; - -use crate::{dispatch, FdHandler}; - -/// Loopback transport that dispatches in-process. -/// -/// Owns the `FdHandler` so the client and server share a single -/// process with no concurrency. Useful for host-side integration -/// tests and for validating wire format round-trips. -/// -/// Dispatch runs inside `start`, so the response is ready on the first -/// `poll`. The split-phase shape is kept so client code written against -/// a real channel runs unchanged here. -pub struct LoopbackTransport { - handler: F, - response: [u8; MAX_RESPONSE_SIZE], - /// Response length, set while a round-trip is in flight. - pending: Option, -} - -impl LoopbackTransport { - pub fn new(handler: F) -> Self { - Self { - handler, - response: [0u8; MAX_RESPONSE_SIZE], - pending: None, - } - } -} - -impl Transport for LoopbackTransport { - fn start(&mut self, req: &[u8]) -> Result<(), TransportError> { - if self.pending.is_some() { - return Err(TransportError::WrongState); - } - if req.len() > MAX_REQUEST_SIZE { - return Err(TransportError::TooLarge); - } - - let n = dispatch(&mut self.handler, req, &mut self.response); - if n == 0 { - return Err(TransportError::Failed); - } - self.pending = Some(n); - Ok(()) - } - - fn poll(&mut self, resp: &mut [u8]) -> Result, TransportError> { - let n = self.pending.ok_or(TransportError::WrongState)?; - if resp.len() < n { - self.pending = None; - return Err(TransportError::TooLarge); - } - resp[..n].copy_from_slice(&self.response[..n]); - self.pending = None; - Ok(Some(n)) - } - - fn cancel(&mut self) -> Result<(), TransportError> { - if self.pending.take().is_none() { - return Err(TransportError::WrongState); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - /// Start one request and poll the response out, as the client layer - /// does; the loopback always has it ready on the first poll. - fn round_trip( - transport: &mut LoopbackTransport, - req: &[u8], - resp: &mut [u8], - ) -> usize { - transport.start(req).unwrap(); - transport.poll(resp).unwrap().unwrap() - } - use pldm_ipc_api::status::TransferMode; - use pldm_ipc_api::wire::{self, MAX_RESPONSE_SIZE}; - use pldm_ipc_api::{DenyReason, FdStatus, ResponseCode}; - - /// Minimal handler for loopback tests. - struct StubFd { - status: FdStatus, - } - - impl StubFd { - fn idle() -> Self { - Self { - status: FdStatus::Idle { reason: 0 }, - } - } - - fn with_offer() -> Self { - Self { - status: FdStatus::OfferPending { - target: 0x0001, - total: 0x0010_0000, - mode: TransferMode::InTransport, - svn_delayed: false, - }, - } - } - } - - impl FdHandler for StubFd { - fn accept_offer(&mut self, _base: u32) -> Result<(), ResponseCode> { - self.status = FdStatus::ReadyXfer; - Ok(()) - } - fn reject_offer(&mut self) -> Result<(), ResponseCode> { - self.status = FdStatus::Idle { reason: 0 }; - Ok(()) - } - fn grant_verify(&mut self) -> Result<(), ResponseCode> { - Ok(()) - } - fn deny_verify(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { - Ok(()) - } - fn grant_apply(&mut self) -> Result<(), ResponseCode> { - Ok(()) - } - fn deny_apply(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { - Ok(()) - } - fn query_status(&mut self) -> Result { - Ok(self.status) - } - fn grant_activate(&mut self) -> Result<(), ResponseCode> { - Ok(()) - } - fn deny_activate(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { - Ok(()) - } - fn ack_cancel(&mut self) -> Result<(), ResponseCode> { - Ok(()) - } - fn grant_svn_commit(&mut self) -> Result<(), ResponseCode> { - Ok(()) - } - fn deny_svn_commit(&mut self, _reason: DenyReason) -> Result<(), ResponseCode> { - Ok(()) - } - } - - #[test] - fn query_status_through_loopback() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let mut req = [0u8; 16]; - let req_len = wire::encode_query_status(&mut req).unwrap(); - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); - assert!(h.is_success()); - let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); - let status = FdStatus::decode(payload).unwrap(); - assert_eq!(status, FdStatus::Idle { reason: 0 }); - } - - #[test] - fn accept_offer_then_query_shows_ready_xfer() { - let mut transport = LoopbackTransport::new(StubFd::with_offer()); - - // Accept the offer. - let mut req = [0u8; 16]; - let req_len = wire::encode_accept_offer(&mut req, 0x2000_0000).unwrap(); - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); - assert!(h.is_success()); - - // Query status: should now be ReadyXfer. - let req_len = wire::encode_query_status(&mut req).unwrap(); - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); - let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); - let status = FdStatus::decode(payload).unwrap(); - assert_eq!(status, FdStatus::ReadyXfer); - } - - #[test] - fn reject_offer_then_query_shows_idle() { - let mut transport = LoopbackTransport::new(StubFd::with_offer()); - - let mut req = [0u8; 16]; - let req_len = wire::encode_reject_offer(&mut req).unwrap(); - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); - assert!(h.is_success()); - - let req_len = wire::encode_query_status(&mut req).unwrap(); - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - let h = wire::decode_response_header(&resp[..resp_len]).unwrap(); - let payload = wire::get_response_payload(&resp[..resp_len], &h).unwrap(); - let status = FdStatus::decode(payload).unwrap(); - assert_eq!(status, FdStatus::Idle { reason: 0 }); - } - - #[test] - fn start_while_pending_is_wrong_state() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let mut req = [0u8; 16]; - let req_len = wire::encode_query_status(&mut req).unwrap(); - - transport.start(&req[..req_len]).unwrap(); - assert_eq!( - transport.start(&req[..req_len]), - Err(TransportError::WrongState) - ); - } - - #[test] - fn poll_without_start_is_wrong_state() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); - } - - #[test] - fn cancel_releases_the_round_trip() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let mut req = [0u8; 16]; - let req_len = wire::encode_query_status(&mut req).unwrap(); - - transport.start(&req[..req_len]).unwrap(); - transport.cancel().unwrap(); - assert_eq!(transport.cancel(), Err(TransportError::WrongState)); - - // The transport is usable again after a cancel. - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - let resp_len = round_trip(&mut transport, &req[..req_len], &mut resp); - assert!(wire::decode_response_header(&resp[..resp_len]) - .unwrap() - .is_success()); - } - - #[test] - fn poll_into_a_short_buffer_is_too_large() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let mut req = [0u8; 16]; - let req_len = wire::encode_query_status(&mut req).unwrap(); - - transport.start(&req[..req_len]).unwrap(); - let mut resp = [0u8; 1]; - assert_eq!(transport.poll(&mut resp), Err(TransportError::TooLarge)); - // The failed poll ended the round-trip. - assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); - } - - #[test] - fn start_with_an_oversized_request_is_too_large() { - let mut transport = LoopbackTransport::new(StubFd::idle()); - let req = [0u8; MAX_REQUEST_SIZE + 1]; - assert_eq!(transport.start(&req), Err(TransportError::TooLarge)); - // Nothing was started, so there is nothing to poll. - let mut resp = [0u8; MAX_RESPONSE_SIZE]; - assert_eq!(transport.poll(&mut resp), Err(TransportError::WrongState)); - } -}