Skip to content

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

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

chrysh wants to merge 8 commits 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, 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
Design doc for the PLDM service as IPC server, orchestrator as client.
Reverses the direction from the notify/intake design: the orchestrator
calls into PLDM via channel_transact, PLDM nudges back with USER signals.
Covers both in-transport and out-of-transport image transfer.

Assisted-by: Claude
Design doc for an alternative to OpenPRoT#458: PLDM-FD runs as IPC server, the
orchestrator is its client. The FD drives the PLDM protocol and executes
firmware operations through FdOps callbacks (fw_data_download calls the
flash device server, verify calls the crypto service, apply and activate
call the device server). The orchestrator is a gatekeeper: it grants or
denies each phase (verify, apply, activate) but does not execute
operations itself. Firmware lands in the staging region and is verified in
place, no intermediate copies. Platform owns slot logic.

Assisted-by: Claude
ServiceCall<Req, Resp> is the universal IPC primitive: start() sends
non-blocking, the caller adds the completion signal() to its WaitGroup,
and try_recv() picks up the result after wake. No process ever blocks on
another; object_wait on the WaitGroup sleeps the thread until any signal
fires.

The orchestrator never blocks. Its WaitGroup multiplexes FD nudge
signals, ServiceCall completions (SVN bump, write filter),
CompromiseDetected, and timers.

The platform driver is a trait linked at build time, not a service.
apply and activate use board-specific logic from the trait, then execute
the actual I/O via the device server.

FdOps::verify delegates the entire operation to the crypto service in a
single ServiceCall. The crypto service owns the full read-hash-check
pipeline.

Assisted-by: Claude
Names the five crates and which two are kernel-tagged, so the protocol path
builds and tests on the host with a loopback, the way services/i2c splits.
Adds the header layout and the error vocabulary the op table implied but
never wrote down.

Records the blocking rule: the ops are ServiceCalls, and a blocking
channel_transact is the alternative only with a timeout sized against the
boot watchdogs, since a transact in flight delays every one of them. The
diagram arrows said channel_transact and now match.

Fixes the nudge citation. The level-triggered USER signal is i2c's, not the
MCTP server's; drive_pending is a deferred channel_respond, not a wake.

Assisted-by: Claude
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch 2 times, most recently from 4242d48 to f1b64a2 Compare September 14, 2026 18:59
Bumping the security revision at ActivationPending spends the anti-rollback
floor before any boot is judged, which leaves the superseded image below the
floor and makes a trial-boot revert impossible.

DSP0267 1.3.0 separates the two. The UA sets Security Revision Number Delayed
Update on UpdateComponent and sends UpdateSecurityRevision (0x22, section
12.19) once it is satisfied with the running image. Both sequences now end
with that phase, gated on a confirmed trial. The orchestrator still owns the
floor write; GrantSvnCommit only tells the FD it may answer the UA.

Replaces the before-or-after-Activate open question with the two the split
raises: what to do when the UA omits the delayed-update flag, and when it
never sends 0x22 at all.

Assisted-by: Claude
@chrysh
chrysh force-pushed the docs-pldm-server-ipc branch from f1b64a2 to 493bd7d Compare September 14, 2026 20:21
Adds the containment section the design assumed but never wrote down. The FD
holds no flash handle, so the device server enforces the staging window and the
SMC write filter backs it in hardware. Both cover staging only: apply and
activate are FD-initiated ServiceCalls gated inside the FD's own callback, so a
compromised FD can still reach them. That gap is stated, not closed.

Answers the FD-death open question with restart-and-nudge. pw_kernel has no
peer-closed signal, and channel objects are reset only when a supervisor joins
the dead process, so the orchestrator learns about the death from the restarted
FD reporting Idle while it still believes an update is running. Two gaps stay
open: there is no supervisor in this tree, and an FD that never restarts still
needs a backstop timer above worst-case transfer plus FD_T1.

Adds activation reporting: the response means accepted, the outcome comes from
GetStatus and GetFirmwareParameters, and a failed activation leaves the old
image booting. Also records that the FD answers ALREADY_IN_UPDATE_MODE from its
own state machine, without the orchestrator.

Corrects who clears the nudge. The doc said the orchestrator lowers the signal,
which it cannot: object_set_peer_user_signal acts on the peer, so the
orchestrator has no call to clear its own. The FD clears when it answers
QueryStatus and raises again on the next change.

Assisted-by: Claude
A crypto service that never answers, Status::Error being listed as a nudge
trigger but never defined, FdOps::cancel_update_component appearing in no
diagram, verify failure detail that stops at a bool, and the missing completion
code for refusing a non-delayed SVN update.

The refusal one carries a timing constraint worth stating with it: the FD
answers UpdateComponent before the orchestrator ever sees the offer, so a policy
refusal cannot wait for the orchestrator.

Assisted-by: Claude
pldm-lib calls both callbacks repeatedly through fd_progress, and the grant
gate lives inside that poll. Verify showed it in two notes, apply not at all.
Both polls are loop boxes now, in each sequence.

Assisted-by: Claude
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.

2 participants