From 7b830af37b75fa7ae4b3378d59b29e1faa751fe3 Mon Sep 17 00:00:00 2001 From: Christina Quast Date: Mon, 7 Sep 2026 16:34:41 +0200 Subject: [PATCH] docs: add PLDM/orchestrator IPC design diagram Sequence diagram covering the two-channel IPC architecture between the PLDM FirmwareDevice service and the orchestrator. Covers the notify channel (pre-transfer veto), intake channel (offer/write/complete/poll), async effect chain with poll_stage, USER signal nudge, and FD-initiated PLDM commands. Includes verified findings: USER signal is level-triggered (no lost wakeups), MCTP server buffers 4 messages independently while PLDM is in a transact, and poll_stage worst case is bounded by flash erase time. Assisted-by: Claude --- .../orchestrator/pldm-orchestrator-ipc.html | 558 ++++++++++++++++++ 1 file changed, 558 insertions(+) create mode 100644 docs/src/design/orchestrator/pldm-orchestrator-ipc.html 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 @@ +PLDM-Orchestrator IPC + + +

PLDM / Orchestrator IPC

+

Two kernel channels, both initiated by PLDM. Firmware bytes never cross either channel.

+ +
+
Notify channel (pldm-notify-api)
+
Intake channel (update-api)
+
Blocking (channel_transact)
+
USER signal nudge (non-blocking)
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + UA (BMC) + remote, over MCTP + + + + PLDM FirmwareDevice + single thread: run_terminus + + + + Orchestrator + single thread: object_wait loop + + + + Shared Storage + ext. SPI flash + + + + + + + + + NOTIFY + + + + RequestUpdate (MCTP) + + + + BLOCKS + + + + LOW RISK + MCTP server + buffers 4 msgs + + + channel_transact + Request::UpdateRequested + + + check state, policy + + + Response::Accepted | Rejected + + + + if Accepted + + + INTAKE + + + + BLOCKS + + + LOW RISK + (fast path) + + + Offer { target: TargetId, total: u64 } + + + validate target + length, reserve staging + + + IntakeStatus::Receiving { written: 0, total } + + + + loop + FD pulls chunks from UA via RequestFirmwareData + + + + RequestFirmwareData (MCTP) + + firmware chunk response + + + + write firmware bytes to flash (direct, no IPC) + + + + BLOCKS + + + + LOW RISK + Write is + fast path + + + Write { offset: u64, len: u16 } + + + track contiguous prefix + + + IntakeStatus::Receiving { written, total } + + + + + optional + + + Poll + + IntakeStatus + + + + BLOCKS + + + LOW RISK + queues only + returns fast + + + Complete + + + check coverage, queue Pending::UpdateRequest + + + IntakeStatus + + + + async: orchestrator event loop drains pending + + + + + TransferComplete (MCTP) + + + + FREE + + + + MCTP responsive + + + + PLDM + services + UA on + MCTP + + + EFFECT CHAIN (non-blocking steps) + + 1. + poll_pending + 2. + SM: Ready -> Updating + 3. + poll_stage (one step) + 4. + return to object_wait + ^^ + repeat 3-4 until phase done + IPC responsive between steps + + + + PayloadSource::read_at + + + + + object_set_peer_user_signal + dataless nudge, wakes PLDM's WaitGroup + + + POLL + FD COMMANDS + + + loop + wake on USER signal, poll status, send *Complete to UA when phase finishes + + + + BLOCKS + + + LOW RISK + Poll is a + latched read + + + Poll + + + read latched IntakeStatus + + + Authenticating | Staging | Activated | Failed + + + when phase done + + VerifyComplete (MCTP) + + + + ApplyComplete (MCTP) + + FD must know phase completion to initiate these + + + any time + + + Abort + + IntakeStatus::Idle + + + + Blocking direction: always PLDM -> Orchestrator, never the reverse + Every IPC response is immediate. Effects run async via poll_stage (one step, return, repeat). + PLDM stays free to service UA on MCTP. USER signal nudge replaces blind polling. + FD initiates TransferComplete, VerifyComplete, ApplyComplete to UA when it learns phase is done. + +
+ +
+

Wire formats

+
+
+
Notify channel (4B each way)
+
+Req:  [op:1][len:1][reserved:2]
+Resp: [code:1][len:1][reserved:2]
+
+ops:   UpdateRequested (0)
+codes: Accepted (0) | Rejected (1)
+
+
+
Intake channel (16B req, 20B resp)
+
+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
+
+
+
+ +
+
+
Every call blocks PLDM
+

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.

+
+
+
Starvation resolved: all IPC responses are immediate
+

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.

+
+
+
Firmware bytes stay out of IPC
+

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, does not dispatch
+

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.

+
+
+
Caveat: poll_stage must be incremental
+

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.

+
+
+
USER signal nudge replaces blind polling
+

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.

+
+
+
FD initiates TransferComplete, VerifyComplete, ApplyComplete
+

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.

+
+
+
MCTP buffering while PLDM is busy
+

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.

+
+
+
Open: activation gating
+

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.

+
+