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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ the endpoint or its session can explicitly reclaim both backends through
of caches and acceptance that a locally staged receive will be discarded.
Invalidation alone does not establish quiescence.

`transport::notification::try_send_and_notify` publishes first and then invokes
one platform-supplied doorbell operation. A notification failure is returned as
`Published::NotificationFailed`, so the already-published message is not retried
by mistake. The platform must still qualify the peer's ACK/drain/recheck and
cache ordering; a pending or successful doorbell is not proof of delivery.
This helper does not implement interrupt handling or asynchronous wakeup.

Run independent host tests with `cargo test --locked --target
x86_64-unknown-linux-gnu --features defmt`; the RISC-V check uses the project's
pinned nightly and `-Zbuild-std=core,alloc`.
17 changes: 17 additions & 0 deletions src/services/random_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,23 @@ impl<'a> RandomBufferImage<'a> {
pub fn bytes(&self) -> &[u8] {
self.bytes
}
/// Process a locally copied IPC18 message against this image's registration.
///
/// The caller must route only messages from the authorized firmware peer;
/// a message ID or matching numeric address does not authenticate it. This
/// function neither receives from a queue nor acknowledges a notification.
/// Decode and bounds failures do not call `fill` or change the destination.
/// On a source failure the message may already have been consumed by the
/// caller's endpoint; retry and notification policy belong to the platform.
pub fn handle_message<E>(
&mut self,
message: Message<'_>,
scratch: &mut [u8],
fill: impl FnOnce(&mut [u8]) -> Result<(), E>,
) -> Result<(), ServiceError<E>> {
let request = UpdateRequest::decode(message, self.region).map_err(ServiceError::Request)?;
self.update(request, scratch, fill)
}
/// Fill the selected range transactionally using a caller-provided entropy
/// operation. The platform supplies its own attempt budget and delay policy.
/// No allocation, global interrupt masking, IPC ACK or notification occurs.
Expand Down
57 changes: 57 additions & 0 deletions src/services/random_buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,63 @@ fn message(bytes: &[u8]) -> Message<'_> {
Message::new(MessageId::UPDATE_SHARED_RANDOM_NUMBER, Priority::Low, bytes).unwrap()
}

#[test]
fn service_handles_only_registered_requests_and_preserves_image_on_failure() {
use core::cell::Cell;

let region = BufferRegion::try_from_control_bounds(0xc1000, 0xc1008).unwrap();
let mut bytes = [0xa5; 8];
let mut image = RandomBufferImage::attach(region, &mut bytes).unwrap();
let mut scratch = [0; 8];
let calls = Cell::new(0);
let valid = payload(0, 0xc1000, 8);
let wrong_id =
Message::new(MessageId::try_from_raw(0).unwrap(), Priority::Low, &valid).unwrap();
assert_eq!(
image.handle_message(wrong_id, &mut scratch, |_| {
calls.set(calls.get() + 1);
Ok::<_, ()>(())
}),
Err(ServiceError::Request(RequestError::WrongMessage))
);
assert_eq!(
image.handle_message(message(&valid), &mut scratch[..7], |_| {
calls.set(calls.get() + 1);
Ok::<_, ()>(())
}),
Err(ServiceError::ScratchTooSmall)
);
let wrong = payload(0, 0xc1001, 8);
assert_eq!(
image.handle_message(message(&wrong), &mut scratch, |_| {
calls.set(calls.get() + 1);
Ok::<_, ()>(())
}),
Err(ServiceError::Request(RequestError::UnregisteredRegion))
);
let second_half = payload(2, 0xc1000, 8);
assert_eq!(
image.handle_message(message(&second_half), &mut scratch, |out| {
calls.set(calls.get() + 1);
out.fill(0x55);
Err("source failed")
}),
Err(ServiceError::Random("source failed"))
);
assert_eq!(image.bytes(), &[0xa5; 8]);
assert_eq!(calls.get(), 1);
image
.handle_message(message(&second_half), &mut scratch, |out| {
out.fill(0x5a);
Ok::<_, ()>(())
})
.unwrap();
assert_eq!(
image.bytes(),
&[0xa5, 0xa5, 0xa5, 0xa5, 0x5a, 0x5a, 0x5a, 0x5a]
);
}

#[test]
fn explicit_bounds_reject_wrap_and_do_not_assume_ct_sram() {
for (start, end) in [(0, 0), (4, 3), (0, u32::MAX), (0xefffffff, 0xf0000000)] {
Expand Down
1 change: 1 addition & 0 deletions src/transport/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Experimental platform transports. No interrupt or cache maintenance is implied.
pub mod ws53;

pub mod notification;
/// Experimental SDK shared-memory queues.
pub mod spsc;
121 changes: 121 additions & 0 deletions src/transport/notification.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
//! Publish-before-notify coordination without assuming a particular doorbell.
//!
//! The platform owns notification resources, cache handoff, ordering, and the
//! peer's drain/recheck protocol. A pending notification is not proof that a
//! newly published message will be observed. This module does not establish
//! peer liveness, wakeup delivery, or ownership transfer.

use crate::endpoint::{Endpoint, SendError};

/// A message was published, followed by one notification attempt.
/// Neither variant promises that the peer received or processed the message.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Published<N> {
/// The platform notification operation returned success.
Notified,
/// The operation failed or reported an outstanding notification. The
/// message is already published; do not retry the message blindly.
NotificationFailed(N),
}

/// Publish one complete message and then make exactly one notification attempt.
///
/// `Err` comes solely from the endpoint; the callback is not called on error.
/// In particular, `SendError::Indeterminate` can still mean that publication
/// occurred, so the platform must reconcile that case without blindly resending.
/// `Ok(Published::NotificationFailed(..))` means publication succeeded even
/// though notification did not: callers may retry the notification separately
/// only under their platform/peer protocol, never republish automatically.
///
/// The callback may use the platform's explicitly handed-over HAL doorbell.
/// A successful callback or an already-pending indication does not resolve the
/// race between a peer's ACK and queue inspection. The platform must establish a
/// proven drain/recheck or equivalent protocol, including barriers and cache
/// maintenance; this helper does not install an ISR or provide async waits.
pub fn try_send_and_notify<E: Endpoint, N>(
endpoint: &mut E,
message: &[u8],
notify: impl FnOnce() -> Result<(), N>,
) -> Result<Published<N>, SendError> {
endpoint.try_send(message)?;
Ok(match notify() {
Ok(()) => Published::Notified,
Err(error) => Published::NotificationFailed(error),
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::endpoint::ReceiveError;
use core::cell::Cell;

struct Fake<'a> {
published: &'a Cell<usize>,
result: Result<(), SendError>,
}
impl Endpoint for Fake<'_> {
fn try_send(&mut self, _: &[u8]) -> Result<(), SendError> {
// Simulate the "published but not known to caller" branch of an
// indeterminate outcome; the helper must not reinterpret it.
if self.result.is_ok() || self.result == Err(SendError::Indeterminate) {
self.published.set(self.published.get() + 1);
}
self.result
}
fn try_receive(&mut self, _: &mut [u8]) -> Result<Option<usize>, ReceiveError> {
Ok(None)
}
}

#[test]
fn publish_precedes_notification_and_failure_does_not_republish() {
let published = Cell::new(0);
let attempts = Cell::new(0);
let mut endpoint = Fake {
published: &published,
result: Ok(()),
};
let result = try_send_and_notify(&mut endpoint, b"message", || {
assert_eq!(published.get(), 1);
attempts.set(attempts.get() + 1);
Err("pending")
});
assert_eq!(result, Ok(Published::NotificationFailed("pending")));
assert_eq!(published.get(), 1);
assert_eq!(attempts.get(), 1);
let result = try_send_and_notify(&mut endpoint, b"next", || {
assert_eq!(published.get(), 2);
attempts.set(attempts.get() + 1);
Ok::<_, &str>(())
});
assert_eq!(result, Ok(Published::Notified));
assert_eq!(published.get(), 2);
assert_eq!(attempts.get(), 2);
}

#[test]
fn failed_or_indeterminate_publication_never_invokes_notification() {
for error in [
SendError::Full,
SendError::Corrupt,
SendError::Invalidated,
SendError::Indeterminate,
] {
let published = Cell::new(0);
let mut endpoint = Fake {
published: &published,
result: Err(error),
};
let result = try_send_and_notify(&mut endpoint, b"message", || -> Result<(), ()> {
panic!("notification must not run on publication error")
});
assert_eq!(result, Err(error));
assert_eq!(
published.get(),
usize::from(error == SendError::Indeterminate)
);
}
}
}
38 changes: 38 additions & 0 deletions src/transport/spsc_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,3 +500,41 @@ fn invalidated_session_can_reclaim_both_quiescent_queues_with_staged_message() {
assert_eq!(tx_drops.load(AtomicOrdering::SeqCst), 1);
assert_eq!(rx_drops.load(AtomicOrdering::SeqCst), 1);
}

#[test]
fn notified_publication_preserves_published_frame_when_doorbell_is_pending() {
use crate::protocol::FirmwareProtocol;
use crate::transport::notification::{Published, try_send_and_notify};
use crate::transport::ws53::{FrameAuthority, PollingEndpoint};
struct TestPolicy;
// SAFETY: this peer only copies the inline test frame, with no pointers or
// service side effects; it has no external resource or lifetime needs.
unsafe impl FrameAuthority for TestPolicy {
fn permits(&self, _: Message<'_>) -> bool {
true
}
}
let tx_memory = prepared(16, true);
let rx_memory = prepared(16, false);
let peer_tx = tx_memory.bytes.clone();
let tx = unsafe { Transmit::from_prepared(tx_memory, Channel::Channel1) }.unwrap();
let rx = unsafe { Receive::from_prepared(rx_memory, Channel::Channel1) }.unwrap();
let mut endpoint = unsafe {
PollingEndpoint::from_prepared(tx, rx, TestPolicy, FirmwareProtocol::Sdk163d9150)
};
let frame = [1, 0, 2, 0, 0xaa, 0xbb];
let result = try_send_and_notify(&mut endpoint, &frame, || {
assert_eq!(read16(&peer_tx, 6), frame.len() as u16);
Err("doorbell pending")
});
assert_eq!(
result,
Ok(Published::NotificationFailed("doorbell pending"))
);
assert_eq!(sdk_pop(&peer_tx, 16).unwrap(), frame);
assert!(sdk_pop(&peer_tx, 16).is_none());
// SAFETY: synthetic peer is stopped and all host references/observations
// are complete; no staged message or external payload remains.
let (tx, rx, _) = unsafe { endpoint.reclaim_after_peer_quiescent() };
drop((tx, rx));
}
Loading