Pldm improvement - #379
Conversation
d16c6b8 to
539fea4
Compare
539fea4 to
4006f4c
Compare
9b05e70 to
96562d5
Compare
|
A bit AI-y, but the problems it raises seem legit. Two regression tests for 1. Responder must filter by UA EID. Right now /// Security regression: the FD must only act on commands from the Update
/// Agent EID it was told to serve (`run_terminus`'s `remote_eid`), not from
/// any endpoint that happens to be on the bus.
#[test]
fn responder_ignores_commands_from_unexpected_eid() {
const ATTACKER_EID: u8 = 99;
let fd_ops = MockFdOps {
component_accepted: Cell::new(false),
download_bytes_received: Cell::new(0),
verified: Cell::new(false),
applied: Cell::new(false),
activated: Cell::new(false),
};
let ua_to_fd_packets = RefCell::new(Vec::new());
let ua_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(
Eid(UA_EID),
0,
BufferSender { packets: &ua_to_fd_packets },
));
let attacker_to_fd_packets = RefCell::new(Vec::new());
let attacker_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(
Eid(ATTACKER_EID),
0,
BufferSender { packets: &attacker_to_fd_packets },
));
let fd_to_ua_packets = RefCell::new(Vec::new());
let fd_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(
Eid(FD_EID),
0,
BufferSender { packets: &fd_to_ua_packets },
));
// Responder pump delivers BOTH queues, so whichever is pending is seen.
let responder_client = DirectClientWithPump::new(&fd_server, || {
transfer(&ua_to_fd_packets, &mut fd_server.borrow_mut());
ua_to_fd_packets.borrow_mut().clear();
transfer(&attacker_to_fd_packets, &mut fd_server.borrow_mut());
attacker_to_fd_packets.borrow_mut().clear();
});
let responder_transport = MctpPldmTransport::new(responder_client);
let requester_transport =
MctpPldmTransport::new(DirectClientWithPump::new(&fd_server, || {}));
let mut fd = FirmwareDevice::init(
&fd_ops,
&pldm_interface::config::PLDM_PROTOCOL_CAPABILITIES,
responder_transport,
requester_transport,
);
let mut fd_buf = [0u8; 1024];
let mut run_fd_once =
|| match fd.run_terminus(UA_EID, &mut fd_buf, TIMEOUT_MILLIS, TIMEOUT_MILLIS) {
Ok(()) => {}
Err(PldmServiceError::Mctp(e)) if e.is_timeout() => {}
Err(e) => panic!("firmware device failed: {e:?}"),
};
let mut buf = [0u8; 1024];
// Attacker (EID 99) sends SetTid(0x99).
let set_tid = SetTidRequest::new(0, PldmMsgType::Request, 0x99);
let req_len = set_tid.encode(&mut buf).expect("encode attacker SetTid");
let attacker_handle = attacker_server
.borrow_mut()
.req(FD_EID)
.expect("attacker allocate request handle to FD");
attacker_server
.borrow_mut()
.send(Some(attacker_handle), 0x01, None, None, false, &buf[..req_len])
.expect("attacker send SetTid");
run_fd_once();
fd_to_ua_packets.borrow_mut().clear();
// Legitimate UA (EID 8) queries the TID.
let get_tid = GetTidRequest::new(1, PldmMsgType::Request);
let req_len = get_tid.encode(&mut buf).expect("encode UA GetTid");
let ua_handle = ua_server
.borrow_mut()
.req(FD_EID)
.expect("UA allocate request handle to FD");
ua_server
.borrow_mut()
.send(Some(ua_handle), 0x01, None, None, false, &buf[..req_len])
.expect("UA send GetTid");
run_fd_once();
transfer(&fd_to_ua_packets, &mut ua_server.borrow_mut());
fd_to_ua_packets.borrow_mut().clear();
let mut resp = [0u8; 1024];
let meta = ua_server
.borrow_mut()
.try_recv(ua_handle, &mut resp)
.expect("GetTid response should be available");
assert!(meta.payload_size >= 5, "GetTid response too short");
assert_ne!(
resp[4], 0x99,
"FD acted on a SetTid from an unexpected EID ({ATTACKER_EID}); \
the responder path must filter by the UA EID passed to run_terminus"
);
}2. Pin the /// Contract regression: an idle `run_terminus` with a non-zero timeout must
/// exit with `Mctp(TimedOut)` rather than block or loop forever. Both host
/// tests use this as their "queue drained / done" signal.
#[test]
fn run_terminus_exits_with_timeout_when_idle() {
let fd_ops = MockFdOps {
component_accepted: Cell::new(false),
download_bytes_received: Cell::new(0),
verified: Cell::new(false),
applied: Cell::new(false),
activated: Cell::new(false),
};
let fd_to_ua_packets = RefCell::new(Vec::new());
let fd_server: RefCell<Server<_, 16>> = RefCell::new(Server::new(
Eid(FD_EID),
0,
BufferSender { packets: &fd_to_ua_packets },
));
let responder_transport =
MctpPldmTransport::new(DirectClientWithPump::new(&fd_server, || {}));
let requester_transport =
MctpPldmTransport::new(DirectClientWithPump::new(&fd_server, || {}));
let mut fd = FirmwareDevice::init(
&fd_ops,
&pldm_interface::config::PLDM_PROTOCOL_CAPABILITIES,
responder_transport,
requester_transport,
);
let mut fd_buf = [0u8; 1024];
match fd.run_terminus(UA_EID, &mut fd_buf, 5, 5) {
Err(PldmServiceError::Mctp(e)) if e.is_timeout() => {}
other => panic!(
"idle run_terminus should exit with Mctp(TimedOut); got {other:?}"
),
}
} |
|
Two more regression guards for the EID fix. These aren't new findings — they lock in that the filter covers paths the current I also collapsed the per-test MCTP wiring into one /// Stand up UA (EID 8), a rogue endpoint (EID 99) and the FD (EID 42) over
/// in-memory MCTP, then run `body`. `body` gets a `send(src_eid, pldm_bytes)`
/// closure that delivers one command, runs the terminus once, and returns the
/// response the *sender* receives — `None` if the FD stayed silent.
fn with_bus(body: impl FnOnce(&MockFdOps, &mut dyn FnMut(u8, &[u8]) -> Option<Vec<u8>>)) {
let fd_ops = MockFdOps::default();
let (ua_q, atk_q, fd_q) = (
RefCell::new(Vec::new()),
RefCell::new(Vec::new()),
RefCell::new(Vec::new()),
);
let ua = RefCell::new(Server::<_, 16>::new(Eid(UA_EID), 0, BufferSender { packets: &ua_q }));
let atk =
RefCell::new(Server::<_, 16>::new(Eid(ATTACKER_EID), 0, BufferSender { packets: &atk_q }));
let fd_srv = RefCell::new(Server::<_, 16>::new(Eid(FD_EID), 0, BufferSender { packets: &fd_q }));
// One responder pump drains both senders' queues into the FD before recv.
let responder = DirectClientWithPump::new(&fd_srv, || {
transfer(&ua_q, &mut fd_srv.borrow_mut());
ua_q.borrow_mut().clear();
transfer(&atk_q, &mut fd_srv.borrow_mut());
atk_q.borrow_mut().clear();
});
let requester = DirectClientWithPump::new(&fd_srv, || {});
let mut fd = FirmwareDevice::init(
&fd_ops,
&pldm_interface::config::PLDM_PROTOCOL_CAPABILITIES,
MctpPldmTransport::new(responder),
MctpPldmTransport::new(requester),
);
let mut fd_buf = [0u8; 1024];
let mut send = |src_eid: u8, payload: &[u8]| -> Option<Vec<u8>> {
let sender = if src_eid == UA_EID { &ua } else { &atk };
let h = sender.borrow_mut().req(FD_EID).expect("req handle");
sender
.borrow_mut()
.send(Some(h), 0x01, None, None, false, payload)
.expect("send");
let _ = fd.run_terminus(UA_EID, &mut fd_buf, TIMEOUT_MILLIS, TIMEOUT_MILLIS);
transfer(&fd_q, &mut sender.borrow_mut());
fd_q.borrow_mut().clear();
let mut resp = [0u8; 1024];
sender
.borrow_mut()
.try_recv(h, &mut resp)
.map(|m| resp[..m.payload_size].to_vec())
};
body(&fd_ops, &mut send);
}
/// #1 — a rogue endpoint must not drive the firmware-update state machine.
#[test]
fn rogue_eid_cannot_drive_update() {
with_bus(|fd_ops, send| {
send(ATTACKER_EID, &request_update()); // dropped, no response expected
let status = send(UA_EID, &get_status()).expect("UA gets a status reply");
let st = GetStatusResponse::decode(&status).expect("decode status");
assert_eq!(
st.current_state,
FirmwareDeviceState::Idle as u8,
"rogue RequestUpdate advanced the FD state machine"
);
assert!(!fd_ops.component_accepted.get());
});
}
/// #2 (bonus) — the FD must not disclose state to a rogue endpoint (no response).
#[test]
fn rogue_eid_gets_no_response() {
with_bus(|_fd_ops, send| {
assert!(
send(ATTACKER_EID, &get_status()).is_none(),
"FD replied to a GetStatus from an unexpected EID"
);
});
}Encode helpers + minimal
|
|
Thanks, |
b266c9e to
0217b29
Compare
…assing remote_eid. Unexpected EID tests use set/get TID to verify.
0217b29 to
8bb7522
Compare
a788402 to
def9a2e
Compare
… contains the terminus loop and is wrapped in run_terminus. run_terminus while match on error and completed.
@CourtneyDrant Not currently, it is meant a regression guard for future code changes. |
|
| requester_timeout_millis, | ||
| )?; | ||
| let resp_total_len = | ||
| // resp_len.checked_add(1).ok_or(PldmServiceError::Overflow)?; |
There was a problem hiding this comment.
Leftover commented-out line — please remove.
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`PldmServiceError::Overflow`] if `buf` is too small. |
There was a problem hiding this comment.
This still references PldmServiceError::Overflow, which no longer exists — respond_once's Errors section was updated, this one was missed.
This pull request was done to improve on a previous design of the pldm-service. It has one process which is the Firmware Device. Firmware Device is the OpenProt Pldm terminus for FW update. It will loop to receive messages from the UA and when ready, will request data from the UA. The loop ensures that between each request the terminus will check for UA requests to receive status or cancel an update.