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..39d51067 --- /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 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. 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 wire; + +pub use error::{DenyReason, PldmIpcError, ResponseCode, WireError}; +pub use status::{FdStatus, TransferMode}; +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/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)); + } +} diff --git a/services/pldm/ipc-server/BUILD.bazel b/services/pldm/ipc-server/BUILD.bazel new file mode 100644 index 00000000..704f385e --- /dev/null +++ b/services/pldm/ipc-server/BUILD.bazel @@ -0,0 +1,20 @@ +# 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", + "//util/service", + ], +) + +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..50c246a2 --- /dev/null +++ b/services/pldm/ipc-server/src/lib.rs @@ -0,0 +1,742 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! PLDM IPC server: the FD side of the IPC channel. +//! +//! 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] + +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. +/// +/// 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`. 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) + } +} + +fn dispatch_inner( + handler: &mut F, + request: &[u8], + response: &mut [u8], +) -> Result { + let header = wire::decode_request_header(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 => { + 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).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}"); + } + + #[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).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(); + 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).unwrap(); + 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).unwrap(); + 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).unwrap(); + 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).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 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).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 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).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()); + } +}