Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions services/orchestrator/adapters/pldm/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ rust_library(
edition = "2024",
visibility = ["//visibility:public"],
deps = [
"//services/orchestrator/sm:orchestrator_sm",
"//services/pldm:pldm_service",
"//services/pldm/notify-api:pldm_notify_api",
],
)

Expand Down
162 changes: 103 additions & 59 deletions services/orchestrator/adapters/pldm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,88 +1,132 @@
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0

//! PLDM-backed adapter for the Boot Orchestrator's update-request input.
//! Orchestrator side of the pldm-notify wire: decodes one notification from the
//! PLDM service and says what it means in the orchestrator's terms.
//!
//! [`UpdateRequestLatch`] binds the PLDM firmware-device service's
//! [`FdEventSink`] seam to the orchestrator's
//! [`Event::UpdateRequest`]: the PLDM run loop notifies the latch when the
//! Update Agent's `RequestUpdate` is accepted, and the orchestrator run loop
//! drains it with [`take`](UpdateRequestLatch::take). This crate depends on
//! both stacks by design — the PLDM service stays orchestrator-free and the
//! orchestrator stays transport-free, the same rule that keeps HAL adapters
//! out of `orchestrator-capabilities`.
//! Nothing here reaches the state machine. The notification arrives before the
//! FD has accepted the UA's request, so there is nothing to authenticate yet;
//! the machine hears about an update later, when the candidate is complete.
//!
//! [`dispatch`] is pure, no IPC and no globals, so it tests on the host. The
//! kernel wait-and-respond loop is elsewhere, and it answers before it acts on
//! the notification, since `channel_respond` has to be prompt.
//!
//! Depends on the wire crate only, never on the PLDM stack and never on
//! orchestrator-sm, the same rule the orchestrator's HAL adapters follow.

#![cfg_attr(not(test), no_std)]
#![forbid(unsafe_code)]
#![warn(missing_docs)]

use openprot_orchestrator_sm::Event;
use openprot_pldm_service::firmware_device::{FdEvent, FdEventSink};
use openprot_pldm_notify_api::{Request, Response};

/// Latches an accepted PLDM `RequestUpdate` until the orchestrator run loop
/// drains it as [`Event::UpdateRequest`].
/// What the PLDM service reported, in the orchestrator's words.
///
/// A `bool` latch, not a counter: the FD rejects a second `RequestUpdate`
/// while an update is in progress (`ALREADY_IN_UPDATE_MODE`), so at most one
/// accepted request can be outstanding per update cycle. Should a completed
/// or cancelled cycle admit a new `RequestUpdate` before the previous latch
/// is drained, the two coalesce into one [`Event::UpdateRequest`] — which is
/// what the state machine would do anyway (an update already being handled
/// defers further requests).
#[derive(Default)]
pub struct UpdateRequestLatch {
pending: bool,
}

impl UpdateRequestLatch {
/// A latch with nothing pending.
pub const fn new() -> Self {
Self { pending: false }
}

/// Drain the latch: [`Event::UpdateRequest`] if a `RequestUpdate` was
/// accepted since the last call, else `None`.
pub fn take(&mut self) -> Option<Event> {
self.pending.then(|| {
self.pending = false;
Event::UpdateRequest
})
}
/// Translating here is the point of this crate: [`NotifyOp`] is PLDM's
/// vocabulary across a process boundary, and letting it reach the run loop is
/// the coupling the adapter exists to prevent.
///
/// [`NotifyOp`]: openprot_pldm_notify_api::NotifyOp
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Notification {
/// A UA sent a `RequestUpdate`. The orchestrator's verdict decides whether
/// the FD accepts; no firmware bytes have moved yet.
UpdateRequested,
}

/// Latches [`FdEvent::UpdateRequested`]; other FD lifecycle events have no
/// orchestrator mapping yet and are dropped here by design.
impl FdEventSink for UpdateRequestLatch {
fn notify(&mut self, event: FdEvent) {
if matches!(event, FdEvent::UpdateRequested) {
self.pending = true;
}
/// Decodes one notification, encodes an accept or reject, and returns what
/// was reported.
///
/// `accept` is the caller's verdict: true to accept the request, false to
/// reject it. The Notification is returned either way so the caller knows
/// what was requested (for logging on reject, watchdog arming on accept).
///
/// Returns how many bytes of `response` are the answer. `response` must be at
/// least [`MAX_RESPONSE_SIZE`](openprot_pldm_notify_api::MAX_RESPONSE_SIZE)
/// bytes; a shorter one gets nothing written and a return of 0, which the
/// caller must not put on the wire.
///
/// Never panics. A frame that does not decode gets nothing written, a return
/// of 0, and nothing reported.
pub fn dispatch(
request: &[u8],
response: &mut [u8],
accept: bool,
) -> (usize, Option<Notification>) {
let notification = match Request::decode(request) {
Ok(Request::UpdateRequested) => Notification::UpdateRequested,
// non_exhaustive: a future op this build doesn't know, or a bad frame.
// No answer, so the UA's transact times out and retries.
Ok(_) | Err(_) => return (0, None),
};
let answer = if accept {
Response::Accepted
} else {
Response::Rejected
};
match answer.encode(response) {
Ok(len) => (len, Some(notification)),
Err(_) => (0, None),
}
}

#[cfg(test)]
mod tests {
use super::*;
use openprot_pldm_notify_api::{NotifyOp, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE};

fn round_trip(request: Request, accept: bool) -> (Option<Notification>, Option<Response>) {
let mut out = [0u8; MAX_REQUEST_SIZE];
let len = request.encode(&mut out).unwrap();
let mut back = [0u8; MAX_RESPONSE_SIZE];
let (answer_len, notification) = dispatch(&out[..len], &mut back, accept);
(notification, Response::decode(&back[..answer_len]).ok())
}

#[test]
fn empty_latch_yields_nothing() {
assert_eq!(UpdateRequestLatch::new().take(), None);
fn an_update_request_that_the_caller_accepts_is_reported_and_confirmed() {
assert_eq!(
round_trip(Request::UpdateRequested, true),
(
Some(Notification::UpdateRequested),
Some(Response::Accepted)
)
);
}

#[test]
fn accepted_request_yields_one_event() {
let mut latch = UpdateRequestLatch::new();
latch.notify(FdEvent::UpdateRequested);
assert_eq!(latch.take(), Some(Event::UpdateRequest));
assert_eq!(latch.take(), None, "a drained latch must not re-fire");
fn a_rejected_request_update_is_still_reported() {
assert_eq!(
round_trip(Request::UpdateRequested, false),
(
Some(Notification::UpdateRequested),
Some(Response::Rejected)
)
);
}

#[test]
fn a_frame_that_does_not_decode_gets_no_answer_and_reports_nothing() {
let mut back = [0xFFu8; MAX_RESPONSE_SIZE];
for bad in [
&[0u8][..],
&[0xFF, 0, 0, 0][..],
&[NotifyOp::UpdateRequested as u8, 0, 1, 0][..],
&[NotifyOp::UpdateRequested as u8, 1, 0, 0][..],
] {
assert_eq!(dispatch(bad, &mut back, true), (0, None));
assert!(back.iter().all(|&b| b == 0xFF));
}
}

#[test]
fn undrained_notifications_coalesce() {
let mut latch = UpdateRequestLatch::new();
latch.notify(FdEvent::UpdateRequested);
latch.notify(FdEvent::UpdateRequested);
assert_eq!(latch.take(), Some(Event::UpdateRequest));
assert_eq!(latch.take(), None);
fn a_response_buffer_below_the_minimum_gets_nothing() {
let mut out = [0u8; MAX_REQUEST_SIZE];
let len = Request::UpdateRequested.encode(&mut out).unwrap();
let mut back = [0xFFu8; MAX_RESPONSE_SIZE - 1];
assert_eq!(dispatch(&out[..len], &mut back, true), (0, None));
assert!(back.iter().all(|&b| b == 0xFF));
}
}
2 changes: 0 additions & 2 deletions services/pldm/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ rust_test(
":pldm_service",
"//services/mctp/api:mctp_api",
"//services/mctp/server:mctp_server_lib",
"//services/orchestrator/adapters/pldm:orchestrator_pldm_adapter",
"//services/orchestrator/sm:orchestrator_sm",
"@rust_crates//:mctp",
"@rust_crates//:mctp-lib",
"@rust_crates//:pldm-common",
Expand Down
21 changes: 21 additions & 0 deletions services/pldm/notify-api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Licensed under the Apache-2.0 license
# SPDX-License-Identifier: Apache-2.0

load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")

# Contract only: both processes decode this, so it carries no transport and no
# state and builds on the host. Untagged, so a kernel binary links it as-is.
rust_library(
name = "pldm_notify_api",
srcs = ["src/lib.rs"],
crate_name = "openprot_pldm_notify_api",
edition = "2024",
visibility = ["//visibility:public"],
deps = ["@rust_crates//:zerocopy"],
)

# Host wire tests: build on the host platform, no kernel/QEMU.
rust_test(
name = "pldm_notify_api_test",
crate = ":pldm_notify_api",
)
30 changes: 30 additions & 0 deletions services/pldm/notify-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# pldm-notify-api

The wire the PLDM service uses to tell the orchestrator that something has
happened. Contract only, no transport and no state, so both processes depend on
it and it builds and tests on the host.

PLDM initiates and the orchestrator handles, because PLDM is where these things
are known and the orchestrator's loop already waits on several objects.

One op today, `NotifyOp::UpdateRequested`, and two answers, `Response::Accepted`
and `Response::Rejected`. All carry no payload, so a frame is 4 bytes each way:
`[op][len][reserved:2]` in, `[code][len][reserved:2]` back.

To add a notification, give it the next `NotifyOp` discriminant. If it carries
data, put the length in `len` and raise `MAX_REQUEST_SIZE`; `len` is in the
header now so that does not change the frame format. The opcode space is flat
and nothing in it is specific to firmware update, so any PLDM type can take a
range. Reserved fields are zero and the decoder rejects a frame that sets them,
so they can be given a meaning later.

`Response::Accepted` lets the transfer proceed. `Response::Rejected` tells the
PLDM service that the orchestrator will not act on this request (already
updating, recovering, locked, or policy). The verdict is the caller's, not the
adapter's.

The orchestrator-side handler is `services/orchestrator/adapters/pldm`.

Run the tests with:

bazel test //services/pldm/notify-api:pldm_notify_api_test
Loading