mctp-server: add in-process SPDM requester Parallel to the existing in-process SPDM responder. Same src/main.rs, selected by Cargo feature flag (in-process-responder vs in-process-requester) on the Bazel rust_app target, enforced mutually exclusive via compile_error!. - New system_image at //target/ast1060-evb/mctp-requester:mctp_requester; the existing //target/ast1060-evb/mctp:mctp keeps the responder role. - Second rust_app target mctp_server_requester in services/mctp/server/ with distinct codegen_crate_name (app_mctp_server_requester). - Phase-2 loop body runs an 8-state VCA FSM one step per iteration (SendVersion -> AwaitVersion -> ... -> Done / Failed), interleaved with the existing Phase-1 I2C drain. - AWAIT_STEP_BUDGET (10_000) safety net: an Await state that sits on TimedOut too long transitions to Failed instead of hanging. - Counters req_send_ok / req_recv_ok / req_recv_pending logged on the Done transition. Bundled pre-Phase-1 unblockers for the mctp-lib hotfix at 023e75e: - Server::inbound now discards the new Option<AppCookie> return. - main.rs consumes the MctpI2cHeader struct from receiver.decode() (was a bare u8 source address). Docs updated: DESIGN_IN_PROCESS_REQUESTER.md, IMPLEMENTATION_PLAN.md (new), INITIALIZATION.md (requester setup + glossary). Build verification: bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester
diff --git a/services/mctp/server/BUILD.bazel b/services/mctp/server/BUILD.bazel index d22ca9d..d90e598 100644 --- a/services/mctp/server/BUILD.bazel +++ b/services/mctp/server/BUILD.bazel
@@ -104,6 +104,7 @@ crate_features = [ "i2c-polling", "direct-client", + "in-process-responder", ], edition = "2024", system_config = "//target/ast1060-evb/mctp:system_config", @@ -126,3 +127,37 @@ "@oot_crates_no_std//:spdm-lib", ], ) + +# In-process SPDM requester variant. Same src/main.rs, different feature +# set. No separate system_image target yet — use the mctp:mctp image once +# it is re-wired, or build this target alone to verify compilation. +rust_app( + name = "mctp_server_requester", + codegen_crate_name = "app_mctp_server_requester", + srcs = ["src/main.rs"], + crate_features = [ + "i2c-polling", + "direct-client", + "in-process-requester", + ], + edition = "2024", + system_config = "//target/ast1060-evb/mctp-requester:system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + ":mctp_server_lib", + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/mctp/api:mctp_api", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "//target/ast1060-evb/spdm-req-resp-test:mock_platform", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@oot_crates_no_std//:spdm-lib", + ], +)
diff --git a/services/mctp/server/Cargo.toml b/services/mctp/server/Cargo.toml index 570e3b5..22f8eb0 100644 --- a/services/mctp/server/Cargo.toml +++ b/services/mctp/server/Cargo.toml
@@ -18,14 +18,21 @@ [features] # Polling mode + in-process SPDM responder is the default build. # Disable with --no-default-features to get the notification (WaitGroup + IRQ) mode. -default = ["i2c-polling", "direct-client"] +default = ["i2c-polling", "in-process-responder"] # Build mctp_server with a blocking I2C poll loop instead of the WaitGroup + # IRQ-driven notification loop. No WG or I2C2_IRQ capability is required. # Enable via crate_features = ["i2c-polling"] in the Bazel rust_app target. i2c-polling = [] # Expose DirectMctpClient for in-process use (no IPC channel required). -# Required when running an SPDM responder inside the MCTP server process. +# Required by in-process-responder and in-process-requester; on its own it +# only exposes the type from the library without instantiating any SPDM role. direct-client = [] +# Run an SPDM responder inside the MCTP server process (polling-loop Phase 2). +# Mutually exclusive with in-process-requester. +in-process-responder = ["direct-client"] +# Run an SPDM requester inside the MCTP server process (polling-loop Phase 2). +# Mutually exclusive with in-process-responder. +in-process-requester = ["direct-client"] [dev-dependencies] mctp-lib = { git = "https://github.com/9elements/mctp-lib.git", branch = "buildup", package = "mctp-lib" }
diff --git a/services/mctp/server/DESIGN_IN_PROCESS_REQUESTER.md b/services/mctp/server/DESIGN_IN_PROCESS_REQUESTER.md new file mode 100644 index 0000000..f8a6e4c --- /dev/null +++ b/services/mctp/server/DESIGN_IN_PROCESS_REQUESTER.md
@@ -0,0 +1,465 @@ +# In-Process SPDM Requester — Design Spec + +Status: draft +Scope: add an in-process SPDM **requester** to the MCTP server, mirroring the +existing in-process SPDM responder at +[src/main.rs:144-446](src/main.rs#L144-L446). + +--- + +## 1. Motivation + +The responder path collapses three user-space processes (mctp_server + +spdm_responder + i2c_server) into two by running the SPDM responder inside +the MCTP server via `DirectMctpClient`. We want the symmetric capability on +the requester side: drive SPDM VCA / attestation flows from inside the MCTP +server without an IPC channel to a separate `spdm_requester` app. + +Benefits (same as responder): + +- No `channel_handler` / `channel_initiator` pair between mctp_server and + spdm_requester — lower RAM, fewer kernel objects in `system.json5`. +- No IPC serialize/deserialize on every SPDM request. +- One process to flash/debug for requester-side scenarios. + +--- + +## 2. Key Asymmetry vs. the Responder + +The responder is **passive** — it consumes what arrives. Its loop is +naturally polling-friendly: + +``` +loop { + drain_i2c_into_router(); // Phase 1 + let _ = ctx.responder_process_message(..); // Phase 2: TimedOut == normal +} +``` + +The requester is **active** — it initiates exchanges. `requester_process_message` +internally calls `receive_response`, which calls `client.recv(..)`. +`DirectMctpClient::recv` returns `Err(TimedOut)` immediately if the Router has +not yet assembled a full message. So a requester that calls +`requester_process_message` before I2C has delivered the response will *fail +fast* instead of blocking. + +The design must therefore **interleave I2C draining with SPDM progress** while +the requester is waiting for a response. Two viable shapes: + +| # | Shape | Loop call site | SPDM call site | +|---|---|---|---| +| A | External FSM with two-phase loop (same shape as responder) | mctp_server main | Only advances when I2C has produced a message | +| B | Blocking client that pumps I2C inside `recv` | mctp_server main (simpler) | `requester_process_message` looks synchronous | + +We pick **Shape A** for symmetry with the responder and because it keeps +`DirectMctpClient` unchanged and decoupled from I2C. Shape B is recorded in +§9 as a future simplification. + +--- + +## 3. Architecture + +```text +┌─ I2C Hardware ───────────────────────────────────────┐ +│ Slave-mode frames arrive on bus 2 │ +│ (responses from the remote SPDM responder) │ +└──────────────┬───────────────────────────────────────┘ + │ wait_for_messages() → TargetMessage + ▼ +┌─ MCTP Server Process ────────────────────────────────┐ +│ │ +│ Phase 1 (always): │ +│ receiver.decode() → raw MCTP packet │ +│ server_cell.borrow_mut().inbound(pkt) │ +│ └─ Router reassembles fragments │ +│ │ +│ Phase 2 (requester FSM step): │ +│ match req_state { │ +│ SendVersion → generate_get_version │ +│ ctx.requester_send_request() │ +│ → I2cSender → wire │ +│ state = AwaitVersion │ +│ AwaitVersion → ctx.requester_process_message() │ +│ Ok → state = SendCaps │ +│ Err → stay (TimedOut == normal) │ +│ … SendCaps / AwaitCaps / SendAlgs / AwaitAlgs │ +│ Done → log success, transition to Idle │ +│ } │ +│ │ +│ All SPDM transport I/O goes through │ +│ DirectMctpClient ↔ server_cell (no IPC). │ +└──────────────────────────────────────────────────────┘ +``` + +Same `RefCell<Server>` shared-ownership trick as the responder +([direct_client.rs](src/direct_client.rs)). + +--- + +## 4. Compilation Switches + +### 4.1 Cargo features (services/mctp/server/Cargo.toml) + +Current: + +```toml +default = ["i2c-polling", "direct-client"] +i2c-polling = [] # polling I2C loop (no WG/IRQ) +direct-client = [] # expose DirectMctpClient +``` + +Add one new feature: + +```toml +# Build the in-process SPDM requester inside the polling loop. +# Mutually exclusive with `in-process-responder`. +# Depends on `direct-client` (the in-process MctpClient impl). +in-process-requester = ["direct-client"] +``` + +Rename the existing responder role to a feature that makes the two roles +symmetric and mutually exclusive: + +```toml +# Build the in-process SPDM responder inside the polling loop. +# Mutually exclusive with `in-process-requester`. +in-process-responder = ["direct-client"] +``` + +Keep `direct-client` as the lower-level capability (exposes +`DirectMctpClient`). Either role feature implies it. + +Backwards compatibility: keep `direct-client` alone as a valid build (library +only; no SPDM role instantiated in main), and treat the old +`i2c-polling + direct-client` combination as equivalent to +`i2c-polling + in-process-responder` via a transitional alias in `Cargo.toml`: + +```toml +# Transitional: prior builds selected the responder via bare `direct-client`. +# New builds should name the role explicitly. +``` + +The Bazel `rust_app` target keeps `direct-client` until call sites migrate to +the role-named feature. + +### 4.2 Build-time guard + +A `compile_error!` in [src/main.rs](src/main.rs) rejects enabling both roles: + +```rust +#[cfg(all(feature = "in-process-requester", feature = "in-process-responder"))] +compile_error!( + "features `in-process-requester` and `in-process-responder` are mutually exclusive" +); +``` + +### 4.3 Bazel `rust_app` target + +New target `mctp_server_requester` in +[services/mctp/server/BUILD.bazel](services/mctp/server/BUILD.bazel), parallel +to the existing `mctp_server` target: + +```starlark +rust_app( + name = "mctp_server_requester", + codegen_crate_name = "app_mctp_server", # same handle::MCTP/I2C bindings + srcs = ["src/main.rs"], + crate_features = [ + "i2c-polling", + "in-process-requester", + ], + edition = "2024", + system_config = "//target/ast1060-evb/mctp:system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + # … identical deps to mctp_server … + ], +) +``` + +The existing `mctp_server` target flips to +`crate_features = ["i2c-polling", "in-process-responder"]` once downstream +system images migrate. + +### 4.4 System configuration + +No new kernel objects are required. The in-process requester reuses exactly +the same `system.json5` process definition as the in-process responder +([target/ast1060-evb/mctp/system.json5](../../target/ast1060-evb/mctp/system.json5)): +`MCTP` channel_handler (still unused in polling+in-process mode), `I2C` +channel_initiator, `WG` wait_group (unused in polling mode). + +--- + +## 5. Module Layout + +All new code in [services/mctp/server/src/main.rs](src/main.rs); no new files +unless a helper grows past ~50 lines. + +``` +src/main.rs + ├─ imports (cfg-gated by role) + ├─ consts: OWN_EID, OWN_I2C_ADDR, + │ REMOTE_RESPONDER_EID (new, 42 to match spdm_requester.rs) + ├─ mctp_loop() [feature = "i2c-polling"] + │ ├─ I2C setup (unchanged) + │ ├─ Server::new → RefCell<Server> (unchanged) + │ ├─ #[cfg(in-process-responder)] responder setup (unchanged) + │ ├─ #[cfg(in-process-requester)] requester setup (new, §6) + │ └─ loop { + │ Phase 1: drain I2C → server.inbound() (unchanged) + │ Phase 2: + │ #[cfg(in-process-responder)] responder_step(..) + │ #[cfg(in-process-requester)] requester_step(&mut req_state, ..) + │ } + ├─ mctp_loop() [not(i2c-polling)] (unchanged) + └─ entry / panic_handler (unchanged) +``` + +If `requester_step` / `requester_setup` together exceed ~100 lines, move +them into a new module `src/in_process_requester.rs`, gated +`#[cfg(feature = "in-process-requester")]`, exposing: + +```rust +pub(crate) struct InProcessRequester<'a, S: Sender, const N: usize> { … } +impl<…> InProcessRequester<'_, S, N> { + pub fn new(server: &'a RefCell<Server<S, N>>, remote_eid: u8, …) -> Result<Self, Error>; + pub fn step(&mut self); // one FSM step; no-op when Done + pub fn is_done(&self) -> bool; +} +``` + +Prefer the in-line `main.rs` layout first (matches the responder pattern); +extract only if the symmetry breaks readability. + +--- + +## 6. Requester State Machine + +### 6.1 States + +```rust +#[cfg(feature = "in-process-requester")] +enum ReqState { + SendVersion, + AwaitVersion, + SendCapabilities, + AwaitCapabilities, + SendAlgorithms, + AwaitAlgorithms, + Done, // success; loop idles in Phase 2 no-op + Failed, // terminal; loop idles in Phase 2 no-op +} +``` + +Start state: `SendVersion`. + +### 6.2 Step semantics (Phase 2) + +One **step** = one Phase 2 invocation of the FSM from one loop iteration. +A step must return promptly so that Phase 1 keeps draining I2C on the next +iteration — a step never blocks waiting for a response. + +```rust +match req_state { + ReqState::SendVersion => { + msg_buf.reset(); + if generate_get_version(&mut ctx, &mut msg_buf, VersionReqPayload::new(0, 0)).is_err() + || ctx.requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID).is_err() { + req_state = ReqState::Failed; + } else { + req_state = ReqState::AwaitVersion; + } + } + ReqState::AwaitVersion => { + msg_buf.reset(); + match ctx.requester_process_message(&mut msg_buf) { + Ok(_) => req_state = ReqState::SendCapabilities, + Err(_) => { /* TimedOut == stay; protocol errors also stay but counted */ } + } + } + // … same pattern for Capabilities, Algorithms … + ReqState::Done | ReqState::Failed => { /* no-op */ } +} +``` + +### 6.3 Distinguishing "no message yet" from real protocol errors + +`requester_process_message` currently returns a single `Err(_)` value that +collapses transport `TimedOut` with genuine SPDM protocol errors. Two options: + +1. **Short-term**: Ride on `Err(_)` as "stay in state" — accept that a real + protocol error would wedge the requester in an await state forever. Add + a safety-net **retry budget** per await state (e.g. 10,000 steps) after + which `req_state = ReqState::Failed`. + +2. **Proper fix**: Plumb `TransportError` through `requester_process_message` + so callers can discriminate. Preferred, but touches spdm-lib — track as + a follow-up. + +Ship option 1; open follow-up for option 2. + +### 6.4 Retry / timeout budget + +Per await state: + +```rust +const AWAIT_STEP_BUDGET: u32 = 10_000; +let mut await_steps: u32 = 0; +// on each Err in an Await* state: +await_steps = await_steps.wrapping_add(1); +if await_steps >= AWAIT_STEP_BUDGET { + req_state = ReqState::Failed; +} +// reset to 0 on state transition +``` + +10k steps at the measured steady-state poll rate gives an order-of-magnitude +wall-clock ceiling; the exact number is tunable once we measure. + +### 6.5 Interleaving constraints + +The two-phase loop's interleaving of I2C draining and SPDM FSM steps is the +mechanism that makes the design work — without Phase 1 pumping inbound +fragments into the router, Phase 2's `requester_process_message` would +never see an assembled response. Three properties must hold for this to +be safe and responsive: + +1. **Latency tax per step.** Each loop iteration runs Phase 1 (one + `wait_for_messages` call) before Phase 2 (FSM step). If the response is + already assembled in the router, the FSM still waits out a Phase 1 poll + cycle before consuming it — one I2C poll-budget duration of added + latency per step. Acceptable for VCA at wire speed; revisit if + attestation flows grow enough steps to care. + +2. **Multi-fragment responses.** `wait_for_messages` reads at most one + frame per call (`msgs: [TargetMessage; 1]`). A response that spans N + MCTP fragments needs N Phase-1 iterations before Phase 2 can succeed, + so `AWAIT_STEP_BUDGET` (§6.4) must cover + `N × (I2C poll period)` for the largest expected message. The 10,000 + default is generous for VCA; recheck when GET_CERTIFICATE lands. + +3. **`wait_for_messages` must be non-blocking.** The design relies on + `wait_for_messages(..., None)` returning `Err(TimedOut)` when idle + rather than blocking forever; otherwise Phase 2 never runs. The + responder loop already depends on this — see the comment at + [src/main.rs:390-393](src/main.rs#L390-L393). + +Non-issues (explicitly enumerated so future readers don't reopen them): + +- **Cross-flow routing.** The MCTP router dispatches by handle; unrelated + inbound MCTP traffic cannot land in the requester's `req_handle` queue. +- **RefCell borrow conflicts.** Phase 1's `server.borrow_mut()` finishes + before Phase 2 starts; the borrows never overlap. +- **Dropped frames during send.** `requester_send_request` blocks on IPC + to the I2C server, but slave-mode receives are buffered in the I2C + server process and picked up on the next `wait_for_messages`. + +--- + +## 7. Initialization Sequence (requester mode) + +Executed once before the loop, between I2C setup and the `loop { }`. Mirrors +responder setup at +[src/main.rs:182-299](src/main.rs#L182-L299); the only functional differences +are marked **[R]**. + +``` +1. DirectMctpClient::new(&server) [same] +2. MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID) [R] +3. transport.init_sequence() [R: allocates req handle, + not listener] +4. MockCertStore / MockHash×3 / MockRng / MockEvidence [same] +5. DemoPeerCertStore (needed for future GET_DIGESTS/CERTIFICATE) [R] +6. CapabilityFlags: cert_cap=1, chal_cap=1, meas_cap=0, chunk_cap=1 [R] + (no meas_fresh_cap; meas_cap=0) +7. DeviceCapabilities { include_supported_algorithms: false } [R] + (see spdm_requester.rs comment: V1.3 param1 bit 2 + is rejected by current responder) +8. LocalDeviceAlgorithms (identical to responder) [same] +9. SpdmContext::new(..., peer_cert_store=Some(&mut demo_peer), ...) [R] +10. MessageBuf (reused across FSM states) [same] +11. req_state = ReqState::SendVersion [R] +``` + +Constants borrowed verbatim from +[target/ast1060-evb/spdm-req-resp-test/spdm_requester.rs](../../target/ast1060-evb/spdm-req-resp-test/spdm_requester.rs) +so behavior matches the existing external requester app bit-for-bit. + +--- + +## 8. Observability + +Mirror the responder’s fault-isolation counters. All `u32`, log on first +event and every Nth after to avoid flooding: + +| Counter | Increment on | Log cadence | +|---|---|---| +| `i2c_pkt` | successful `receiver.decode` | already exists | +| `inbound_err` | `server.inbound()` error | already exists | +| `decode_err` | `receiver.decode()` error | already exists | +| `i2c_recv_err` | `wait_for_messages` non-timeout error | already exists | +| `idle_polls` | `wait_for_messages` timeout | already exists | +| `req_state_entered` | each ReqState transition | log every transition | +| `req_send_ok` | successful requester_send_request | every event | +| `req_send_err` | failed requester_send_request | every event (terminal) | +| `req_recv_ok` | successful requester_process_message | every event | +| `req_recv_pending` | Err in Await* state (treated as TimedOut) | first + every 256th | +| `req_recv_err_terminal` | state transitioned to Failed | every event | + +A `ReqState::Done` transition logs: + +``` +SPDM VCA completed: version/caps/algs OK in N steps, elapsed ~M idle_polls +``` + +after which the loop continues stepping (Phase 1 still drains I2C so later +inbound traffic is handled; Phase 2 is a no-op). + +--- + +## 9. Open Questions / Future Work + +1. **Blocking DirectMctpClient variant (Shape B)**: add an + `I2cPumpingDirectMctpClient` that holds `&I2cClient + &MctpI2cReceiver + + &RefCell<Server>` and, inside `recv`, drains I2C until a message is + assembled (or a deadline expires). With that, `spdm_requester_test()` from + the existing app could be reused almost verbatim and the external FSM + disappears. Requires splitting current Server RefCell borrows or wrapping + in a struct to satisfy the borrow checker. + +2. **TransportError discrimination**: plumb the underlying `TransportError` + through `requester_process_message` so we can distinguish + `NoRequestInFlight` / `ReceiveError(TimedOut)` from protocol errors + without a step budget. + +3. **Triggering**: when does the requester start? Currently the spec assumes + "immediately at boot, single-shot VCA". A real use will want external + triggers — an IPC operation (`start_spdm`, target EID as arg), a GPIO, + or a policy. Out of scope here; add when there is a caller. + +4. **Mutual exclusion of roles**: we chose mutually-exclusive features. If + we later want a **dual role** (a device that both attests others and is + attested), we need two SpdmContexts sharing one Server — tractable but + not covered by this spec. + +5. **Mock platform sharing**: `target/ast1060-evb/spdm-req-resp-test/mock_platform` + is already a dep of `mctp_server`. Reuse as-is; if the cert material + diverges between requester and responder builds, consider a + `mock_platform_requester` / `mock_platform_responder` split. + +--- + +## 10. Summary of Compilation Switch Matrix (post-change) + +| Features | I2C IRQ | IPC served | In-process role | +|---|---|---|---| +| _(none)_ | WG + IRQ | MCTP clients | none | +| `i2c-polling` | polling | none | none | +| `i2c-polling` + `in-process-responder` | polling | none | **responder** | +| `i2c-polling` + `in-process-requester` | polling | none | **requester** | +| any combination setting both roles | — | — | `compile_error!` | + +Default remains `i2c-polling + in-process-responder` (current behavior), +until system images choose otherwise.
diff --git a/services/mctp/server/IMPLEMENTATION_PLAN.md b/services/mctp/server/IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..ba935bc --- /dev/null +++ b/services/mctp/server/IMPLEMENTATION_PLAN.md
@@ -0,0 +1,536 @@ +# In-Process SPDM Requester — Implementation Plan + +Companion to [DESIGN_IN_PROCESS_REQUESTER.md](DESIGN_IN_PROCESS_REQUESTER.md). +Breaks the design into landable phases, each independently compileable and +testable so nothing goes dark on `main` between merges. + +## Status + +| Phase | Status | Notes | +|---|---|---| +| 0 — baseline build + assumption check | ✅ done | Required two pre-Phase-1 fixes (see Phase 0 Resolution Log) | +| 1 — feature flag restructuring | ✅ done | `direct-client` is now the shared capability; role features `in-process-responder` / `in-process-requester` pick the FSM. Mutual-exclusion `compile_error!` in `main.rs`. | +| 2 — compileable requester stub | ✅ done | Forced creation of a second system_image package `//target/ast1060-evb/mctp-requester` so the standalone `rust_app` target actually compiles (standalone `rust_app` hits a `pw_kernel/userspace` platform-constraint error without a system image). | +| 3 — FSM happy path | ✅ done | Eight-state VCA FSM, one step per polling-loop iteration. | +| 4 — safety net + counters | ✅ done | `AWAIT_STEP_BUDGET = 10_000`, `await_try!` macro, counters `req_send_ok` / `req_recv_ok` / `req_recv_pending`. | +| 5 — Bazel target + system image | ✅ done (merged into Phase 2) | See Phase 2 note above. | +| 6 — two-board integration test | ⏸ blocked | No on-target hardware available in this environment. See IMPLEMENTATION_PLAN.md §A for measurements to collect when hardware is accessible. | +| 7 — docs + follow-up TODOs | ✅ done | [INITIALIZATION.md](INITIALIZATION.md) updated with requester glossary + setup steps; follow-up design refs annotated at the FSM in `main.rs`. | + +Build verification at end of Phase 7: + +``` +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp # responder — clean +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester # requester — clean +``` + +Legend: +- **Goal** — what this phase must achieve. +- **Deliverables** — concrete artifacts produced. +- **Files** — edited / added in this phase. +- **Exit criteria** — what proves the phase is done. +- **Risks / rollback** — what can go wrong; how to back out. + +--- + +## How To Build Which Role + +Role (requester vs. responder) is selected at **build time**, not at +runtime. Same `src/main.rs` source, two binaries, chosen by which Bazel +**system-image** target you build. Standalone `rust_app` targets do not +build in isolation — `pw_kernel/userspace` requires a `target_codegen` → +`kernel_config` chain that only exists inside a `system_image`. + +**Developer commands:** + +```bash +# Responder image (default) +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp + +# Requester image +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester +``` + +Running any test or integration probe uses the same prefix +(`bazel test --config=k_ast1060_evb <target>`). + +**Selection chain** — each layer picks the next: + +``` + System image → rust_app label → Cargo features → #[cfg] in main.rs + ──────────────────────────────────────────────── ───────────────────────────────────────────── ─────────────────────────────────────────────────────── ───────────────────────── + //target/ast1060-evb/mctp:mctp → //services/mctp/server:mctp_server → ["i2c-polling", "direct-client", "in-process-responder"] → responder setup + step + //target/ast1060-evb/mctp-requester:mctp_requester → //services/mctp/server:mctp_server_requester → ["i2c-polling", "direct-client", "in-process-requester"] → requester setup + FSM +``` + +Each `rust_app` has its own `codegen_crate_name` — `app_mctp_server` for +the responder, `app_mctp_server_requester` for the requester — because two +`rust_app` rules in the same Bazel package cannot share the codegen name. +`src/main.rs` picks the right one via a cfg-gated `use`. + +**Guards:** + +- Both roles at once → rejected by `compile_error!` at the top of `main.rs`. +- Neither role + `i2c-polling` → polling loop compiles with no SPDM in-process + (still valid; drains I2C only). +- No `i2c-polling` → the notification (WG + IRQ) loop compiles; role features + are ignored because `in-process-*` code is also gated on `i2c-polling`. +- `direct-client` is listed explicitly in both `crate_features` sets; + Bazel's `rust_binary` does not resolve Cargo-level feature implications + (`in-process-* = ["direct-client"]`), so the transitive dep must be + named directly. + +**What flashes:** each system image binds one of the two `rust_app` labels +via its `apps = [...]` list, together with `i2c_server`. The two images +are mutually exclusive — flash one or the other, not both. + +**Direct `cargo build` is not a supported path** for the binary — the +package sets `autobins = false` and `main.rs` is a Pigweed binary built +only through Bazel. The Cargo features still matter because Bazel passes +them through; they're just never consumed by a bare `cargo` invocation +of `main.rs`. + +--- + +## Phase 0 — Baseline & Assumption Verification + +**Goal**: confirm the starting source tree builds clean before any new +code is written. Source-level only — no on-target execution is required +or assumed at this stage. + +**Deliverables**: +- Successful Bazel build of + `//target/ast1060-evb/mctp:mctp` (responder system image) at current + `ocp-emea-demo-spdm-spdm-req-resp-anthony-i2c-polling` HEAD. +- Source-level re-read of the existing polling loop at + [src/main.rs:322-445](src/main.rs#L322-L445) to confirm the interleaving + assumptions in design §6.5 still match the code (in particular, that + `wait_for_messages(..., None)` paired with the `e.is_timeout()` arm at + [src/main.rs:390](src/main.rs#L390) matches constraint §6.5.3). +- **Skipped:** standalone build of + `//target/ast1060-evb/spdm-req-resp-test:spdm_requester_app`. The app is + marked `# DISABLED` in the sibling `system_image` and fails platform + analysis when built alone. The reference constants in + [spdm_requester.rs](../../target/ast1060-evb/spdm-req-resp-test/spdm_requester.rs) + are consumed as source, not as a binary, so skipping this build does + not block later phases. + +**Files**: none modified **initially**. See Phase 0 Resolution Log below. + +**Exit criteria**: the responder system image builds clean; the §6.5 +cross-check is recorded as "verified against HEAD" in the commit +message of Phase 1. + +**Risks / rollback**: none — read-only phase by default; any unblocker +applied here gets its own commit (see log below). + +### Phase 0 Resolution Log + +- **Blocker discovered**: at branch HEAD, commit `023e75e "Update mctp-lib + (to hotfix branch)"` changed `mctp_lib::Router::inbound` to return + `Result<Option<AppCookie>, MctpError>`, but the + [services/mctp/server/src/server.rs:267-271](src/server.rs#L267-L271) + wrapper `Server::inbound` still declared `Result<(), MctpError>`. The + error surfaces as E0308 and fails *every* Bazel target that depends on + `mctp_server_lib` (responder system image, req-resp test image, + etc.). +- **Verification that the break is isolated**: a worktree at + `d4719a0 "Chasing down an MCTP packet issue."` (the commit immediately + preceding the mctp-lib hotfix) built `//target/ast1060-evb/mctp:mctp` + clean. So the break is entirely within `023e75e`. +- **Resolution — Fix A** (discard the new cookie): + ```rust + pub fn inbound(&mut self, pkt: &[u8]) -> Result<(), MctpError> { + self.stack + .inbound(pkt) + .map(|_| ()) + .map_err(mctp_error_to_server_error) + } + ``` + Chosen over Fix B (propagate `Option<AppCookie>` to callers) because: + no caller reads the return value today, the propagation case is cheap + to add later if Shape-B from design §9.1 ever lands, and keeping the + unblocker orthogonal to the requester work eases `git blame`. + Landed as its own small commit before any Phase 1 work. + +- **Second blocker** (surfaced after Fix A unblocked rustc past the first + error): the same mctp-lib hotfix changed + `MctpI2cReceiver::decode`'s return type from `(&[u8], u8)` to + `(&[u8], MctpI2cHeader)` — a struct + `{ dest: u8, source: u8, byte_count: usize }` — but + [src/main.rs:350-361](src/main.rs#L350-L361) still treated the second + tuple element as a bare `u8` source address. `as u32` on a struct + triggers E0605. +- **Resolution — Fix A′** (match the new type, minimum diff): rename + the binding from `src_addr` to `hdr` and read the source address + field explicitly: `hdr.source as u32`. Landed in the same + "pre-Phase-1 unblocker" commit as Fix A. + +**Deferred to on-target bringup**: measuring actual idle-poll rate, +`wait_for_messages` latency, and worst-case fragment count is out of +scope for Phase 0. `AWAIT_STEP_BUDGET` will ship with a generous +compile-time default (§6.4) and be tuned when a runtime becomes +available — see Appendix §A. + +--- + +## Phase 1 — Feature Flag Restructuring + +**Goal**: rename the responder role feature and add the requester role +feature, without changing behavior. Pure refactor. + +**Deliverables**: +- `services/mctp/server/Cargo.toml`: + - Add `in-process-responder = ["direct-client"]`. + - Add `in-process-requester = ["direct-client"]`. + - Update `default` to `["i2c-polling", "in-process-responder"]`. + - Keep `direct-client` as-is (library-exposed capability). +- `services/mctp/server/src/main.rs`: + - Add at top: + ```rust + #[cfg(all(feature = "in-process-requester", feature = "in-process-responder"))] + compile_error!("features `in-process-requester` and `in-process-responder` are mutually exclusive"); + ``` + - Replace every `#[cfg(feature = "direct-client")]` inside the polling + loop and its setup block with + `#[cfg(feature = "in-process-responder")]`. + - Leave the `mod direct_client;` export and `DirectMctpClient` untouched + — those belong to the `direct-client` feature, which still exists. +- `services/mctp/server/BUILD.bazel`: + - `mctp_server` target: replace + `crate_features = ["i2c-polling", "direct-client"]` with + `["i2c-polling", "in-process-responder"]`. + - Keep `rust_library` `mctp_server_lib` with `crate_features = ["direct-client"]`. + +**Files**: `Cargo.toml`, `src/main.rs`, `BUILD.bazel`. + +**Exit criteria**: +- `bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp` + succeeds (the standalone `rust_app` target cannot be built in isolation + because `pw_kernel/userspace` needs a system-image codegen context). +- Built binary is byte-identical in behavior to Phase 0 baseline (responder + loop still runs; log lines at startup unchanged). +- Attempting to build with both features enabled emits the + `compile_error!`. + +**Risks / rollback**: +- *Risk*: downstream Bazel targets still set `direct-client` directly and + silently lose the responder. *Mitigation*: grep the repo for + `direct-client` in `crate_features`; the only consumer is + `services/mctp/server/BUILD.bazel`. +- *Rollback*: revert the single refactor commit; no logic changed. + +--- + +## Phase 2 — Compileable Requester Stub + +**Goal**: add the requester setup skeleton behind +`in-process-requester` as a no-op Phase 2 step. Proves feature gating, +imports, and the init sequence compile — no FSM logic yet. + +**Deliverables**: +- `services/mctp/server/src/main.rs`: + - Add constant `const REMOTE_RESPONDER_EID: u8 = 42;` (module-scope, not + cfg-gated — a single `#[allow(dead_code)]` keeps it clean when the + feature is off). + - Add imports gated on + `#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))]`: + mock platform types, `MctpSpdmTransport`, `SpdmContext`, VCA + `generate_*` functions, `DemoPeerCertStore`. + - Inside `mctp_loop()` under the same cfg, add the full init sequence + from §7 of the design doc: + 1. `DirectMctpClient::new(&server)` + 2. `MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID)` + 3. `transport.init_sequence()` with error log + return + 4. Mock platform instances (cert_store, hash×3, rng, evidence, + peer_cert_store) + 5. `CapabilityFlags` / `DeviceCapabilities` matching + [spdm_requester.rs](../../target/ast1060-evb/spdm-req-resp-test/spdm_requester.rs) + 6. `LocalDeviceAlgorithms` (factor into a helper to avoid duplicating + the responder's inline block) + 7. `SpdmContext::new(..., Some(&mut peer_cert_store), ...)` + 8. `MessageBuf` on stack buffer + 9. `let mut req_state = ReqState::SendVersion;` — **but** Phase 2 + step body is still just + `#[allow(unused_variables)] let _ = &mut req_state;` with a log + `"requester FSM stub: state=..."` every 0xfff loops. + - Add the `ReqState` enum (full variant set — §6.1) even though only + one variant is used, so Phase 3 just fills in match arms. + +**Files**: `src/main.rs`. + +**Exit criteria**: +- `bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester` + succeeds. (Phase 2 verification required pulling Phase 5's + system-image package forward — a standalone `rust_app` target is not + buildable on its own in this tree; see `mctp-requester/` for the + system image created here.) +- The responder build is unaffected. +- Running the requester build on-target logs the init sequence, then + idle-poll messages with the stub state — no SPDM traffic emitted yet. + +**Risks / rollback**: +- *Risk*: Rust borrow checker rejects `SpdmContext::new` arg layout + because `transport` and `server` share a lifetime. *Mitigation*: mirror + the responder's exact let-binding order, which is already known to + compile. +- *Risk*: `DemoPeerCertStore` import path differs from responder + (responder uses `None`). *Mitigation*: path already resolved in + `spdm_requester.rs` — copy. +- *Rollback*: revert commit; responder build unaffected. + +--- + +## Phase 3 — FSM Implementation (Happy Path) + +**Goal**: wire the six Send/Await states so a full VCA flow completes on +the wire. + +**Deliverables**: +- Replace the Phase-2 stub body with the match from §6.2 of the design + doc. Each Send state: + 1. `msg_buf.reset()` + 2. `generate_<step>(...)` + 3. `ctx.requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID)` + 4. Advance to matching `Await*` state. +- Each Await state: + 1. `msg_buf.reset()` + 2. `ctx.requester_process_message(&mut msg_buf)` + 3. `Ok(_)` → advance; `Err(_)` → stay (counted, not yet budgeted). +- On entry to `ReqState::Done`, log once: + `"SPDM VCA completed: version/caps/algs OK"`. +- `Done` and `Failed` states become no-ops in Phase 2 (Phase 1 keeps + draining I2C unconditionally, so late inbound traffic is still handled). + +**Files**: `src/main.rs`. + +**Exit criteria**: +- End-to-end test on AST1060-EVB against the existing + `spdm_responder` app running on a second device (or the same device + with the responder built separately) produces three successful + request/response pairs. +- Protocol-analyzer capture (or I2C frame logs already present) shows + GET_VERSION / GET_CAPABILITIES / NEGOTIATE_ALGORITHMS emitted in order + with matching response consumption. +- `spdm_err` / equivalent pending-counter stays bounded (not monotonically + growing past expected poll-while-waiting count). + +**Risks / rollback**: +- *Risk*: a protocol error wedges the FSM in an Await state forever + (design §6.3, option 1 not yet implemented). *Mitigation*: Phase 4 + adds the step budget; until then, a power-cycle is acceptable for + bringup. +- *Risk*: `include_supported_algorithms = false` / other capability + fields diverge between the in-process requester and the responder's + expectations. *Mitigation*: copy verbatim from `spdm_requester.rs`; + already known to interoperate with `spdm_responder.rs`. +- *Rollback*: revert to Phase 2 stub. + +--- + +## Phase 4 — Safety Net & Observability + +**Goal**: bound failure latency and give on-target debugging the same +counter set as the responder. + +**Deliverables**: +- `AWAIT_STEP_BUDGET` const + `await_steps` counter per design §6.4, + reset on every state transition, transitions FSM to `Failed` on + exhaustion with an error log naming the state that timed out. +- Counters from design §8, each `u32` wrapping, logged on first event and + every 256th: + - `req_send_ok`, `req_send_err`, `req_recv_ok`, `req_recv_pending`, + `req_recv_err_terminal`. +- State-transition log (one line per transition, not rate-limited — low + volume). +- `Done` transition log augmented with + `"completed in {N} steps, ~{idle_polls} idle polls"`. + +**Files**: `src/main.rs`. + +**Exit criteria**: +- Forcing a fault (e.g. responder offline) causes `Failed` transition + within the budget and emits the named-state error; device does not + hang. +- Counters visible in logs match observed behavior (send counts == 3 on + success path; pending spikes and settles at each Await entry). + +**Risks / rollback**: +- *Risk*: step budget too tight → spurious failures under a legitimately + multi-fragment response (§6.5.2). *Mitigation*: ship with a deliberately + generous default (`AWAIT_STEP_BUDGET = 10_000`) and defer tightening + until on-target measurements exist (see Appendix §A). Erring high + costs only wall-clock on genuine failures; erring low wedges the + happy path. +- *Rollback*: revert budget block, keep counters (useful regardless). + +--- + +## Phase 5 — Bazel Target & System Image + +**Goal**: make the requester role selectable as a first-class Bazel build +without touching the responder's target. + +**Deliverables**: +- New target in `services/mctp/server/BUILD.bazel`: + ```starlark + rust_app( + name = "mctp_server_requester", + codegen_crate_name = "app_mctp_server", + srcs = ["src/main.rs"], + crate_features = ["i2c-polling", "in-process-requester"], + edition = "2024", + system_config = "//target/ast1060-evb/mctp:system_config", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ # identical to mctp_server + ":mctp_server_lib", + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/mctp/api:mctp_api", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "//services/spdm/transport-mctp:spdm_transport_mctp", + "//target/ast1060-evb/spdm-req-resp-test:mock_platform", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@oot_crates_no_std//:spdm-lib", + ], + ) + ``` +- Choose whether the default system image + ([target/ast1060-evb/mctp/BUILD.bazel](../../target/ast1060-evb/mctp/BUILD.bazel)) + wires the requester or the responder. Do **not** change the default in + this phase — add a second system image build instead (e.g. + `system_requester`) so both can be flashed side by side during + bringup. + +**Files actually landed**: +- `services/mctp/server/BUILD.bazel` — second `rust_app(name = "mctp_server_requester", codegen_crate_name = "app_mctp_server_requester", ...)` target. +- `target/ast1060-evb/mctp-requester/BUILD.bazel` — new sibling package with its own `system_image`, `target_codegen`, `target_linker_script`, `rust_binary(name="target",...)`, and `uart_boot_image`. +- `target/ast1060-evb/mctp-requester/system.json5` — copy of the mctp image's config with the MCTP app renamed to `mctp_server_requester`. +- `target/ast1060-evb/mctp-requester/target.rs` — copy of the kernel entry shim. + +**Exit criteria**: +- Both system images build clean from scratch: + ``` + bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp + bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester + ``` +- Responder system image continues to boot unchanged. +- Requester system image boots and reaches the FSM entry log within + the expected startup window (deferred to on-target bringup — Phase 6). + +**Risks / rollback**: +- *Risk*: `codegen_crate_name` collision when both `rust_app` targets + live in the same Bazel package. *Resolved*: the two targets use + distinct codegen crate names (`app_mctp_server`, + `app_mctp_server_requester`) and `src/main.rs` selects between them + with a cfg-gated `use`. +- *Risk*: platform-constraint analysis error when building + `rust_app` standalone. *Resolved*: the `rust_app` target must be + consumed by a `system_image`, never built directly. +- *Rollback*: delete the new system-image package; responder unaffected. + +--- + +## Phase 6 — Integration Test + +**Goal**: a repeatable test that exercises the in-process requester +against the in-process responder on the bench. + +**Deliverables**: +- Two-board test config: board A flashed with + `mctp_server_requester` system image, board B flashed with + `mctp_server` (responder). Both use current `OWN_EID` / `OWN_I2C_ADDR` + constants, with board A targeting board B at + `REMOTE_RESPONDER_EID = 42` — board B must be re-configured to EID + 42 (its current `OWN_EID = 0x08`; this requires a new const + `RESPONDER_OWN_EID` on the responder image, or swapping A↔B roles). +- Documented test recipe in `README.md` (new section) or a dedicated + `tests/ON_TARGET_README.md`: flash steps, expected log lines, expected + duration-to-`Done`. +- Automated log-checking script is out of scope; bringup-level manual + verification is sufficient. + +**Files**: docs only, plus any const bump in main.rs if we change +`OWN_EID` on the responder side. + +**Exit criteria**: +- Manual test produces a `SPDM VCA completed` line on the requester side + and three matching processed-request lines on the responder side. + +**Risks / rollback**: +- *Risk*: EID mismatch between the two images. *Mitigation*: the spec + prescribes explicit named constants for both ends; document the + pairing in the test recipe. +- *Rollback*: none needed — docs only. + +--- + +## Phase 7 — Cleanup & Follow-Up Tickets + +**Goal**: close loose ends the design doc called out and make sure +nothing is left under-documented. + +**Deliverables**: +- File follow-up issues (or local TODO comments tagged with a tracking + tag) for: + - Design §6.3 option 2 — propagate `TransportError` out of + `requester_process_message` so TimedOut can be discriminated. + - Design §9.1 — pumping-`DirectMctpClient` variant (Shape B). + - Design §9.3 — external trigger mechanism (IPC / GPIO / policy). + - Design §9.4 — dual-role configuration. +- Update [INITIALIZATION.md](INITIALIZATION.md) with a new "Polling mode + (requester)" subsection mirroring the responder's, so the glossary + stays truthful. +- Update the build-modes table in both `main.rs` module docs and + `INITIALIZATION.md` to list the new requester row. + +**Files**: `INITIALIZATION.md`, `src/main.rs` (module-level doc comment +only). + +**Exit criteria**: follow-up issues filed; docs reviewed; no +undocumented feature flag. + +**Risks / rollback**: none — docs and issue filing. + +--- + +## Phase Dependency Graph + +``` +Phase 0 ──► Phase 1 ──► Phase 2 ──► Phase 3 ──► Phase 4 ──► Phase 5 ──► Phase 6 ──► Phase 7 + │ ▲ + └────────────────────────────────────┘ + (Phase 5 can start after Phase 2 in parallel + with Phase 3/4 if a separate hand is available) +``` + +Phases 1 and 2 are small (half-day each). Phase 3 is the bulk of the +work — expect a full day including on-target debug. Phases 4–6 are +each half-day. Phase 7 is a loose-ends sweep. + +--- + +## A. Deferred Measurements (on-target bringup) + +These values are **not** required to land Phases 0–5. Capture them once +hardware execution is available (Phase 6 onward) and revisit §6.4 of the +design doc if any value contradicts the back-of-envelope `10_000` +default. + +| Metric | Measured value | Source | +|---|---|---| +| `idle_polls` increment rate (per wall-clock second) | _deferred_ | responder log, counted over a fixed window | +| `wait_for_messages` average return latency when idle | _deferred_ | derived from idle_polls rate | +| Largest expected VCA message fragment count | _deferred_ | `receiver.decode` SOM/EOM logs | +| Observed peak `await_steps` on happy path | _deferred_ | requester log at `Done` transition | +| Tuned `AWAIT_STEP_BUDGET` (≥ ~3× observed peak) | _deferred_ | computed | + +Until these exist, the design ships with the default and relies on the +Phase-4 state-timeout log line to surface a bad choice loudly rather +than silently.
diff --git a/services/mctp/server/INITIALIZATION.md b/services/mctp/server/INITIALIZATION.md index fb6fe1e..7a1cd31 100644 --- a/services/mctp/server/INITIALIZATION.md +++ b/services/mctp/server/INITIALIZATION.md
@@ -6,14 +6,24 @@ ## Build Modes -The server compiles into one of two mutually exclusive modes, selected by Cargo -feature flags set in the Bazel `rust_app` target. +The server compiles into one of several mutually exclusive modes, selected by +Cargo feature flags set in the Bazel `rust_app` target. `in-process-requester` +and `in-process-responder` are mutually exclusive and the binary emits a +`compile_error!` if both are enabled at once. -| Feature flags | Mode | IPC served? | SPDM in-process? | +| Feature flags | Mode | IPC served? | SPDM role in-process | |---|---|---|---| -| _(none)_ | Notification | Yes | No | -| `i2c-polling` | Polling | No | No | -| `i2c-polling` + `direct-client` | Polling + SPDM | No | Yes | +| _(none)_ | Notification | Yes | none | +| `i2c-polling` | Polling | No | none | +| `i2c-polling` + `direct-client` + `in-process-responder` | Polling + SPDM | No | responder | +| `i2c-polling` + `direct-client` + `in-process-requester` | Polling + SPDM | No | requester | + +Build targets: + +``` +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp:mctp # responder image +bazel build --config=k_ast1060_evb //target/ast1060-evb/mctp-requester:mctp_requester # requester image +``` --- @@ -70,7 +80,7 @@ → MCTP router, exclusively owned. Stored in `server_plain`. -── SPDM setup (direct-client only) ────────────────────────────────────────── +── SPDM setup (in-process-responder) ────────────────────────────────────── 9. DirectMctpClient::new(&server_cell) → In-process MctpClient. Calls server_cell methods directly; @@ -116,6 +126,58 @@ Both declared outside the loop to avoid borrow-checker conflict. ``` +── SPDM setup (in-process-requester) ────────────────────────────────────── + +Mirrors steps 9–18 of the responder path; only the differences are shown. + +``` +9. DirectMctpClient::new(&server_cell) [identical to responder] + +10. MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID) + → SPDM transport layer in requester mode (targets EID 42). + `REMOTE_RESPONDER_EID = 42` matches `spdm_requester.rs`. + +11. transport.init_sequence() + → Calls server.req(REMOTE_RESPONDER_EID) in-process, reserving a + request handle in the router. + FATAL if the router's request table is full. + +12. MockCertStore / MockHash×3 / MockRng / MockEvidence [identical] +12a. DemoPeerCertStore::default() [requester-only] + → Provides the peer's certificate chain to SPDM; required even if + only VCA is exercised. Stored in `peer_cert_store`. + +13. CapabilityFlags { cert_cap=1, chal_cap=1, meas_cap=0, chunk_cap=1 } + → No `meas_fresh_cap` (requester does not publish measurements). + +14. DeviceCapabilities { include_supported_algorithms: false } + → V1.3 param1 bit 2 is rejected by the responder as reserved; leave + it unset so algorithm negotiation goes through NEGOTIATE_ALGORITHMS. + +15. SUPPORTED_VERSIONS = [V12, V13] [identical] +16. LocalDeviceAlgorithms (SHA-384, ECDSA-P384) [identical] + +17. SpdmContext::new(..., Some(&mut peer_cert_store), ...) + → Requester passes `Some(peer_cert_store)` (responder passes `None`). + +18. spdm_buf + msg_buf [identical] + +19. req_state = ReqState::SendVersion [requester-only] + → Initial FSM state. The Phase-2 step below walks through + SendVersion → AwaitVersion → SendCapabilities → AwaitCapabilities → + SendAlgorithms → AwaitAlgorithms → Done (or → Failed on error / + AWAIT_STEP_BUDGET exhaustion). +``` + +The polling loop then runs two phases per iteration: + +``` +Phase 1 (always): drain I2C → server.inbound() → router reassembly +Phase 2 (responder): spdm_ctx.responder_process_message() — Err on no data +Phase 2 (requester): FSM step — advance on Ok, stay on Err, transition to + Failed if await_steps hits AWAIT_STEP_BUDGET (10_000) +``` + --- ### Notification mode (default, no `i2c-polling`) @@ -159,6 +221,8 @@ |---|---|---| | `OWN_EID` | `0x08` | MCTP Endpoint ID of this device | | `OWN_I2C_ADDR` | `0x10` | I2C 7-bit slave address of this device | +| `REMOTE_RESPONDER_EID` | `42` | MCTP EID of the peer responder (requester-only) | +| `AWAIT_STEP_BUDGET` | `10_000` | Max FSM steps per Await state before Failed (requester-only) | ### Polling mode variables @@ -206,15 +270,28 @@ | `i2c_recv_err` | `wait_for_messages` returns `Err` | 1st occurrence, then every 16th | | `decode_err` | `receiver.decode()` failure | 1st occurrence, then every 16th | | `inbound_err` | `server.inbound()` returns `Err` | 1st occurrence, then every 16th | -| `spdm_ok` | `responder_process_message` returns `Ok` | Every success (at `info` level) | -| `spdm_err` | `responder_process_message` returns `Err` | 1st occurrence, then every 256th (at `debug` level — mostly TimedOut noise) | +| `spdm_ok` | `responder_process_message` returns `Ok` (responder-only) | Every success (at `info` level) | +| `spdm_err` | `responder_process_message` returns `Err` (responder-only) | 1st occurrence, then every 256th (at `debug` level — mostly TimedOut noise) | + +### Requester FSM variables (`in-process-requester`, loop-scoped) + +| Name | Type | Purpose | +|---|---|---| +| `req_state` | `ReqState` | Current FSM state (SendVersion → AwaitAlgorithms → Done/Failed) | +| `req_send_ok` | `u32` | Successful `requester_send_request` calls (3 on happy path) | +| `req_recv_ok` | `u32` | Successful `requester_process_message` calls (3 on happy path) | +| `req_recv_pending` | `u32` | `requester_process_message` returned `Err` (mostly TimedOut) | +| `await_steps` | `u32` | Consecutive Phase-2 steps in the current Await state; reset on transition; triggers Failed at `AWAIT_STEP_BUDGET` | +| `peer_cert_store` | `DemoPeerCertStore` | Peer cert chain handed to `SpdmContext::new` (requester-only) | --- -## Handle IDs (`app_mctp_server::handle`) +## Handle IDs (`app_mctp_server::handle` or `app_mctp_server_requester::handle`) -Generated by `app_package` codegen from `system.json5`. The numeric values are -target-specific and should not be hardcoded. +Each Bazel `rust_app` target generates its own codegen crate: +`app_mctp_server` for the responder image, `app_mctp_server_requester` for the +requester image. `src/main.rs` picks the right one with a cfg-gated `use`. +The numeric values are target-specific and should not be hardcoded. | Handle | Used by | Purpose | |---|---|---|
diff --git a/services/mctp/server/src/main.rs b/services/mctp/server/src/main.rs index b6d8171..bf4bd4c 100644 --- a/services/mctp/server/src/main.rs +++ b/services/mctp/server/src/main.rs
@@ -9,11 +9,15 @@ //! //! Two mutually exclusive event loops are compiled depending on feature flags: //! -//! | Features | Mode | IPC served | SPDM in-process | +//! | Features | Mode | IPC served | SPDM role in-process | //! |---|---|---|---| -//! | _(none)_ | Notification (WaitGroup + IRQ) | Yes | No | -//! | `i2c-polling` | Polling | No | No | -//! | `i2c-polling` + `direct-client` | Polling + SPDM | No | Yes | +//! | _(none)_ | Notification (WaitGroup + IRQ) | Yes | none | +//! | `i2c-polling` | Polling | No | none | +//! | `i2c-polling` + `in-process-responder` | Polling + SPDM | No | responder | +//! | `i2c-polling` + `in-process-requester` | Polling + SPDM | No | requester (added in Phase 2+) | +//! +//! `in-process-requester` and `in-process-responder` are mutually +//! exclusive and enforced with a `compile_error!` below. //! //! # Notification Mode Architecture (default) //! @@ -39,7 +43,7 @@ //! └──────────────────────────────────┘ //! ``` //! -//! # In-Process SPDM Responder Architecture (`i2c-polling` + `direct-client`) +//! # In-Process SPDM Responder Architecture (`i2c-polling` + `in-process-responder`) //! //! When both features are enabled the SPDM responder runs inside this process. //! `DirectMctpClient` replaces the IPC channel — it calls `Server` methods @@ -86,6 +90,13 @@ #![no_main] #![no_std] +// Role-feature mutual exclusion: the in-process requester and responder +// cannot both be instantiated in the same binary. +#[cfg(all(feature = "in-process-requester", feature = "in-process-responder"))] +compile_error!( + "features `in-process-requester` and `in-process-responder` are mutually exclusive" +); + // Imports shared by both modes. use i2c_api::{BusIndex, I2cAddress, I2cTargetClient, TargetMessage}; use i2c_client::IpcI2cClient; @@ -94,9 +105,16 @@ 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). +// Each Bazel `rust_app` target generates its own handle-table crate named +// after `codegen_crate_name`. Pick the right one for this build. +#[cfg(not(feature = "in-process-requester"))] +use app_mctp_server::handle; +#[cfg(feature = "in-process-requester")] +use app_mctp_server_requester::handle; + +// Imports shared by every in-process SPDM role (requester or responder). +// Gated on `direct-client` because both role features imply it. #[cfg(feature = "i2c-polling")] use core::cell::RefCell; #[cfg(all(feature = "i2c-polling", feature = "direct-client"))] @@ -119,6 +137,18 @@ ReqBaseAsymAlg, SpdmVersion, }; +// Imports used only by the in-process SPDM requester. +#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))] +use mock_platform::DemoPeerCertStore; +#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))] +use spdm_lib::commands::algorithms::request::generate_negotiate_algorithms_request; +#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))] +use spdm_lib::commands::capabilities::request::generate_capabilities_request_local; +#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))] +use spdm_lib::commands::version::VersionReqPayload; +#[cfg(all(feature = "i2c-polling", feature = "in-process-requester"))] +use spdm_lib::commands::version::request::generate_get_version; + // 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}; @@ -134,11 +164,44 @@ const OWN_EID: u8 = 8; const OWN_I2C_ADDR: u8 = 0x10; +/// Remote EID of the SPDM responder targeted by the in-process requester. +/// Matches `spdm_requester.rs` so the two requester implementations are +/// interchangeable against the same responder image. +#[allow(dead_code)] +const REMOTE_RESPONDER_EID: u8 = 42; + +/// Max number of Phase-2 steps spent in a single `Await*` state before the +/// requester gives up and transitions to `Failed`. Generous default — see +/// design §6.4 and IMPLEMENTATION_PLAN.md Appendix §A; tune once on-target +/// measurements exist. 10k at the observed idle-poll rate gives a +/// comfortable wall-clock ceiling while still bounding genuine hangs. +#[cfg(feature = "in-process-requester")] +#[allow(dead_code)] +const AWAIT_STEP_BUDGET: u32 = 10_000; + +/// Requester FSM state (Phase 2 of the polling loop when +/// `in-process-requester` is enabled). Stepped once per loop iteration; +/// each `Await*` tick either advances on `Ok` or stays (counted) on `Err`. +#[cfg(feature = "in-process-requester")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[repr(u32)] +enum ReqState { + SendVersion = 0, + AwaitVersion = 1, + SendCapabilities = 2, + AwaitCapabilities = 3, + SendAlgorithms = 4, + AwaitAlgorithms = 5, + Done = 6, + Failed = 7, +} + // --------------------------------------------------------------------------- // 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. +// When built with the "in-process-responder" feature, an SPDM responder +// runs in-process using DirectMctpClient (no IPC channel to a separate +// process). Enable with crate_features = ["i2c-polling", +// "in-process-responder"] in Bazel. // --------------------------------------------------------------------------- #[cfg(feature = "i2c-polling")] @@ -179,13 +242,13 @@ // --------------------------------------------------------------------------- // SPDM responder setup (only when direct-client feature is enabled) // --------------------------------------------------------------------------- - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut transport = { let client = DirectMctpClient::new(&server); MctpSpdmTransport::new_responder(client) }; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] { pw_log::info!("MCTP server: registering SPDM listener (msg_type=0x05)"); if transport.init_sequence().is_err() { @@ -196,22 +259,22 @@ pw_log::info!("MCTP server: SPDM listener registered"); } - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut cert_store = MockCertStore::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut hash = MockHash::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut m1_hash = MockHash::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut l1_hash = MockHash::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut rng = MockRng::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let evidence = MockEvidence::new(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut flags = CapabilityFlags::default(); - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] { flags.set_cert_cap(1); flags.set_chal_cap(1); @@ -220,7 +283,7 @@ flags.set_chunk_cap(1); } - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let capabilities = DeviceCapabilities { ct_exponent: 0, flags, @@ -229,10 +292,10 @@ include_supported_algorithms: true, }; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] static SUPPORTED_VERSIONS: [SpdmVersion; 2] = [SpdmVersion::V12, SpdmVersion::V13]; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let algorithms = { let mut measurement_spec = MeasurementSpecification::default(); measurement_spec.set_dmtf_measurement_spec(1); @@ -270,7 +333,7 @@ } }; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut spdm_ctx = { pw_log::info!("MCTP server: creating SPDM context (v1.2+v1.3, ECDSA-P384, SHA-384)"); match SpdmContext::new( @@ -298,6 +361,142 @@ } }; + // --------------------------------------------------------------------------- + // SPDM requester setup (only when in-process-requester feature is enabled). + // Mirrors the responder block above; role-specific differences: + // • transport created with new_requester(client, REMOTE_RESPONDER_EID) + // • capabilities: meas_cap=0, no meas_fresh_cap, include_supported_algorithms=false + // • peer_cert_store: Some(&mut DemoPeerCertStore) (required by VCA+) + // --------------------------------------------------------------------------- + #[cfg(feature = "in-process-requester")] + let mut transport = { + let client = DirectMctpClient::new(&server); + MctpSpdmTransport::new_requester(client, REMOTE_RESPONDER_EID) + }; + + #[cfg(feature = "in-process-requester")] + { + pw_log::info!( + "MCTP server: initializing SPDM requester transport (target eid={})", + REMOTE_RESPONDER_EID as u32 + ); + if transport.init_sequence().is_err() { + pw_log::error!("MCTP server: SPDM transport init_sequence failed — \ + req({}) rejected; router request table may be full", + REMOTE_RESPONDER_EID as u32); + return Err(pw_status::Error::Internal); + } + pw_log::info!("MCTP server: SPDM requester transport ready"); + } + + #[cfg(feature = "in-process-requester")] + let mut cert_store = MockCertStore::new(); + #[cfg(feature = "in-process-requester")] + let mut hash = MockHash::new(); + #[cfg(feature = "in-process-requester")] + let mut m1_hash = MockHash::new(); + #[cfg(feature = "in-process-requester")] + let mut l1_hash = MockHash::new(); + #[cfg(feature = "in-process-requester")] + let mut rng = MockRng::new(); + #[cfg(feature = "in-process-requester")] + let evidence = MockEvidence::new(); + #[cfg(feature = "in-process-requester")] + let mut peer_cert_store = DemoPeerCertStore::default(); + + #[cfg(feature = "in-process-requester")] + let mut flags = CapabilityFlags::default(); + #[cfg(feature = "in-process-requester")] + { + flags.set_cert_cap(1); + flags.set_chal_cap(1); + flags.set_meas_cap(0); + flags.set_chunk_cap(1); + } + + #[cfg(feature = "in-process-requester")] + let capabilities = DeviceCapabilities { + ct_exponent: 0, + flags, + data_transfer_size: 1024, + max_spdm_msg_size: 4096, + // Setting true at V1.3 encodes param1 bit 2 of GET_CAPABILITIES, + // which the responder currently rejects as an unexpected reserved + // field. Algorithm negotiation goes through NEGOTIATE_ALGORITHMS. + include_supported_algorithms: false, + }; + + #[cfg(feature = "in-process-requester")] + static SUPPORTED_VERSIONS: [SpdmVersion; 2] = [SpdmVersion::V12, SpdmVersion::V13]; + + #[cfg(feature = "in-process-requester")] + 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 = "in-process-requester")] + let mut spdm_ctx = { + pw_log::info!("MCTP server: creating SPDM requester context (v1.2+v1.3, ECDSA-P384, SHA-384)"); + match SpdmContext::new( + &SUPPORTED_VERSIONS, + &mut transport, + capabilities, + algorithms, + &mut cert_store, + Some(&mut peer_cert_store), + &mut hash, + &mut m1_hash, + &mut l1_hash, + &mut rng, + &evidence, + ) { + Ok(ctx) => { + pw_log::info!("MCTP server: SPDM requester context ready"); + ctx + } + Err(_) => { + pw_log::error!("MCTP server: SpdmContext::new failed (requester) — \ + check cert_store, hash, rng platform impls"); + return Err(pw_status::Error::Internal); + } + } + }; + + // Shared between responder and requester — both need a MessageBuf for + // SPDM encode/decode. Gated on `direct-client` (implied by either role). #[cfg(feature = "direct-client")] let mut spdm_buf = [0u8; MAX_PAYLOAD_SIZE]; // MessageBuf is created once outside the loop; reset() is called each iteration. @@ -310,13 +509,31 @@ let mut i2c_recv_err: u32 = 0; let mut decode_err: u32 = 0; let mut inbound_err: u32 = 0; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut spdm_ok: u32 = 0; - #[cfg(feature = "direct-client")] + #[cfg(feature = "in-process-responder")] let mut spdm_err: u32 = 0; let mut i2c_pkt: u32 = 0; let mut idle_polls: u32 = 0; + // Requester FSM: starts at SendVersion, walks the VCA flow, ends at + // Done or Failed. Stepped once per loop iteration in Phase 2 below. + #[cfg(feature = "in-process-requester")] + let mut req_state = ReqState::SendVersion; + // Observability counters — see design §8. All u32 wrapping; log volume + // is bounded by rate-limiting inside the FSM step. + #[cfg(feature = "in-process-requester")] + let mut req_send_ok: u32 = 0; + #[cfg(feature = "in-process-requester")] + let mut req_recv_ok: u32 = 0; + #[cfg(feature = "in-process-requester")] + let mut req_recv_pending: u32 = 0; + // Number of consecutive steps spent in the current Await state. Reset + // to zero on every state transition; exhaustion of AWAIT_STEP_BUDGET + // transitions the FSM to Failed so the device does not hang. + #[cfg(feature = "in-process-requester")] + let mut await_steps: u32 = 0; + pw_log::info!("MCTP server ready, polling for I2C packets"); loop { @@ -374,7 +591,7 @@ ); match receiver.decode(&msg_with_dest) { - Ok((pkt, src_addr)) => { + Ok((pkt, hdr)) => { 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); @@ -385,9 +602,9 @@ "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, + hdr.source as u32, + hdr.dest as u32, + hdr.byte_count as u32, pkt.len() as u32, som as u32, eom as u32, seq as u32, msg_type as u32, ); @@ -456,7 +673,7 @@ // 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")] + #[cfg(feature = "in-process-responder")] { msg_buf.reset(); match spdm_ctx.responder_process_message(&mut msg_buf) { @@ -483,6 +700,155 @@ } } } + + // Phase 2 (requester): one FSM step per loop iteration. Send + // states emit a request and advance; Await states try to consume + // a response and either advance on Ok or stay on Err (which is + // the normal "router has not assembled the response yet" case + // given DirectMctpClient::recv returns TimedOut immediately). + // Done/Failed are terminal no-ops; Phase 1 keeps draining I2C. + #[cfg(feature = "in-process-requester")] + { + // Design follow-ups (see DESIGN_IN_PROCESS_REQUESTER.md): + // §6.3 option 2: plumb TransportError through + // `requester_process_message` so genuine protocol errors + // can be distinguished from TimedOut; today the Err arm + // relies on AWAIT_STEP_BUDGET alone. + // §9.1: pumping-`DirectMctpClient` (Shape B) would let the + // FSM call a synchronous API and delete the state machine. + // §9.3: external trigger — today the requester fires VCA + // at boot; a real caller will want IPC/GPIO/policy control. + // + // Await* states share a retry pattern: try to process one + // message; on Ok advance, on Err count it as pending and + // check the step budget. `await_try!` centralizes that + // logic and emits the state-timeout log if the budget + // runs out. + macro_rules! await_try { + ($next:expr, $state_code:expr) => {{ + msg_buf.reset(); + match spdm_ctx.requester_process_message(&mut msg_buf) { + Ok(_) => { + req_recv_ok = req_recv_ok.wrapping_add(1); + await_steps = 0; + req_state = $next; + } + Err(_) => { + req_recv_pending = req_recv_pending.wrapping_add(1); + await_steps = await_steps.wrapping_add(1); + if await_steps >= AWAIT_STEP_BUDGET { + pw_log::error!( + "SPDM requester: AWAIT_STEP_BUDGET exhausted in \ + state={} (steps={} pending_total={}) — giving up", + $state_code as u32, + await_steps as u32, + req_recv_pending as u32, + ); + req_state = ReqState::Failed; + } + } + } + }}; + } + + match req_state { + ReqState::SendVersion => { + msg_buf.reset(); + let ok = generate_get_version( + &mut spdm_ctx, + &mut msg_buf, + VersionReqPayload::new(0, 0), + ) + .is_ok() + && spdm_ctx + .requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID) + .is_ok(); + if ok { + req_send_ok = req_send_ok.wrapping_add(1); + await_steps = 0; + pw_log::info!("SPDM requester: sent GET_VERSION, awaiting VERSION"); + req_state = ReqState::AwaitVersion; + } else { + pw_log::error!("SPDM requester: GET_VERSION send failed"); + req_state = ReqState::Failed; + } + } + ReqState::AwaitVersion => { + await_try!(ReqState::SendCapabilities, ReqState::AwaitVersion); + if req_state == ReqState::SendCapabilities { + pw_log::info!("SPDM requester: VERSION ok, sending GET_CAPABILITIES"); + } + } + ReqState::SendCapabilities => { + msg_buf.reset(); + let ok = generate_capabilities_request_local(&mut spdm_ctx, &mut msg_buf) + .is_ok() + && spdm_ctx + .requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID) + .is_ok(); + if ok { + req_send_ok = req_send_ok.wrapping_add(1); + await_steps = 0; + pw_log::info!("SPDM requester: sent GET_CAPABILITIES, awaiting CAPABILITIES"); + req_state = ReqState::AwaitCapabilities; + } else { + pw_log::error!("SPDM requester: GET_CAPABILITIES send failed"); + req_state = ReqState::Failed; + } + } + ReqState::AwaitCapabilities => { + await_try!(ReqState::SendAlgorithms, ReqState::AwaitCapabilities); + if req_state == ReqState::SendAlgorithms { + pw_log::info!( + "SPDM requester: CAPABILITIES ok, sending NEGOTIATE_ALGORITHMS" + ); + } + } + ReqState::SendAlgorithms => { + msg_buf.reset(); + let ok = generate_negotiate_algorithms_request( + &mut spdm_ctx, + &mut msg_buf, + None, + None, + None, + None, + ) + .is_ok() + && spdm_ctx + .requester_send_request(&mut msg_buf, REMOTE_RESPONDER_EID) + .is_ok(); + if ok { + req_send_ok = req_send_ok.wrapping_add(1); + await_steps = 0; + pw_log::info!( + "SPDM requester: sent NEGOTIATE_ALGORITHMS, awaiting ALGORITHMS" + ); + req_state = ReqState::AwaitAlgorithms; + } else { + pw_log::error!("SPDM requester: NEGOTIATE_ALGORITHMS send failed"); + req_state = ReqState::Failed; + } + } + ReqState::AwaitAlgorithms => { + await_try!(ReqState::Done, ReqState::AwaitAlgorithms); + if req_state == ReqState::Done { + pw_log::info!( + "SPDM VCA completed: version/caps/algs OK \ + (send_ok={} recv_ok={} recv_pending={} idle_polls={})", + req_send_ok as u32, + req_recv_ok as u32, + req_recv_pending as u32, + idle_polls as u32, + ); + } + } + ReqState::Done | ReqState::Failed => { + // Terminal states — Phase 1 continues draining I2C, + // Phase 2 is a no-op. + } + } + } } }
diff --git a/target/ast1060-evb/mctp-requester/BUILD.bazel b/target/ast1060-evb/mctp-requester/BUILD.bazel new file mode 100644 index 0000000..13965b4 --- /dev/null +++ b/target/ast1060-evb/mctp-requester/BUILD.bazel
@@ -0,0 +1,85 @@ +# Licensed under the Apache-2.0 license + +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast1060-evb:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target:uart_boot_image.bzl", "uart_boot_image") + +# --------------------------------------------------------------------------- +# System image: I2C server + MCTP server (in-process SPDM requester variant) +# Mirrors //target/ast1060-evb/mctp:mctp but substitutes mctp_server_requester +# for mctp_server. The two images are mutually exclusive — flash one or the +# other, not both. +# --------------------------------------------------------------------------- + +system_image( + name = "mctp_requester", + apps = [ + "//services/i2c/server:i2c_server", + "//services/mctp/server:mctp_server_requester", + ], + kernel = ":target", + platform = "//target/ast1060-evb", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "mctp_requester_test", + image = ":mctp_requester", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +# Generate UART bootable image +uart_boot_image( + name = "mctp_requester_uart", + src = ":mctp_requester", + out = "mctp_requester_uart.bin", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast1060-evb:linker_script_template", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast1060-evb:entry", + "//target/ast1060-evb:console_backend_uart", + "@rust_crates//:cortex-m-semihosting", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +)
diff --git a/target/ast1060-evb/mctp-requester/system.json5 b/target/ast1060-evb/mctp-requester/system.json5 new file mode 100644 index 0000000..a905ca1 --- /dev/null +++ b/target/ast1060-evb/mctp-requester/system.json5
@@ -0,0 +1,125 @@ +// Licensed under the Apache-2.0 license + +// AST1060-EVB MCTP Echo Server + Client Configuration +// ARM Cortex-M4 @ 200 MHz +// 768KB SRAM (640KB usable), executes from RAM +// +// Memory Layout (PMSAv7-friendly, power-of-2 aligned regions): +// FIXED: Kernel RAM now comes BEFORE application RAM to avoid boot crashes +// 0x00000000 - 0x00000800: Vector table + kernel annotations (2048 bytes) +// 0x00000800 - 0x00020000: Kernel flash (~126KB) +// 0x00020000 - 0x00040000: i2c_server flash (128KB) +// 0x00040000 - 0x00060000: mctp_server flash (128KB) +// 0x00060000 - 0x00080000: Kernel RAM (128KB) +// 0x00080000 - 0x00090000: i2c_server RAM (64KB) +// 0x00090000 - 0x000A0000: mctp_server RAM (64KB) +// +// Total: 0xA0000 = 640KB (fits within the 640KB usable window of the AST1060's 768KB SRAM). +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 2048, // 0x800 (vector table + process annotations for all 3 apps) + }, + kernel: { + flash_start_address: 0x00000800, // After vector table + annotations + flash_size_bytes: 129024, // 0x1F800: ends at exactly 0x20000 (128KB boundary) + ram_start_address: 0x00060000, // Kernel RAM comes FIRST, before all app RAM + ram_size_bytes: 131072, // 128KB + }, + apps: [ + // ──── I2C Server ──── + // Owns the AST1060 I2C controllers. + // The MCTP server uses I2C as its transport. + { + name: "i2c_server", + flash_size_bytes: 131072, // 128KB for server code + ram_size_bytes: 65536, // 64KB RAM (register maps + buffers) + processes: [ + { + name: "i2c server process", + objects: [ + { + name: "I2C", + type: "channel_handler", // Server side of client→server channel + }, + //{ + // // AST1060 I2C2 combined master+slave interrupt (IRQ 112). + // name: "I2C2_IRQ", + // type: "interrupt", + // irqs: [ + // { + // name: "i2c2", + // number: 112, + // }, + // ], + //}, + { + name: "WG", + type: "wait_group", + }, + ], + threads: [ + { + name: "i2c server thread", + stack_size_bytes: 4096, // 4KB stack + }, + ], + memory_mappings: [ + { + // AST1060 I2C peripheral block: I2cglobal through I2cFilter + // Base: 0x7e7b0000, covers all 14 controllers + buffers + filter + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + { + // SCU register block — scu310 at +0x310 is read by + // ClockConfig::from_hardware() to derive APB clock frequency. + name: "scu", + type: "device", + start_address: 0x7e6e2000, + size_bytes: 0x1000, + }, + ], + }, + ], + }, + // ──── MCTP Server ──── + // Runs the MCTP stack + in-process SPDM requester. Same footprint + // as the responder variant; only the app name differs. + { + name: "mctp_server_requester", + flash_size_bytes: 65536, // 64KB for MCTP server code + ram_size_bytes: 65536, // 64KB RAM (Router state + SPDM context + buffers) + processes: [ + { + name: "mctp server process", + objects: [ + { + name: "MCTP", + type: "channel_handler", // Server side: echo client → MCTP server + }, + { + name: "I2C", + type: "channel_initiator", // Client side: MCTP server → I2C server + handler_process: "i2c server process", + handler_object_name: "I2C", + }, + { + name: "WG", + type: "wait_group", + }, + ], + threads: [ + { + name: "mctp server thread", + stack_size_bytes: 32768, // 8KB stack (Router + fragmentation buffers) + }, + ], + }, + ], + }, + ], +}
diff --git a/target/ast1060-evb/mctp-requester/target.rs b/target/ast1060-evb/mctp-requester/target.rs new file mode 100644 index 0000000..931afa5 --- /dev/null +++ b/target/ast1060-evb/mctp-requester/target.rs
@@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license + +//! AST1060-EVB MCTP Echo Target +//! +//! This target runs the I2C server, MCTP server, and MCTP echo application +//! as separate userspace processes communicating over IPC channels. + +#![no_std] +#![no_main] + +use target_common::{TargetInterface, declare_target}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST1060-EVB MCTP Echo"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);