diff --git a/docs/src/design/orchestrator/pldm-orchestrator-ipc.html b/docs/src/design/orchestrator/pldm-orchestrator-ipc.html new file mode 100644 index 000000000..be5132f64 --- /dev/null +++ b/docs/src/design/orchestrator/pldm-orchestrator-ipc.html @@ -0,0 +1,558 @@ +
Two kernel channels, both initiated by PLDM. Firmware bytes never cross either channel.
+ ++Req: [op:1][len:1][reserved:2] +Resp: [code:1][len:1][reserved:2] + +ops: UpdateRequested (0) +codes: Accepted (0) | Rejected (1)+
+Req: [op:1][target:1][rsv:2][arg:8][len:2][rsv:2] + (no payload, bytes go direct to flash) +Resp: [code:1][phase:1][detail:1][rsv:1] + [written:8][total:8] + +ops: Offer(0) Write(1) Complete(2) Abort(3) Poll(4) + +OPEN: add Activate(5) if activation is UA-gated, + or auto-activate after staging+
Both channels use channel_transact on the PLDM side: send request, block until the orchestrator responds. The orchestrator never blocks on PLDM. It handles requests in its object_wait loop and can nudge PLDM via object_set_peer_user_signal (dataless, non-blocking). While PLDM is in a transact, the MCTP server (separate process) keeps consuming I2C interrupts and reassembling messages into 4 slots (NUM_RECEIVE). Overflow drops silently, no backpressure to the bus.
Every orchestrator IPC handler responds before doing real work. Complete queues Pending::UpdateRequest and returns. Effects run asynchronously: the event loop calls poll_stage, which does one step and returns, then the loop goes back to object_wait. Between steps the orchestrator can serve incoming channel_read requests (Poll, Abort). PLDM's channel_transact blocks for microseconds, not seconds.
PLDM writes firmware data directly to external SPI flash. The orchestrator reads it back through PayloadSource::read_at. The 768 KB SRAM is too small to buffer an image, and keeping attacker-controlled PLDM input on the far side of the process boundary is the design intent.
Complete queues a Pending::UpdateRequest. The event loop's poll_pending drains it between waits. The effect chain (auth, stage, activate) runs outside any channel response, so Complete itself returns fast.
The "non-blocking steps" claim holds only if every poll_stage call finishes quickly. The designed direct-flash adapter does one flash op per step: a sector erase (10-100 ms) or a page program (1-5 ms). During that window the orchestrator cannot serve IPC, and the MCTP server's 4 reassembler slots are the only buffer. For the transfer loop this is fine (orchestrator is idle, transacts return in microseconds). For verify/apply the worst case is a sector erase, which is short enough that 4 MCTP slots won't fill under normal UA traffic (occasional GetStatus/Cancel, retried on timeout). A signature check that hashes the entire image from SPI in one step would reintroduce the starvation window; auth steps must be chunked the same way.
When a phase changes (auth done, staging step done, activated), the orchestrator calls object_set_peer_user_signal. Verified level-triggered: the signal is OR'd into the peer's active_signals bitfield and persists until the sender lowers it. If PLDM is busy when the nudge arrives, the next object_wait returns immediately, no lost wakeups. PLDM sends Poll only after a nudge, reads the latched IntakeStatus, and decides whether to send VerifyComplete or ApplyComplete to the UA. Nudges coalesce: two phase changes before PLDM polls = one wakeup, and the status may skip phases (Authenticating straight to Activated). Status is monotonic, so the FD infers earlier completions and sends the corresponding *Complete commands back-to-back.
These three commands are FD-initiated per the PLDM spec. The UA waits for the FD to send them, not the other way around. This is why PLDM needs to learn about phase completion from the orchestrator: it must send VerifyComplete when verify passes and ApplyComplete when apply finishes. Without the nudge + poll path, the FD has no way to know when to fire these.
The MCTP server runs in its own process and keeps consuming I2C interrupts while PLDM is in channel_transact. Completed messages sit in mctp-estack's 4 reassembler slots (NUM_RECEIVE=4, 1032 bytes each, tunable at build time). When all 4 slots are full, further inbound packets are silently dropped (NoSpace). Below that, the I2C server holds one frame in a single-slot latch (overwritten on the next IRQ), and the AST1060 hardware NAKs the bus if the I2C server hasn't re-armed. During the transfer loop this doesn't matter: the orchestrator is idle, transacts return fast, and chunk flow is FD-paced. During verify/apply, only occasional GetStatus/Cancel arrives, well within 4 slots.
The PLDM spec makes activation UA-initiated (ActivateFirmware command, sent after ApplyComplete). The intake wire has no Activate op, so the orchestrator would auto-activate after staging. Either that's intentional (the UA's ActivateFirmware becomes a no-op) or the wire needs an Activate(5) op so the FD can forward the UA's command. Connects to the staging-is-inert question.