blob: b6d8171d2d3ba6120ccc787d02297f626673bfec [file]
// Licensed under the Apache-2.0 license
//! MCTP Server — IPC Dispatch Loop
//!
//! Userspace service that receives MCTP requests over a Pigweed IPC channel,
//! dispatches them to the MCTP server core, and responds with results.
//!
//! # Build Modes
//!
//! Two mutually exclusive event loops are compiled depending on feature flags:
//!
//! | Features | Mode | IPC served | SPDM in-process |
//! |---|---|---|---|
//! | _(none)_ | Notification (WaitGroup + IRQ) | Yes | No |
//! | `i2c-polling` | Polling | No | No |
//! | `i2c-polling` + `direct-client` | Polling + SPDM | No | Yes |
//!
//! # Notification Mode Architecture (default)
//!
//! ```text
//! ┌─ Client ──────────────────────────┐
//! │ channel_transact(request) │
//! └──────────────┬────────────────────┘
//! │ IPC channel
//! ▼
//! ┌─ This Server ─────────────────────┐
//! │ object_wait(WG, READABLE) │
//! │ ├─ user_data=0: IPC client │
//! │ │ channel_read / dispatch / │
//! │ │ channel_respond │
//! │ └─ user_data=1: I2C slave IRQ │
//! │ get_pending_messages │
//! │ receiver.decode → inbound() │
//! └──────────────┬────────────────────┘
//! │ mctp-stack Router
//! ▼
//! ┌─ I2C Transport ──────────────────┐
//! │ I2cSender → I2C Server IPC │
//! └──────────────────────────────────┘
//! ```
//!
//! # In-Process SPDM Responder Architecture (`i2c-polling` + `direct-client`)
//!
//! When both features are enabled the SPDM responder runs inside this process.
//! `DirectMctpClient` replaces the IPC channel — it calls `Server` methods
//! directly via a `RefCell`. No separate `spdm_responder` app process is needed.
//!
//! ```text
//! ┌─ I2C Hardware ───────────────────────────────────────┐
//! │ Slave-mode frames arrive on bus 2 │
//! └──────────────┬───────────────────────────────────────┘
//! │ wait_for_messages() → TargetMessage
//! ▼
//! ┌─ This Process ───────────────────────────────────────┐
//! │ Phase 1: receiver.decode() → raw MCTP packet │
//! │ server_cell.borrow_mut().inbound(pkt) │
//! │ └─ Router reassembles fragments │
//! │ │
//! │ Phase 2: spdm_ctx.responder_process_message() │
//! │ │ MctpSpdmTransport::recv_request() │
//! │ │ DirectMctpClient::recv() │
//! │ │ server_cell.borrow_mut().try_recv() │
//! │ │ └─ returns Ok(msg) or Err(TimedOut) │
//! │ └─ on TimedOut: no-op, loop continues │
//! │ └─ on Ok: SPDM state machine processes request │
//! │ MctpSpdmTransport::send_response() │
//! │ DirectMctpClient::send() │
//! │ server_cell.borrow_mut().send() │
//! │ I2cSender → I2C Server IPC → wire │
//! └──────────────────────────────────────────────────────┘
//! ```
//!
//! # IPC Pattern (notification mode)
//!
//! 1. `object_wait(WG, READABLE)` — block until an event fires
//! 2. `channel_read(MCTP, ...)` — read raw request bytes from IPC client
//! 3. `dispatch_mctp_op(...)` — decode and execute the MCTP operation
//! 4. `channel_respond(MCTP, ...)` — send response back to client
//!
//! # Handle Binding
//!
//! Handles are provided by the `app_package` Bazel rule, generated from
//! `system.json5` into `app_mctp_server::handle::{I2C, MCTP, WG}`.
#![no_main]
#![no_std]
// Imports shared by both modes.
use i2c_api::{BusIndex, I2cAddress, I2cTargetClient, TargetMessage};
use i2c_client::IpcI2cClient;
use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver};
use openprot_mctp_api::wire::MAX_PAYLOAD_SIZE;
use pw_status::Result;
use userspace::entry;
use userspace::syscall;
use app_mctp_server::handle;
// Imports used only by the in-process SPDM responder (i2c-polling + direct-client).
#[cfg(feature = "i2c-polling")]
use core::cell::RefCell;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use mock_platform::{MockCertStore, MockEvidence, MockHash, MockRng};
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use openprot_mctp_server::direct_client::DirectMctpClient;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use openprot_spdm_transport_mctp::MctpSpdmTransport;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use spdm_lib::codec::MessageBuf;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use spdm_lib::context::SpdmContext;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use spdm_lib::platform::transport::SpdmTransport;
#[cfg(all(feature = "i2c-polling", feature = "direct-client"))]
use spdm_lib::protocol::{
AeadCipherSuite, AlgorithmPriorityTable, BaseAsymAlgo, BaseHashAlgo, CapabilityFlags,
DeviceAlgorithms, DeviceCapabilities, DheNamedGroup, KeySchedule, LocalDeviceAlgorithms,
MeasurementHashAlgo, MeasurementSpecification, MelSpecification, OtherParamSupport,
ReqBaseAsymAlg, SpdmVersion,
};
// Imports used only by the notification (WaitGroup + IRQ) loop.
#[cfg(not(feature = "i2c-polling"))]
use openprot_mctp_api::wire::{MctpRequestHeader, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE};
#[cfg(not(feature = "i2c-polling"))]
use openprot_mctp_api::ResponseCode;
#[cfg(not(feature = "i2c-polling"))]
use openprot_mctp_server::dispatch;
#[cfg(not(feature = "i2c-polling"))]
use userspace::syscall::Signals;
#[cfg(not(feature = "i2c-polling"))]
use userspace::time::Instant;
const OWN_EID: u8 = 8;
const OWN_I2C_ADDR: u8 = 0x10;
// ---------------------------------------------------------------------------
// Polling mode — blocks on wait_for_messages(); no WaitGroup or IRQ needed.
// When built with the "direct-client" feature, an SPDM responder runs
// in-process using DirectMctpClient (no IPC channel to a separate process).
// Enable with crate_features = ["i2c-polling", "direct-client"] in Bazel.
// ---------------------------------------------------------------------------
#[cfg(feature = "i2c-polling")]
fn mctp_loop() -> Result<()> {
pw_log::info!("MCTP server starting (I2C polling mode, eid=0x{:02x} addr=0x{:02x})",
OWN_EID as u32, OWN_I2C_ADDR as u32);
let mut i2c = IpcI2cClient::new(handle::I2C);
pw_log::info!("MCTP server: configuring I2C target address 0x{:02x} on bus 2",
OWN_I2C_ADDR as u32);
let addr = I2cAddress::new(OWN_I2C_ADDR).map_err(|_| {
pw_log::error!("MCTP server: invalid I2C address 0x{:02x}", OWN_I2C_ADDR as u32);
pw_status::Error::InvalidArgument
})?;
i2c.configure_target_address(BusIndex::BUS_2, addr).map_err(|_| {
pw_log::error!("MCTP server: configure_target_address failed");
pw_status::Error::Internal
})?;
pw_log::info!("MCTP server: enabling I2C receive on bus 2");
i2c.enable_receive(BusIndex::BUS_2).map_err(|_| {
pw_log::error!("MCTP server: enable_receive failed");
pw_status::Error::Internal
})?;
let sender = I2cSender::new(IpcI2cClient::new(handle::I2C), BusIndex::BUS_2, OWN_I2C_ADDR);
let receiver = MctpI2cReceiver::new(OWN_I2C_ADDR);
// RefCell lets DirectMctpClient borrow `server` alongside the I2C path in
// the same polling loop. A single name is used in both cfg variants so
// every call site is uniform.
let server = RefCell::new(openprot_mctp_server::Server::<_, 16>::new(
mctp::Eid(OWN_EID),
0,
sender,
));
// ---------------------------------------------------------------------------
// SPDM responder setup (only when direct-client feature is enabled)
// ---------------------------------------------------------------------------
#[cfg(feature = "direct-client")]
let mut transport = {
let client = DirectMctpClient::new(&server);
MctpSpdmTransport::new_responder(client)
};
#[cfg(feature = "direct-client")]
{
pw_log::info!("MCTP server: registering SPDM listener (msg_type=0x05)");
if transport.init_sequence().is_err() {
pw_log::error!("MCTP server: S PDM transport init_sequence failed \
listener(0x05) rejected; router listener table may be full");
return Err(pw_status::Error::Internal);
}
pw_log::info!("MCTP server: SPDM listener registered");
}
#[cfg(feature = "direct-client")]
let mut cert_store = MockCertStore::new();
#[cfg(feature = "direct-client")]
let mut hash = MockHash::new();
#[cfg(feature = "direct-client")]
let mut m1_hash = MockHash::new();
#[cfg(feature = "direct-client")]
let mut l1_hash = MockHash::new();
#[cfg(feature = "direct-client")]
let mut rng = MockRng::new();
#[cfg(feature = "direct-client")]
let evidence = MockEvidence::new();
#[cfg(feature = "direct-client")]
let mut flags = CapabilityFlags::default();
#[cfg(feature = "direct-client")]
{
flags.set_cert_cap(1);
flags.set_chal_cap(1);
flags.set_meas_cap(2);
flags.set_meas_fresh_cap(1);
flags.set_chunk_cap(1);
}
#[cfg(feature = "direct-client")]
let capabilities = DeviceCapabilities {
ct_exponent: 0,
flags,
data_transfer_size: 1024,
max_spdm_msg_size: 4096,
include_supported_algorithms: true,
};
#[cfg(feature = "direct-client")]
static SUPPORTED_VERSIONS: [SpdmVersion; 2] = [SpdmVersion::V12, SpdmVersion::V13];
#[cfg(feature = "direct-client")]
let algorithms = {
let mut measurement_spec = MeasurementSpecification::default();
measurement_spec.set_dmtf_measurement_spec(1);
let mut measurement_hash_algo = MeasurementHashAlgo::default();
measurement_hash_algo.set_tpm_alg_sha_384(1);
let mut base_asym_algo = BaseAsymAlgo::default();
base_asym_algo.set_tpm_alg_ecdsa_ecc_nist_p384(1);
let mut base_hash_algo = BaseHashAlgo::default();
base_hash_algo.set_tpm_alg_sha_384(1);
let device_algorithms = DeviceAlgorithms {
measurement_spec,
other_param_support: OtherParamSupport::default(),
measurement_hash_algo,
base_asym_algo,
base_hash_algo,
mel_specification: MelSpecification::default(),
dhe_group: DheNamedGroup::default(),
aead_cipher_suite: AeadCipherSuite::default(),
req_base_asym_algo: ReqBaseAsymAlg::default(),
key_schedule: KeySchedule::default(),
};
LocalDeviceAlgorithms {
device_algorithms,
algorithm_priority_table: AlgorithmPriorityTable {
measurement_specification: None,
opaque_data_format: None,
base_asym_algo: None,
base_hash_algo: None,
mel_specification: None,
dhe_group: None,
aead_cipher_suite: None,
req_base_asym_algo: None,
key_schedule: None,
},
}
};
#[cfg(feature = "direct-client")]
let mut spdm_ctx = {
pw_log::info!("MCTP server: creating SPDM context (v1.2+v1.3, ECDSA-P384, SHA-384)");
match SpdmContext::new(
&SUPPORTED_VERSIONS,
&mut transport,
capabilities,
algorithms,
&mut cert_store,
None,
&mut hash,
&mut m1_hash,
&mut l1_hash,
&mut rng,
&evidence,
) {
Ok(ctx) => {
pw_log::info!("MCTP server: SPDM context ready");
ctx
}
Err(_) => {
pw_log::error!("MCTP server: SpdmContext::new failed \
check cert_store, hash, rng platform impls");
return Err(pw_status::Error::Internal);
}
}
};
#[cfg(feature = "direct-client")]
let mut spdm_buf = [0u8; MAX_PAYLOAD_SIZE];
// MessageBuf is created once outside the loop; reset() is called each iteration.
// Creating it inside the loop would leave the mutable borrow of spdm_buf live
// across loop iterations, which the borrow checker rejects (E0499).
#[cfg(feature = "direct-client")]
let mut msg_buf = MessageBuf::new(&mut spdm_buf);
// Fault-isolation counters — logged on first error and at every 16th recurrence.
let mut i2c_recv_err: u32 = 0;
let mut decode_err: u32 = 0;
let mut inbound_err: u32 = 0;
#[cfg(feature = "direct-client")]
let mut spdm_ok: u32 = 0;
#[cfg(feature = "direct-client")]
let mut spdm_err: u32 = 0;
let mut i2c_pkt: u32 = 0;
let mut idle_polls: u32 = 0;
pw_log::info!("MCTP server ready, polling for I2C packets");
loop {
// Phase 1: drain inbound I2C packets into the MCTP router.
let mut msgs = [TargetMessage::default(); 1];
match i2c.wait_for_messages(BusIndex::BUS_2, &mut msgs, None) {
Ok(n) => {
for msg in msgs.get(..n).unwrap_or(&[]) {
let raw = msg.data();
// WORKAROUND: Manually prepend destination address to I2C frame.
// The I2C hardware currently does NOT prepend the destination address,
// but mctp-lib's I2C decoder expects it. This will be fixed when the
// I2C driver is updated to prepend the destination address automatically.
// TODO: Remove this workaround once I2C driver change is in place.
let mut frame_with_dest = [0u8; 256];
if raw.len() + 1 > frame_with_dest.len() {
pw_log::warn!(
"I2C frame too large ({} bytes), skipping",
raw.len() as u32,
);
continue;
}
// Prepend destination address (OWN_I2C_ADDR << 1 | 0 for write)
frame_with_dest[0] = OWN_I2C_ADDR << 1;
frame_with_dest[1..raw.len() + 1].copy_from_slice(raw);
let frame_len = raw.len() + 1;
// Log the frame AFTER prepending destination
// Format: dest_addr cmd byte_count src_addr | MCTP hdr[0..3]
if frame_len >= 9 {
pw_log::info!(
"I2C frame (with prepended dest): dest=0x{:02x} cmd=0x{:02x} bc={} src=0x{:02x} \
| mctp: ver=0x{:02x} deid=0x{:02x} seid=0x{:02x} flags=0x{:02x} \
len={}",
frame_with_dest[0] as u32, frame_with_dest[1] as u32,
frame_with_dest[2] as u32, frame_with_dest[3] as u32,
frame_with_dest[4] as u32, frame_with_dest[5] as u32,
frame_with_dest[6] as u32, frame_with_dest[7] as u32,
frame_len as u32,
);
} else {
pw_log::warn!(
"I2C frame too short ({} bytes) to contain MCTP header",
frame_len as u32,
);
}
// Create temporary TargetMessage with prepended destination
let msg_with_dest = i2c_api::TargetMessage::from_data(
msg.source_address,
&frame_with_dest[..frame_len]
);
match receiver.decode(&msg_with_dest) {
Ok((pkt, src_addr)) => {
i2c_pkt = i2c_pkt.wrapping_add(1);
// Log MCTP packet fields: SOM/EOM from flags byte (pkt[3])
let som = pkt.get(3).map_or(0u8, |b| (b >> 7) & 1);
let eom = pkt.get(3).map_or(0u8, |b| (b >> 6) & 1);
let seq = pkt.get(3).map_or(0u8, |b| (b >> 4) & 0x3);
let msg_type = pkt.get(4).map_or(0u8, |b| b & 0x7f);
pw_log::info!(
"MCTP pkt #{}: src_i2c=0x{:02x} dest_i2c=0x{:02x} bc={} len={} \
SOM={} EOM={} seq={} msg_type=0x{:02x}",
i2c_pkt as u32,
src_addr.source as u32,
src_addr.dest as u32,
src_addr.byte_count as u32,
pkt.len() as u32,
som as u32, eom as u32, seq as u32, msg_type as u32,
);
match server.borrow_mut().inbound(pkt) {
Ok(Some(cookie)) => {
pw_log::debug!(
"MCTP pkt #{} associated with cookie {}",
i2c_pkt as u32,
cookie.0 as u32,
);
}
Ok(None) => {
pw_log::debug!("MCTP pkt #{} discarded (no matching handler)", i2c_pkt as u32);
}
Err(e) => {
inbound_err = inbound_err.wrapping_add(1);
if inbound_err == 1 || inbound_err & 0xf == 0 {
pw_log::error!(
"MCTP server: inbound() error code={} \
total_inbound_errors={}",
e.code as u32,
inbound_err as u32,
);
}
}
}
}
Err(_) => {
decode_err = decode_err.wrapping_add(1);
if decode_err == 1 || decode_err & 0xf == 0 {
pw_log::error!(
"MCTP server: I2C frame decode failed \
total_decode_errors={}",
decode_err as u32,
);
}
}
}
}
}
Err(e) if e.is_timeout() => {
// Timeout is the normal "no frame yet" result from the backend's
// poll-budget loop — not a real error. Just proceed to Phase 2
// (SPDM responder poll) and loop again.
idle_polls = idle_polls.wrapping_add(1);
if idle_polls & 0xfff == 0 {
pw_log::info!(
"MCTP alive: idle_polls={} pkts={}",
idle_polls as u32,
i2c_pkt as u32,
);
}
}
Err(_) => {
i2c_recv_err = i2c_recv_err.wrapping_add(1);
if i2c_recv_err == 1 || i2c_recv_err & 0xf == 0 {
pw_log::error!(
"MCTP server: wait_for_messages failed \
total_i2c_recv_errors={}",
i2c_recv_err as u32,
);
}
}
}
// Phase 2: poll the SPDM responder for an assembled message.
// Transport returning TimedOut means no SPDM message is assembled yet
// — this is the normal steady-state; log only genuine protocol errors.
#[cfg(feature = "direct-client")]
{
msg_buf.reset();
match spdm_ctx.responder_process_message(&mut msg_buf) {
Ok(_) => {
spdm_ok = spdm_ok.wrapping_add(1);
pw_log::info!(
"MCTP server: SPDM request processed ok total_spdm_ok={}",
spdm_ok as u32,
);
}
Err(_) => {
// Most errors here are TransportError::NoRequestInFlight
// (TimedOut from DirectMctpClient::recv — no message ready).
// Count and log only on first occurrence and every 256th after,
// to avoid flooding the log during idle polling.
spdm_err = spdm_err.wrapping_add(1);
if spdm_err == 1 || spdm_err & 0xff == 0 {
pw_log::debug!(
"MCTP server: responder_process_message returned Err \
(likely no message yet) total_spdm_err={}",
spdm_err as u32,
);
}
}
}
}
}
}
// ---------------------------------------------------------------------------
// Notification mode — WaitGroup multiplexes IPC (MCTP channel) and the I2C
// slave interrupt (USER signal). Serves both local IPC MCTP clients and
// inbound I2C transport. Default when "i2c-polling" feature is not set.
// Requires WG and I2C2_IRQ objects in system.json5.
// ---------------------------------------------------------------------------
#[cfg(not(feature = "i2c-polling"))]
fn mctp_loop() -> Result<()> {
pw_log::info!("MCTP server starting");
// I2C notification client: receives slave-mode interrupts via Signals::USER.
let mut i2c_notify = IpcI2cClient::new(handle::I2C);
// Configure I2C bus 2 as slave at our address
let addr = I2cAddress::new(OWN_I2C_ADDR).map_err(|_| pw_status::Error::InvalidArgument)?;
i2c_notify
.configure_target_address(BusIndex::BUS_2, addr)
.map_err(|_| pw_status::Error::Internal)?;
// Enable slave receive mode
i2c_notify
.enable_receive(BusIndex::BUS_2)
.map_err(|_| pw_status::Error::Internal)?;
// Register for notifications
i2c_notify
.register_notification(BusIndex::BUS_2, 0)
.map_err(|_| pw_status::Error::Internal)?;
// Separate handle for the sender — I2cSender takes ownership.
let sender = I2cSender::new(IpcI2cClient::new(handle::I2C), BusIndex::BUS_2, OWN_I2C_ADDR);
let receiver = MctpI2cReceiver::new(OWN_I2C_ADDR);
let mut server = openprot_mctp_server::Server::<_, 16>::new(
mctp::Eid(OWN_EID),
0,
sender,
);
let mut request_buf = [0u8; MAX_REQUEST_SIZE];
let mut response_buf = [0u8; MAX_RESPONSE_SIZE];
let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE];
// Register both event sources with the WaitGroup.
// user_data=0 → IPC from a client (MCTP channel READABLE)
// user_data=1 → I2C slave notification (I2C channel USER)
syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?;
syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?;
pw_log::info!("MCTP server ready, entering event loop");
loop {
let ev = syscall::object_wait(handle::WG, Signals::READABLE, Instant::MAX)?;
if ev.user_data == 1 {
// Inbound I2C data: drain pending messages, decode I2C framing,
// feed raw MCTP packets into the router.
let mut msgs = [TargetMessage::default(); 1];
if let Ok(n) = i2c_notify.get_pending_messages(BusIndex::BUS_2, &mut msgs) {
for msg in &msgs[..n] {
if let Ok((pkt, _src_addr)) = receiver.decode(msg) {
// Feed packet to server; ignore result (no detailed logging in this path)
let _ = server.inbound(pkt);
}
}
}
} else {
// IPC from a client — channel_read is non-blocking here because
// the WaitGroup only fires after READABLE is set.
let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?;
if len < MctpRequestHeader::SIZE {
// Truncated request — respond with error
let resp = openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument);
response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE]
.copy_from_slice(&resp.to_bytes());
syscall::channel_respond(
handle::MCTP,
&response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE],
)?;
continue;
}
// Dispatch and respond
let response_len = dispatch::dispatch_mctp_op(
&request_buf[..len],
&mut response_buf,
&mut server,
&mut recv_buf,
);
syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?;
}
}
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
#[entry]
fn entry() -> ! {
if let Err(e) = mctp_loop() {
pw_log::error!("MCTP server error: {}", e as u32);
let _ = syscall::debug_shutdown(Err(e));
}
loop {}
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}