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, |
a788402 to
def9a2e
Compare
@CourtneyDrant Not currently, it is meant a regression guard for future code changes. |
|
2111867 to
19fcf2a
Compare
|
Can we move any new regression requests to issues and finish this PR? |
chrysh
left a comment
There was a problem hiding this comment.
Yes, let's finish this PR. The RunTerminusResult idle-timeout mapping (timeout -> Completed so callers can drop the is_timeout() special case) can move to a follow-up issue — feel free to open it, or I will.
One small thing to fix here: README.md line 97 still references FD_IPC_MAX_MSG; the constant is now FD_MAX_MSG.
Approving already so this isn't blocked on me.
|
Filed the timeout-mapping follow-up as #429. |
leongross
left a comment
There was a problem hiding this comment.
Resolve the merge conflict and then we can get this merged.
19fcf2a to
5804629
Compare
access from update agent.
…assing remote_eid. Unexpected EID tests use set/get TID to verify.
… contains the terminus loop and is wrapped in run_terminus. run_terminus while match on error and completed.
5804629 to
7764ab2
Compare
|
The merge conflicts are in MODULE.bazel.lock - can we deploy the Bazel conflict autoresolver? |
That looks very promising, good catch! |
7764ab2 to
ae106d8
Compare
ae106d8 to
79f2047
Compare
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.