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

Summary

Alternative to #458. PLDM-FD is the IPC server and owns the protocol state
machine; the orchestrator is its client and grants or denies each phase.
Firmware bytes never pass through the orchestrator.

  • Paired grant and deny ops at every gate, each deny carrying a reason:
    AcceptOffer/RejectOffer, GrantVerify, GrantApply, GrantActivate,
    GrantSvnCommit
  • All IPC is ServiceCall over channel_async_transact, with completion signals
    in a WaitGroup, so no process blocks on another and the FD never parks
  • Activation is pre-authorized at apply-complete, because FdOps::activate is
    synchronous and what it returns is the UA's completion code
  • Staging stops being writable before GrantVerify, so the crypto verdict
    describes the image apply commits
  • The SVN floor is not bumped at activation: the UA sends UpdateSecurityRevision
    (0x22) and the orchestrator advances the floor only on a confirmed trial

Full design: docs/src/design/orchestrator/pldm-server-ipc.md. Open questions
are listed at the end of that file.

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 staging window
    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::download_fw_data 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,<br/>close staging window
    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
    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: apply done, decide activation before the UA asks

    FD->>Orch: USER signal (nudge: activation decision)

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

    Note over UA, Orch: ACTIVATION (FdOps::activate, any time later)

    UA->>FD: ActivateFirmware (MCTP)
    FD-->>UA: ActivateFirmware response (accepted, from the stored grant)

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

    FD->>Orch: USER signal (nudge: activated)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::Idle { reason: ActivateFw }
    Note right of Orch: release staging,<br/>start judging the boot
    deactivate Orch
    end

    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 activation)
    UA->>FD: CancelUpdate (MCTP)
    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall: FdOps::cancel_update_component
    DevSrv-->>FD: signal: Ok
    end
    FD-->>UA: CancelUpdate response
    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 staging window
    Orch->>FD: ServiceCall: AckCancel
    FD-->>Orch: Ok
    deactivate Orch
    end
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; a third party<br/>pre-stages the image, and how it<br/>learns the address is open
    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,<br/>close staging window
    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
    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: apply done, decide activation before the UA asks

    FD->>Orch: USER signal (nudge: activation decision)

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

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

    UA->>FD: ActivateFirmware (MCTP)
    FD-->>UA: ActivateFirmware response (accepted, from the stored grant)

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

    FD->>Orch: USER signal (nudge: activated)

    rect rgb(230, 240, 255)
    activate Orch
    Orch->>FD: ServiceCall: QueryStatus
    FD-->>Orch: Status::Idle { reason: ActivateFw }
    Note right of Orch: release staging,<br/>start judging the boot
    deactivate Orch
    end

    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 activation)
    UA->>FD: CancelUpdate (MCTP)
    rect rgb(230, 255, 230)
    FD->>DevSrv: ServiceCall: FdOps::cancel_update_component
    DevSrv-->>FD: signal: Ok
    end
    FD-->>UA: CancelUpdate response
    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
Loading

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

@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),
}

@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?

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 583182b to d330e8b Compare September 21, 2026 12:27
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