Skip to content

docs: PLDM-FD-as-server IPC design (alternative to #458) - #464

Open
chrysh wants to merge 1 commit into
OpenPRoT:mainfrom
9elements:docs-pldm-server-ipc
Open

chrysh wants to merge 1 commit into
OpenPRoT:mainfrom
9elements:docs-pldm-server-ipc

Conversation

@chrysh

@chrysh chrysh commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Human TL;DR: The design is based on those requirements: PLDM-FD and Orchestrator shall not block. PLDM is IPC-Server. --> nudge from PLDM-FD, orchestrator queries what nudge was about. Other services need to use signals to inform orchestrator async about work being finished (FW written to flash, verification done, etc)

Orchestratrator decides whether FW-Update steps are continued or vetoed. --> Therefore the Orchestrator gives out grants.

PLDM-FD automatically goes through the steps fw_data_download, verify, apply. So the functions FdOps::fw_data_download, FdOps::verify, FdOps::apply are automatically called by PLDM-FD. FdOps::* calls those functions in a loop chunkwise until the callee fills the u8-progress-value with "100" (signifying 100%).

Crypto service can read firmware data directly from Flash service without going through orchestrator.

AI-Summary

Alternative to #458: PLDM-FD is the IPC server, orchestrator is its client.

  • FD drives the PLDM protocol and executes firmware ops through FdOps callbacks (fw_data_download, verify, apply, activate)
  • Orchestrator is a gatekeeper: grants or denies each phase (download via AcceptOffer, verify, apply, activate) but does not execute ops itself
  • FdOps::fw_data_download calls the device server; FdOps::verify delegates to the crypto service (which reads the staged image directly from the device server); FdOps::apply and FdOps::activate use platform driver trait for board-specific logic, execute via device server
  • Minimal copies: firmware lands in its staging region and is verified in place, no intermediate buffers
  • Platform driver is a trait linked at build time, not a service; owns slot logic, knows where each component's image belongs
  • ServiceCall<Req, Resp> is the universal IPC primitive: start() sends non-blocking, signal() goes in WaitGroup, try_recv() picks up result after wake
  • The orchestrator never blocks: WaitGroup multiplexes FD nudges, ServiceCall completions (SVN bump, write filter), CompromiseDetected, timers
  • SVN floor is not bumped at activation: the UA sets Security Revision Number Delayed Update on UpdateComponent, then sends UpdateSecurityRevision (0x22, DSP0267 1.3.0 section 12.19) once it is satisfied with the running image. Bumping at activation would leave the superseded image below the floor and make a trial-boot revert impossible
  • The orchestrator owns the floor write: on a confirmed trial it advances SvnFloor for that component, then sends GrantSvnCommit so the FD can answer the UA; on an unconfirmed trial it denies with PolicyViolation
  • Five crates, split so the protocol path builds and tests on the host: only pldm-ipc-server-runtime and orchestrator-pldm-client-ipc are kernel-tagged, and neither holds protocol logic
  • Fixed 8-byte wire header, op decoded through TryFrom<u8> so an unknown value is a decode error; each side treats the other as untrusted and neither dispatch panics
  • Grant gates add no PLDM states: pldm-lib polls FdOps::verify/apply via fd_progress, our impl returns 0% progress until the orchestrator grants; dispatch loop stays live between polls
  • Delegated verification: first verify() poll sends a single ServiceCall to the crypto service, subsequent polls check for completion; crypto owns the full read-hash-check pipeline in its own process
  • FdOps callbacks must not block for long (CancelUpdate can arrive async)
  • Out-of-transport: platform driver knows the staging address, orchestrator communicates it to the third party that pre-stages the image
  • All orchestrator-to-FD IPC ops return Reply immediately, no Pending
  • All IPC via ServiceCall + WaitGroup (no process ever blocks on another)
  • Diagrams use colored backgrounds: blue for orchestrator IPC, green for FdOps service IPC
  • Open questions: ActivateFirmware/CancelUpdate UA response timing, what the orchestrator does when the UA omits SVNDelayedUpdate or never sends 0x22, priv_data for FdOps, FD death detection, corruption scanner, SMC write filter mechanism

Full design doc: docs/src/design/orchestrator/pldm-server-ipc.md

In-transport sequence

sequenceDiagram
    participant UA as UA (BMC)<br/>remote, over MCTP
    participant FD as PLDM-FD (server)<br/>dispatch loop + run_terminus
    participant Orch as Orchestrator (client)<br/>ServiceCall
    participant DevSrv as Device Server<br/>manages the SPI flash
    participant Crypto as Crypto Service<br/>hash + signature verification

    Note over UA, Crypto: Blue background: orchestrator IPC. Green background: FdOps service IPC.

    Note over UA, Orch: NEGOTIATION (PLDM protocol, MCTP only)

    UA->>FD: RequestUpdate (MCTP)
    FD-->>UA: RequestUpdate response (accepted)
    UA->>FD: PassComponentTable (MCTP)
    FD-->>UA: PassComponentTable response
    UA->>FD: UpdateComponent (MCTP)
    FD-->>UA: UpdateComponent response

    Note over FD, Orch: FD has an offer, nudge the orchestrator

    FD->>Orch: USER signal (nudge: offer ready)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::OfferPending { target, total, mode: InTransport }
    Note right of Orch: validate target + total,<br/>platform driver picks<br/>staging address,<br/>reserve staging,<br/>open SMC write filter
    Orch->>FD: ServiceCall: AcceptOffer { base: FlashAddress }
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over UA, DevSrv: TRANSFER (FD pulls from UA, FdOps writes to flash)

    rect rgb(230, 255, 230)
    loop FdOps::fw_data_download per chunk
        FD->>UA: RequestFirmwareData (MCTP)
        UA-->>FD: firmware chunk
        FD->>DevSrv: ServiceCall: write chunk
        DevSrv-->>FD: signal: Ok
    end
    end

    Note over FD, Orch: transfer done, ask orchestrator to grant verify

    FD->>UA: TransferComplete (MCTP)
    FD->>Orch: USER signal (nudge: verify pending)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::VerifyPending
    Note right of Orch: check isolation, update policy
    Orch->>FD: ServiceCall: GrantVerify
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over FD, Crypto: FdOps::verify

    rect rgb(230, 255, 230)
    FD->>Crypto: ServiceCall::start(VerifyRequest { addr, size })
    Crypto->>DevSrv: read staged image
    DevSrv-->>Crypto: image data
    loop fd_progress poll
        Note over FD: verify() returns 0%, no signal yet
    end
    Crypto-->>FD: signal: Verdict
    Note over FD: verify() poll: try_recv -> 100% + verdict
    end
    FD->>UA: VerifyComplete (MCTP)

    Note over FD, Orch: verify done, ask orchestrator to grant apply

    FD->>Orch: USER signal (nudge: apply pending)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::ApplyPending { verify_ok: true }
    Orch->>FD: ServiceCall: GrantApply
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over FD, DevSrv: FdOps::apply

    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall::start(apply: commit staged image)
    loop fd_progress poll
        Note over FD: apply() returns 0%, no signal yet
    end
    DevSrv-->>FD: signal: Ok
    Note over FD: apply() poll: try_recv -> 100%
    end
    FD->>UA: ApplyComplete (MCTP)

    Note over FD, Orch: ACTIVATION (FdOps::activate)

    UA->>FD: ActivateFirmware (MCTP)
    FD->>Orch: USER signal (nudge: activation requested)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::ActivationPending
    Note right of Orch: close SMC write filter
    Orch->>FD: ServiceCall: Activate
    FD-->>Orch: Ok
    deactivate Orch
    end

    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall: FdOps::activate: set boot preference
    DevSrv-->>FD: signal: Ok
    end

    FD-->>UA: ActivateFirmware response (accepted)

    Note over UA, DevSrv: SVN COMMIT (later, FD back in IDLE, new image running)

    Note right of Orch: judge the boot, then<br/>TrialBoot::confirm or revert
    UA->>FD: UpdateSecurityRevision (MCTP, 0x22)
    FD->>Orch: USER signal (nudge: SVN commit requested)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::SvnCommitPending { component }
    Note right of Orch: a confirmed trial only,<br/>else DenySvnCommit
    Orch->>DevSrv: ServiceCall: SvnFloor::advance
    DevSrv-->>Orch: signal: Ok
    Orch->>FD: ServiceCall: GrantSvnCommit
    FD-->>Orch: Ok
    deactivate Orch
    end

    FD-->>UA: UpdateSecurityRevision response (success)

    Note over UA, Orch: CANCEL (between AcceptOffer and Activate)
    UA->>FD: CancelUpdate (MCTP)
    FD->>Orch: USER signal (nudge: cancelled)
    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::Cancelled
    Note right of Orch: release staging,<br/>close SMC write filter
    Orch->>FD: ServiceCall: AckCancel
    FD-->>Orch: Ok
    deactivate Orch
    end
    FD-->>UA: CancelUpdate response
Loading

Out-of-transport sequence

sequenceDiagram
    participant UA as UA (BMC)<br/>remote, over MCTP
    participant FD as PLDM-FD (server)<br/>dispatch loop + run_terminus
    participant Orch as Orchestrator (client)<br/>ServiceCall
    participant DevSrv as Device Server<br/>manages the SPI flash
    participant Crypto as Crypto Service<br/>hash + signature verification

    Note over UA, Crypto: Blue background: orchestrator IPC. Green background: FdOps service IPC.

    Note over UA, Orch: NEGOTIATION (same as in-transport)

    UA->>FD: RequestUpdate (MCTP)
    FD-->>UA: RequestUpdate response (accepted)
    UA->>FD: PassComponentTable (MCTP)
    FD-->>UA: PassComponentTable response
    UA->>FD: UpdateComponent (MCTP, out-of-transport)
    FD-->>UA: UpdateComponent response

    Note over FD, Orch: FD has an offer, nudge the orchestrator

    FD->>Orch: USER signal (nudge: offer ready)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::OfferPending { target, total, mode: OutOfTransport }
    Note right of Orch: validate target + total,<br/>platform driver picks<br/>staging address
    Orch->>FD: ServiceCall: AcceptOffer { base: FlashAddress }
    Note left of FD: FD does not write in<br/>out-of-transport, but the<br/>orchestrator communicates the<br/>base address to the third party<br/>that pre-stages the image
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over FD, Orch: no transfer phase, image already staged

    FD->>UA: TransferComplete (MCTP)

    Note over FD, Orch: ask orchestrator to grant verify

    FD->>Orch: USER signal (nudge: verify pending)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::VerifyPending
    Note right of Orch: check isolation, update policy
    Orch->>FD: ServiceCall: GrantVerify
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over FD, Crypto: FdOps::verify

    rect rgb(230, 255, 230)
    FD->>Crypto: ServiceCall::start(VerifyRequest { addr, size })
    Crypto->>DevSrv: read staged image
    DevSrv-->>Crypto: image data
    loop fd_progress poll
        Note over FD: verify() returns 0%, no signal yet
    end
    Crypto-->>FD: signal: Verdict
    Note over FD: verify() poll: try_recv -> 100% + verdict
    end
    FD->>UA: VerifyComplete (MCTP)

    Note over FD, Orch: verify done, ask orchestrator to grant apply

    FD->>Orch: USER signal (nudge: apply pending)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::ApplyPending { verify_ok: true }
    Orch->>FD: ServiceCall: GrantApply
    FD-->>Orch: Ok
    deactivate Orch
    end

    Note over FD, DevSrv: FdOps::apply

    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall::start(apply: commit staged image)
    loop fd_progress poll
        Note over FD: apply() returns 0%, no signal yet
    end
    DevSrv-->>FD: signal: Ok
    Note over FD: apply() poll: try_recv -> 100%
    end
    FD->>UA: ApplyComplete (MCTP)

    Note over FD, Orch: ACTIVATION (FdOps::activate, same as in-transport)

    UA->>FD: ActivateFirmware (MCTP)
    FD->>Orch: USER signal (nudge: activation requested)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::ActivationPending
    Orch->>FD: ServiceCall: Activate
    FD-->>Orch: Ok
    deactivate Orch
    end

    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall: FdOps::activate: set boot preference
    DevSrv-->>FD: signal: Ok
    end

    FD-->>UA: ActivateFirmware response (accepted)

    Note over UA, DevSrv: SVN COMMIT (later, FD back in IDLE, new image running)

    Note right of Orch: judge the boot, then<br/>TrialBoot::confirm or revert
    UA->>FD: UpdateSecurityRevision (MCTP, 0x22)
    FD->>Orch: USER signal (nudge: SVN commit requested)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::SvnCommitPending { component }
    Note right of Orch: a confirmed trial only,<br/>else DenySvnCommit
    Orch->>DevSrv: ServiceCall: SvnFloor::advance
    DevSrv-->>Orch: signal: Ok
    Orch->>FD: ServiceCall: GrantSvnCommit
    FD-->>Orch: Ok
    deactivate Orch
    end

    FD-->>UA: UpdateSecurityRevision response (success)

    Note over UA, Orch: CANCEL (between AcceptOffer and Activate)
    UA->>FD: CancelUpdate (MCTP)
    FD->>Orch: USER signal (nudge: cancelled)
    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::Cancelled
    Note right of Orch: discard the accepted offer
    Orch->>FD: ServiceCall: AckCancel
    FD-->>Orch: Ok
    deactivate Orch
    end
    FD-->>UA: CancelUpdate response
Loading

Security revision commit

Activation does not raise the anti-rollback floor. If it did, a trial boot
could never be reverted: the superseded image would sit below the new floor
and refuse to run. DSP0267 1.3.0 keeps the two apart. The UA sets the
Security Revision Number Delayed Update option (UpdateOptionFlags bit 2) on
UpdateComponent, the FD applies and activates without touching the revision,
and the UA sends UpdateSecurityRevision (command 0x22, section 12.19) once it
is satisfied with the image. Until that command arrives a downgrade is still
allowed, which is the window the trial boot lives in.

The FD accepts 0x22 only in the IDLE state, so it arrives outside update mode,
minutes or days after activation. It acts on the active running image, not a
pending one, which is what makes it safe to gate on the boot verdict.

The orchestrator owns the write. On a confirmed trial it advances SvnFloor
for that component, then sends GrantSvnCommit so the FD can answer the UA. The
FD relays the request and the answer and never touches the floor, the same
split the rest of this design uses for anything irreversible. On an
unconfirmed or absent trial the orchestrator denies with PolicyViolation and
the FD returns UPDATE_SECURITY_REVISION_NOT_PERMITTED. DSP0267 has no code for
a policy refusal, so that capability code is the nearest fit.

IPC operations

Op Direction Purpose
AcceptOffer orch -> FD Accept with a staging base address
RejectOffer orch -> FD Reject (FD tells UA in the next response)
GrantVerify orch -> FD Authorize FD to run FdOps::verify
DenyVerify orch -> FD Block verify (e.g. isolated component); FD returns failure to UA
GrantApply orch -> FD Authorize FD to run FdOps::apply
DenyApply orch -> FD Block apply; FD returns failure to UA
QueryStatus orch -> FD Read current FD state (phase, result, error); when OfferPending, includes offer data (target, total, transfer mode, SVN delayed)
Activate orch -> FD Authorize activation
AckCancel orch -> FD Acknowledge cancel, release orchestrator-side resources
GrantSvnCommit orch -> FD Tell the FD the floor is raised, so it can answer the UA
DenySvnCommit orch -> FD Block the commit, reason PolicyViolation (no confirmed trial); FD answers the UA with UPDATE_SECURITY_REVISION_NOT_PERMITTED

FdOps and IPC services

Callback ServiceCall to Purpose
fw_data_download device server Write a firmware chunk to the staging region
verify crypto service Hash and signature check; crypto reads the staged image directly from the device server
apply device server Commit the staged image (platform driver trait determines what to write)
activate device server Set boot preference (platform driver trait determines the operation)
cancel_update_component device server Abort in-flight operations, discard FD transfer state

Test plan

  • Read through both sequence diagrams for protocol correctness
  • Review ServiceCall + WaitGroup pattern for correctness
  • Decide on open questions (response timing, omitted SVNDelayedUpdate, never-sent 0x22, priv_data, streaming verify, FD death detection, corruption scanner, SMC write filter)

Assisted-by: Claude

@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch 2 times, most recently from daf28d3 to bcdec55 Compare September 10, 2026 18:21
@chrysh chrysh changed the title docs: PLDM-as-server IPC design (alternative to #458) docs: PLDM-FD-as-server IPC design (alternative to #458) Sep 10, 2026
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch 6 times, most recently from e66be4b to 3843315 Compare September 11, 2026 10:18
activate Orch
Orch->>FD: channel_transact: QueryStatus
FD-->>Orch: Status::ActivationPending
Note right of Orch: bump SVN (irreversible),<br/>close SMC write filter

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Iirc, the SVN bumping should not happen automatically but only via a manual invocation, once the firmware is considered proven in use? @FerralCoder

@chrysh
chrysh marked this pull request as ready for review September 14, 2026 08:43
@chrysh chrysh mentioned this pull request Sep 14, 2026
2 tasks
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch 2 times, most recently from 4242d48 to f1b64a2 Compare September 14, 2026 18:59
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch from f1b64a2 to 493bd7d Compare September 14, 2026 20:21

Design decisions:

- ServiceCall is the universal IPC primitive. Every cross-process request

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth naming which pw_kernel syscalls start()/signal()/try_recv() map to — e.g. channel_async_transact + channel_async_transact_complete, with the initiator handle added to the caller's WaitGroup for signal().

One implication worth calling out here:
channel_async_transact is unsafe specifically because its send/recv buffers must stay "pinned" until the transaction completes, so ServiceCall<Req, Resp> can't hand start() transient stack buffers — it needs to own Req/Resp for the call's lifetime.

struct IpcPlatform {
// Pinned until try_complete() -- can't share one scratch buffer across
// in-flight channels; each gets its own fixed-size buffer, held here.
gpio_buf: (SendBuf, RecvBuf),
wdt_buf: (SendBuf, RecvBuf),
mailbox_buf: (SendBuf, RecvBuf),
power_buf: (SendBuf, RecvBuf),
}

Alternative to OpenPRoT#458. The FD owns the dispatch loop and the PLDM state
machine; the orchestrator is its client and grants or denies each phase
(AcceptOffer, GrantVerify, GrantApply, GrantActivate, GrantSvnCommit). Firmware
bytes never pass through the orchestrator: FdOps::download_fw_data writes via
the device server and FdOps::verify delegates to the crypto service, which
reads the staged image from the device server itself.

Every cross-process request is a ServiceCall whose completion signal sits in
the caller's WaitGroup, so no process blocks on another and the orchestrator
stays reactive to CompromiseDetected and its boot watchdogs. The grant gates
add no PLDM states: pldm-lib polls verify and apply through fd_progress and
our implementation reports 0% until the grant arrives, which keeps the MCTP
responder live during the wait.

The security revision is not bumped at activation. Bumping there would put
the superseded image below the floor and make a trial-boot revert impossible,
so the UA sets Security Revision Number Delayed Update and sends
UpdateSecurityRevision (0x22) once it is satisfied with the running image.

Five crates, only the server-runtime and the client-ipc kernel-tagged, so the
wire and dispatch run host-side against a loopback. Write containment has two
layers, a device-server staging window and the SMC write filter. Both close
before GrantVerify, because a verdict over flash that can still be written does
not match the image that apply commits. Both cover staging only; apply and
activate stay reachable by a compromised FD and that gap is stated, not closed.
Open questions are listed at the end of the doc, FD death answered with
restart-and-nudge.

Assisted-by: Claude
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch from 0840dbc to 583182b Compare September 16, 2026 09:41
@rusty1968

Copy link
Copy Markdown
Collaborator

@chrysh, I believe this is a more suitable design than the original proposal. If we are in agreement, given you stated that we should agree as a group to move on from that proposal, we can declare the orchestrator as a channel handler defunct and converge on this. Let's present this to the pigweed team so they can give us advice.

@chrysh

chrysh commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@chrysh, I believe this is a more suitable design than the original proposal. If we are in agreement, given you stated that we should agree as a group to move on from that proposal, we can declare the orchestrator as a channel handler defunct and converge on this. Let's present this to the pigweed team so they can give us advice.

@rusty1968 sounds good. We also had a look together with @leongross at osfc2026. How do we present it to the pigweed people? Can you do that? And how can I help?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants