diff --git a/target/ast10x0/tests/util_ipc/async_transaction/BUILD.bazel b/target/ast10x0/tests/util_ipc/async_transaction/BUILD.bazel new file mode 100644 index 000000000..b2a0e272c --- /dev/null +++ b/target/ast10x0/tests/util_ipc/async_transaction/BUILD.bazel @@ -0,0 +1,124 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +# ── System configuration ─────────────────────────────────────────────────────── + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +# ── Kernel image ─────────────────────────────────────────────────────────────── + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +# ── Handler app ───────────────────────────────────────────────────────────────── +# Exercises util_ipc::IpcHandler; increments the byte it's sent. + +rust_app( + name = "handler", + srcs = ["handler_main.rs"], + codegen_crate_name = "app_handler", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + ], +) + +# ── Initiator app ─────────────────────────────────────────────────────────────── +# Exercises util_ipc::IpcInitiator and util_ipc::AsyncTransaction; calls +# debug_shutdown(Ok|Err) to report the result. + +rust_app( + name = "initiator", + srcs = ["initiator_main.rs"], + codegen_crate_name = "app_initiator", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//util/ipc", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +# ── System image ─────────────────────────────────────────────────────────────── + +system_image( + name = "async_transaction_image", + apps = [ + ":handler", + ":initiator", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +# ── Test target ──────────────────────────────────────────────────────────────── +# Run with: +# bazel test --config=virt_ast10x0 //target/ast10x0/tests/util_ipc/async_transaction:async_transaction_qemu_test + +system_image_test( + name = "async_transaction_qemu_test", + image = ":async_transaction_image", + tags = ["qemu_only"], + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":async_transaction_image", + tags = ["kernel"], +) diff --git a/target/ast10x0/tests/util_ipc/async_transaction/handler_main.rs b/target/ast10x0/tests/util_ipc/async_transaction/handler_main.rs new file mode 100644 index 000000000..a9d778316 --- /dev/null +++ b/target/ast10x0/tests/util_ipc/async_transaction/handler_main.rs @@ -0,0 +1,62 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Handler side of the util/ipc AsyncTransaction QEMU test. +//! +//! Exercises `util_ipc::IpcHandler`: waits for a request, reads it, and +//! responds with the request byte incremented by one. A request of +//! `GATED_REQUEST` is held until the initiator raises Signals::USER. + +#![no_main] +#![no_std] + +use app_handler::handle; +use pw_status::Error; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::Instant; +use util_ipc::{IpcHandle, IpcHandler}; + +/// Request byte that parks the handler instead of responding: it raises +/// Signals::USER on the initiator to say it is parked, then waits for the +/// initiator to raise USER back before responding. The parked signal is +/// what makes the initiator's "pending" observation deterministic. +const GATED_REQUEST: u8 = 0x40; + +#[entry] +fn entry() { + let ipc = IpcHandle::new(handle::IPC); + + loop { + if syscall::object_wait(handle::IPC, Signals::READABLE, Instant::MAX).is_err() { + continue; + } + + let mut buf = [0u8; 1]; + match ipc.read(0, &mut buf) { + Ok(1) => { + if buf[0] == GATED_REQUEST { + // Tell the initiator we are parked, wait for its + // release, then lower the parked signal again. + if ipc.set_peer_user_signal(true).is_err() + || syscall::object_wait(handle::IPC, Signals::USER, Instant::MAX).is_err() + { + continue; + } + let _ = ipc.set_peer_user_signal(false); + } + buf[0] = buf[0].wrapping_add(1); + let _ = ipc.respond(&buf); + } + // Transaction was cancelled by the initiator while we were + // waking up; go back to waiting for the next one. + Ok(_) | Err(Error::Unavailable) => continue, + Err(_) => continue, + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +} diff --git a/target/ast10x0/tests/util_ipc/async_transaction/initiator_main.rs b/target/ast10x0/tests/util_ipc/async_transaction/initiator_main.rs new file mode 100644 index 000000000..eebb38047 --- /dev/null +++ b/target/ast10x0/tests/util_ipc/async_transaction/initiator_main.rs @@ -0,0 +1,254 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Initiator side of the util/ipc AsyncTransaction QEMU test. +//! +//! Runs the cases below against `handler`, `ROUNDS` times over, and calls +//! `debug_shutdown(Ok(()))` on full pass or `debug_shutdown(Err(_))` on the +//! first failure. The kernel target writes `TEST_RESULT:PASS/FAIL` to UART. +//! Repeating catches state left behind by an earlier pass, a signal never +//! lowered above all. +//! +//! | Case | Exercises | Expect | +//! |--------------------|-------------------------------------|-------------------| +//! | blocking transact | `IpcInitiator::transact` | byte incremented | +//! | async cancel | `AsyncTransaction::start`/`cancel` | channel freed | +//! | async roundtrip | `AsyncTransaction::start`/`try_recv`| byte incremented | +//! | drop cancels | `AsyncTransaction::drop` | channel freed | +//! | double start | `start` while pending | buffers returned | +//! | try_recv early | `try_recv` before the response | `Unavailable` | + +#![no_main] +#![no_std] + +use app_initiator::handle; +use pw_status::{Error, Result}; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Instant, SystemClock}; +use util_ipc::{AsyncTransaction, IpcHandle, IpcInitiator}; + +static SEND_BUF: [u8; 1] = [0x10]; +static mut RECV_BUF: [u8; 1] = [0u8; 1]; + +/// Second receive buffer, for the case that starts a transaction while +/// another one still borrows `RECV_BUF`. +static mut RECV_BUF2: [u8; 1] = [0u8; 1]; + +/// Request byte the handler holds until Signals::USER is raised, matching +/// `GATED_REQUEST` in handler_main.rs. +static SEND_GATED: [u8; 1] = [0x40]; + +/// # Safety +/// Only called from this single-threaded app, and only while no +/// `AsyncTransaction` still holds a prior borrow of `RECV_BUF`. +unsafe fn recv_buf() -> &'static mut [u8] { + // Safety: see function doc. + unsafe { &mut *core::ptr::addr_of_mut!(RECV_BUF) } +} + +/// # Safety +/// Same contract as `recv_buf`, for the second buffer. +unsafe fn recv_buf2() -> &'static mut [u8] { + // Safety: see function doc. + unsafe { &mut *core::ptr::addr_of_mut!(RECV_BUF2) } +} + +fn test_blocking_transact() -> Result<()> { + let ipc = IpcHandle::new(handle::IPC); + let send = [0x20u8]; + let mut recv = [0u8; 1]; + + let len = ipc.transact(&send, &mut recv, Instant::MAX)?; + if len != 1 || recv[0] != 0x21 { + pw_log::error!("blocking transact: unexpected response"); + return Err(Error::Internal); + } + Ok(()) +} + +fn test_async_cancel() -> Result<()> { + let mut txn = AsyncTransaction::new(IpcHandle::new(handle::IPC)); + // Safety: no other AsyncTransaction is live right now. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + + txn.cancel()?; + if txn.is_pending() { + pw_log::error!("async cancel: still pending after cancel()"); + return Err(Error::Internal); + } + + // Verify the channel is free again. + // Safety: the previous transaction was cancelled above. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + txn.cancel()?; + Ok(()) +} + +fn test_async_roundtrip() -> Result<()> { + let mut txn = AsyncTransaction::new(IpcHandle::new(handle::IPC)); + // Safety: no other AsyncTransaction is live right now. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + + syscall::object_wait(txn.as_raw(), Signals::READABLE, Instant::MAX)?; + + let completion = txn.try_recv()?; + if completion.len != 1 || completion.buffers.recv[0] != 0x11 { + pw_log::error!("async roundtrip: unexpected response"); + return Err(Error::Internal); + } + Ok(()) +} + +/// Dropping a pending transaction has to cancel it, otherwise the channel +/// stays busy and the next start fails with `Unavailable`. +fn test_drop_cancels() -> Result<()> { + { + let mut txn = AsyncTransaction::new(IpcHandle::new(handle::IPC)); + // Safety: no other AsyncTransaction is live right now. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + // Dropped here with the transaction still pending. + } + + let mut txn = AsyncTransaction::new(IpcHandle::new(handle::IPC)); + // Safety: the drop above cancelled the transaction. The kernel clears + // its transaction slot on every path out of the cancel syscall, so it + // no longer points into RECV_BUF. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + txn.cancel()?; + Ok(()) +} + +/// A second start while one is pending fails locally and hands both +/// buffers back in the error. +fn test_double_start() -> Result<()> { + let mut txn = AsyncTransaction::new(IpcHandle::new(handle::IPC)); + // Safety: no other AsyncTransaction is live right now. + txn.start(&SEND_BUF, unsafe { recv_buf() })?; + + // RECV_BUF is still borrowed by the pending transaction, so the second + // start gets its own buffer. + // Safety: nothing else borrows RECV_BUF2. + let result = txn.start(&SEND_BUF, unsafe { recv_buf2() }); + + let outcome = match result { + Ok(()) => { + pw_log::error!("double start: second start() succeeded"); + Err(Error::Internal) + } + Err(e) if e.error != Error::FailedPrecondition => { + pw_log::error!("double start: status code {}", e.error as u32); + Err(Error::Internal) + } + Err(e) => { + let send_returned = core::ptr::eq(e.buffers.send.as_ptr(), SEND_BUF.as_ptr()); + let recv_returned = core::ptr::eq( + e.buffers.recv.as_ptr(), + core::ptr::addr_of!(RECV_BUF2).cast(), + ); + if send_returned && recv_returned { + Ok(()) + } else { + pw_log::error!("double start: buffers not returned"); + Err(Error::Internal) + } + } + }; + + txn.cancel()?; + outcome +} + +/// `try_recv` before the handler responds reports Unavailable and keeps the +/// transaction pending. The handler parks on this request and raises USER +/// to say so, so the observation is not a race: by the time USER arrives +/// the handler has read the request and is not going to respond until +/// released. +fn test_try_recv_before_response() -> Result<()> { + let ipc = IpcHandle::new(handle::IPC); + + // The handler lowers the parked signal before it responds, so USER is + // clear on entry. If it is still set, it leaked from an earlier round + // and the wait below would return without the handler having parked. + // A deadline of now makes this a poll rather than a wait. + if syscall::object_wait(ipc.as_raw(), Signals::USER, SystemClock::now()).is_ok() { + pw_log::error!("try_recv early: stale parked signal"); + return Err(Error::Internal); + } + + let mut txn = AsyncTransaction::new(ipc); + // Safety: no other AsyncTransaction is live right now. + txn.start(&SEND_GATED, unsafe { recv_buf() })?; + + // Wait for the handler to say it is parked. + syscall::object_wait(txn.as_raw(), Signals::USER, Instant::MAX)?; + + match txn.try_recv() { + Err(Error::Unavailable) => {} + Ok(_) => { + pw_log::error!("try_recv early: completed before the handler responded"); + return Err(Error::Internal); + } + Err(e) => { + pw_log::error!("try_recv early: status code {}", e as u32); + return Err(Error::Internal); + } + } + if !txn.is_pending() { + pw_log::error!("try_recv early: transaction no longer pending"); + return Err(Error::Internal); + } + + // Release the handler, then complete as usual. + ipc.set_peer_user_signal(true)?; + syscall::object_wait(txn.as_raw(), Signals::READABLE, Instant::MAX)?; + let completion = txn.try_recv()?; + ipc.set_peer_user_signal(false)?; + + if completion.len != 1 || completion.buffers.recv[0] != 0x41 { + pw_log::error!("try_recv early: unexpected response"); + return Err(Error::Internal); + } + Ok(()) +} + +/// How often the whole sequence runs. Every case leaves the channel idle +/// and both USER signals lowered, so a repeat that fails means state leaked +/// from the pass before it. +const ROUNDS: u32 = 50; + +fn run_all() -> Result<()> { + for round in 0..ROUNDS { + let ret = test_blocking_transact() + .and_then(|_| test_async_cancel()) + .and_then(|_| test_async_roundtrip()) + .and_then(|_| test_drop_cancels()) + .and_then(|_| test_double_start()) + .and_then(|_| test_try_recv_before_response()); + + if ret.is_err() { + pw_log::error!("failed in round {}", round as u32); + return ret; + } + } + Ok(()) +} + +#[entry] +fn entry() { + let ret = run_all(); + + match &ret { + Ok(()) => pw_log::info!("All test cases PASSED"), + Err(e) => pw_log::error!("FAILED: status code {}", *e as u32), + } + + let _ = syscall::debug_shutdown(ret); + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + let _ = userspace::syscall::debug_shutdown(Err(pw_status::Error::Internal)); + loop {} +} diff --git a/target/ast10x0/tests/util_ipc/async_transaction/system.json5 b/target/ast10x0/tests/util_ipc/async_transaction/system.json5 new file mode 100644 index 000000000..30e14d455 --- /dev/null +++ b/target/ast10x0/tests/util_ipc/async_transaction/system.json5 @@ -0,0 +1,78 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 util/ipc AsyncTransaction QEMU test. +// +// Two-process layout: +// +// initiator — exercises util_ipc::IpcInitiator (blocking transact) and +// util_ipc::AsyncTransaction (async start/try_recv/cancel); +// calls debug_shutdown(Ok|Err) to report the result. +// handler — util_ipc::IpcHandler read/respond loop that increments the +// single byte it's sent. +// +// Memory map (AST10x0: 768 KB SRAM, no XIP): +// 0x00000000 - 0x00000500 vector table (1280 B) +// 0x00000500 - 0x00020500 kernel flash (~128 KB) +// 0x00020500 - 0x00060500 app flash (256 KB total; 128 KB per app) +// 0x00060000 - 0x00080000 kernel RAM (128 KB) +// 0x00080000 - 0x000C0000 app RAM (256 KB total; 64 KB + 32 KB per process) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 129792, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "initiator", + flash_size_bytes: 131072, + processes: [ + { + name: "initiator_process", + ram_size_bytes: 65536, + objects: [ + { + name: "ipc", + type: "channel_initiator", + handler_process: "handler_process", + handler_object_name: "ipc", + }, + { + type: "thread", + name: "initiator_thread", + kernel_stack_size_bytes: 4096, + },], + + }, + ], + }, + { + name: "handler", + flash_size_bytes: 131072, + processes: [ + { + name: "handler_process", + ram_size_bytes: 32768, + objects: [ + { + name: "ipc", + type: "channel_handler", + }, + { + type: "thread", + name: "handler_thread", + kernel_stack_size_bytes: 4096, + },], + + }, + ], + }, + ], +} diff --git a/target/ast10x0/tests/util_ipc/async_transaction/target.rs b/target/ast10x0/tests/util_ipc/async_transaction/target.rs new file mode 100644 index 000000000..022a30193 --- /dev/null +++ b/target/ast10x0/tests/util_ipc/async_transaction/target.rs @@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Kernel target for the util/ipc AsyncTransaction QEMU test. +//! +//! Pass/fail is communicated by `initiator` calling +//! `syscall::debug_shutdown(Ok(()) | Err(...))`, which lands here and writes +//! the UART sentinel picked up by qemu_runner.py. + +#![no_std] +#![no_main] + +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 util/ipc AsyncTransaction test"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target); diff --git a/util/ipc/BUILD.bazel b/util/ipc/BUILD.bazel new file mode 100644 index 000000000..d684ccda3 --- /dev/null +++ b/util/ipc/BUILD.bazel @@ -0,0 +1,25 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +rust_library( + name = "ipc", + srcs = [ + "async_transaction.rs", + "lib.rs", + "target.rs", + ], + crate_name = "util_ipc", + edition = "2024", + tags = ["kernel"], + target_compatible_with = select({ + "@platforms//os:none": [], + "//conditions:default": ["@platforms//:incompatible"], + }), + visibility = ["//visibility:public"], + deps = [ + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_status/rust:pw_status", + ], +) diff --git a/util/ipc/async_transaction.rs b/util/ipc/async_transaction.rs new file mode 100644 index 000000000..fba7de927 --- /dev/null +++ b/util/ipc/async_transaction.rs @@ -0,0 +1,173 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Safe async IPC transaction wrapper, layered on `IpcInitiator`. +//! +//! `AsyncTransaction` wraps the unsafe `async_transact_start`/`_complete`/ +//! `_cancel` trio into a state machine (Idle / Pending) that enforces the +//! kernel's one-transaction-per-channel rule locally and keeps the +//! raw-pointer safety contract in one place. +//! +//! Buffers are `&'static` because the kernel holds raw pointers into them +//! for the duration of the transaction. If `AsyncTransaction` owned the buffers +//! inline and the struct moved after `start()`, those pointers would +//! dangle. Static borrows make soundness independent of moves and +//! `mem::forget`. +//! +//! Every exit from Pending returns both buffers to the caller so they can +//! be reused for the next transaction (the standard embedded-DMA ownership +//! pattern). + +use super::IpcInitiator; +use pw_status::{Error, Result}; + +/// Buffers lent to the kernel for the duration of one async transaction. +#[derive(Debug)] +pub struct Buffers { + pub send: &'static [u8], + pub recv: &'static mut [u8], +} + +/// One-at-a-time async IPC transaction on an `IpcInitiator`. +/// +/// Transitions: Idle -> `start()` -> Pending -> `try_recv()`/`cancel()` -> Idle. +pub struct AsyncTransaction { + handle: H, + inflight: Option, +} + +impl AsyncTransaction { + /// Wrap an initiator handle with no outstanding async transaction. + pub fn new(handle: H) -> Self { + Self { + handle, + inflight: None, + } + } + + /// The raw channel handle, e.g. to register with a WaitGroup or pass to + /// `object_wait`. + pub fn as_raw(&self) -> u32 { + self.handle.as_raw() + } + + /// Whether a transaction is in flight. + pub fn is_pending(&self) -> bool { + self.inflight.is_some() + } + + /// Start an async transaction. + /// + /// `send` is the request payload the server will read. `recv` is the + /// buffer the kernel writes the server's response into. Both must be + /// `'static` because the kernel holds raw pointers into them until + /// `try_recv` or `cancel` completes the transaction. + /// + /// On success the buffers are held until `try_recv` or `cancel` + /// returns them. On failure (already pending, or a kernel error) both + /// buffers come back in the `Err` so nothing is lost. + pub fn start( + &mut self, + send: &'static [u8], + recv: &'static mut [u8], + ) -> core::result::Result<(), StartError> { + if self.inflight.is_some() { + return Err(StartError { + error: Error::FailedPrecondition, + buffers: Buffers { send, recv }, + }); + } + + // Safety: send/recv are 'static, so the kernel's raw pointers + // stay valid regardless of what happens to `self`, and they are + // not read, written, or dropped again until try_recv/cancel. + // nosemgrep + let result = unsafe { self.handle.async_transact_start(send, recv) }; + + match result { + Ok(()) => { + self.inflight = Some(Buffers { send, recv }); + Ok(()) + } + Err(error) => Err(StartError { + error, + buffers: Buffers { send, recv }, + }), + } + } + + /// Try to complete a pending transaction. + /// + /// Returns `Ok(Completion { len, send, recv })` when the server has + /// responded. `recv[..len]` holds the response payload; the full + /// buffer is returned so it can be reused. + /// + /// Returns `Err(Error::Unavailable)` if READABLE is not set (server + /// has not responded yet); the buffers stay held and another + /// `try_recv` is expected after the next READABLE signal. + /// + /// Returns `Err(Error::FailedPrecondition)` if no transaction is + /// pending. Any other kernel error leaves the transaction pending; + /// use `cancel()` to reclaim the buffers. + pub fn try_recv(&mut self) -> Result { + if self.inflight.is_none() { + return Err(Error::FailedPrecondition); + } + + let len = self.handle.async_transact_complete()?; + let buffers = self.inflight.take().unwrap(); + Ok(Completion { len, buffers }) + } + + /// Cancel a pending transaction and reclaim the buffers. + /// + /// If the server has already responded, the response is silently + /// discarded. + /// + /// Returns `Err(Error::FailedPrecondition)` if no transaction is + /// pending. The buffers come back even if the cancel syscall errors: the + /// kernel clears its transaction slot on every path, so it no longer + /// holds pointers into them. + pub fn cancel(&mut self) -> Result { + let Some(buffers) = self.inflight.take() else { + return Err(Error::FailedPrecondition); + }; + + // Only failure for a started transaction is Unavailable, meaning the + // transaction was already dropped. The buffers are free either way. + let _ = self.handle.async_cancel(); + Ok(buffers) + } +} + +impl Drop for AsyncTransaction { + fn drop(&mut self) { + if self.inflight.is_some() { + let _ = self.handle.async_cancel(); + } + } +} + +/// Successful completion of an async transaction. +#[derive(Debug)] +pub struct Completion { + /// Number of response bytes written into `buffers.recv`. + pub len: usize, + /// The buffers, returned for reuse. `recv[..len]` holds the response + /// payload. + pub buffers: Buffers, +} + +/// Error from `start()`, carrying the buffers back so they are not lost. +#[derive(Debug)] +pub struct StartError { + pub error: Error, + pub buffers: Buffers, +} + +impl From for Error { + /// Drops the reclaimed buffers; use `StartError` directly to reuse them. + fn from(e: StartError) -> Self { + e.error + } +} diff --git a/util/ipc/lib.rs b/util/ipc/lib.rs new file mode 100644 index 000000000..6c69f2dd7 --- /dev/null +++ b/util/ipc/lib.rs @@ -0,0 +1,102 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! IPC abstraction over Pigweed kernel channels. +//! +//! `IpcInitiator` and `IpcHandler` split the two channel roles the kernel +//! itself distinguishes (`ChannelInitiatorObject` vs `ChannelHandlerObject`), +//! so a type only has to implement the operations its role can actually +//! serve. `AsyncTransaction` is a safe layer on top of `IpcInitiator`'s +//! unsafe async trio, tracking the one-transaction-per-channel invariant +//! locally instead of leaving it to the caller. + +#![no_std] + +use pw_status::Result; + +/// Blocking and async operations available on the initiator side of a +/// channel (a `ChannelInitiatorObject` in the kernel). +pub trait IpcInitiator { + fn transact( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + deadline: Instant, + ) -> Result + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized; + + /// Starts a transaction and returns immediately; poll readiness via + /// `object_wait`/`wait_group_add` on this channel's handle + /// (`Signals::READABLE`), then call `async_transact_complete` or + /// `async_cancel`. Fails with `Error::Unavailable` if a transaction + /// (blocking or async) is already pending on this channel. + /// + /// # Safety + /// The kernel holds raw pointers into `send_data`/`recv_data` (and + /// writes the response into `recv_data`) until the transaction is + /// completed or cancelled — callers must not read or write either + /// buffer, or let them be dropped or moved, until then. `recv_data` + /// must be large enough to hold the response; a response that + /// overflows it is a kernel error, not truncated silently. + unsafe fn async_transact_start( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + ) -> Result<()> + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized; + + fn async_transact_complete(&self) -> Result; + + fn async_cancel(&self) -> Result<()>; + + /// The raw channel handle, e.g. to register with a WaitGroup or pass to + /// `object_wait`. + fn as_raw(&self) -> u32; +} + +/// Operations available on the handler side of a channel (a +/// `ChannelHandlerObject` in the kernel). +pub trait IpcHandler { + fn read(&self, offset: usize, buffer: &mut Buf) -> Result + where + Buf: AsSyscallBuffer + ?Sized; + + fn respond(&self, buffer: &Buf) -> Result<()> + where + Buf: AsSyscallBuffer + ?Sized; +} + +/// Transparent wrapper around a raw IPC handle. +/// +/// `set_peer_user_signal` (defined in the target-only impl) is an inherent +/// method here rather than on `IpcInitiator`/`IpcHandler`, since it's usable +/// from either channel role and duplicating it onto both traits would just +/// mean two copies of the same forwarding call to keep in sync. +/// +/// One transaction per channel is a kernel rule, not something this type +/// enforces: wrapping the same handle in two `AsyncTransaction`s just gets +/// you `Unavailable` on the second `start`, with the buffers handed back — +/// not anything unsound. +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct IpcHandle { + pub handle: u32, +} + +impl IpcHandle { + /// Wraps a raw channel handle. Trusts the caller to pass one the kernel + /// actually gave out. + pub const fn new(handle: u32) -> Self { + Self { handle } + } +} + +mod async_transaction; +mod target; + +pub use async_transaction::{AsyncTransaction, Buffers, Completion, StartError}; +pub use target::{AsSyscallBuffer, Instant}; diff --git a/util/ipc/target.rs b/util/ipc/target.rs new file mode 100644 index 000000000..976cb9864 --- /dev/null +++ b/util/ipc/target.rs @@ -0,0 +1,81 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use super::{IpcHandle, IpcHandler, IpcInitiator}; + +pub use userspace::buffer::AsSyscallBuffer; +pub use userspace::time::Instant; + +impl IpcHandle { + /// Set (set=true) or clear (set=false) Signals::USER on the paired peer. + pub fn set_peer_user_signal(&self, set: bool) -> pw_status::Result<()> { + userspace::syscall::object_set_peer_user_signal(self.handle, set) + } +} + +impl IpcInitiator for IpcHandle { + fn transact( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + deadline: Instant, + ) -> pw_status::Result + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_transact(self.handle, send_data, recv_data, deadline) + } + + unsafe fn async_transact_start( + &self, + send_data: &BufSend, + recv_data: &mut BufRecv, + ) -> pw_status::Result<()> + where + BufSend: AsSyscallBuffer + ?Sized, + BufRecv: AsSyscallBuffer + ?Sized, + { + let (send_ptr, send_len) = send_data.as_raw(); + let (recv_ptr, recv_len) = recv_data.as_raw_mut(); + // Safety: caller upholds the buffer-lifetime contract per this fn's doc. + // nosemgrep + unsafe { + userspace::syscall::channel_async_transact( + self.handle, + send_ptr, + send_len, + recv_ptr, + recv_len, + ) + } + } + + fn async_transact_complete(&self) -> pw_status::Result { + userspace::syscall::channel_async_transact_complete(self.handle) + } + + fn async_cancel(&self) -> pw_status::Result<()> { + userspace::syscall::channel_async_cancel(self.handle) + } + + fn as_raw(&self) -> u32 { + self.handle + } +} + +impl IpcHandler for IpcHandle { + fn read(&self, offset: usize, buffer: &mut Buf) -> pw_status::Result + where + Buf: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_read(self.handle, offset, buffer) + } + + fn respond(&self, buffer: &Buf) -> pw_status::Result<()> + where + Buf: AsSyscallBuffer + ?Sized, + { + userspace::syscall::channel_respond(self.handle, buffer) + } +}