blob: 779ba735a2d14904c862152f439e68d6efc71a46 [file]
// Copyright 2026 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#![no_std]
#![no_main]
use control_commands::Command;
use main_codegen::handle;
use pw_status::{Result, StatusCode};
use userspace::time::{Clock, Duration, Instant, SystemClock, sleep_until};
use userspace::{process_entry, syscall};
const PROCESS_JOIN_TIMEOUT: Duration = Duration::from_secs(5);
const TEST_THREAD_SPAWN_DELAY: Duration = Duration::from_millis(200);
#[unsafe(no_mangle)]
pub extern "C" fn test_thread_entry_initiator(_arg: usize) {
let mut reply = [0u8; 1];
test_logger::step_info!("Test thread: Initiating blocked transaction via Command 3");
let res = syscall::channel_transact(
handle::IPC_CONTROL_INITIATOR,
&[Command::BlockInitiator as u8],
&mut reply,
Instant::MAX,
);
if res == Err(pw_status::Error::Unavailable) {
test_logger::step_passed!("Initiator thread correctly received Unavailable!");
syscall::thread_exit(0);
} else {
test_logger::step_failed!(
"Initiator thread expected Unavailable, got {}",
res.status_code() as u32
);
let _ = syscall::debug_shutdown(Err(pw_status::Error::Internal));
loop {}
}
}
#[unsafe(no_mangle)]
pub extern "C" fn test_thread_entry_handler(_arg: usize) {
test_logger::step_info!("Test thread: Waiting for transaction on IPC_RESET handler");
let wait_res =
syscall::object_wait(handle::IPC_RESET, syscall::Signals::READABLE, Instant::MAX);
if wait_res == Err(pw_status::Error::Cancelled) {
test_logger::step_passed!("Handler thread correctly received Cancelled!");
syscall::thread_exit(0);
} else {
test_logger::step_failed!("Handler thread expected Cancelled");
let _ = syscall::debug_shutdown(Err(pw_status::Error::Internal));
loop {}
}
}
const TEST_THREAD_STACK_SIZE: usize = 1024;
fn do_test() -> Result<()> {
test_logger::start("User Process Termination");
let mut test_thread_stack = [0u8; TEST_THREAD_STACK_SIZE];
let mut reply = [0u8; 1];
// Sleep to allow threads to start and log before the tests start.
// Once threads can be manually started, this will become unnecessary.
if let Err(err) = sleep_until(SystemClock::now() + TEST_THREAD_SPAWN_DELAY) {
test_logger::step_failed!("Failed to sleep for 200ms");
return Err(err);
}
test_invalid_handles()?;
test_clean_and_forced_exit()?;
test_object_reset_basic(&mut reply)?;
test_own_user_signal_preserved_on_terminate(&mut reply)?;
test_peer_user_signal_cleared_on_terminate(&mut reply)?;
test_initiator_unavailable_on_terminate(&mut test_thread_stack)?;
test_handler_error_on_terminate(&mut test_thread_stack)?;
test_wait_group_error_on_initiator_terminate(&mut test_thread_stack, &mut reply)?;
test_wait_group_error_on_handler_terminate(&mut test_thread_stack)?;
test_logger::passed("User Process Termination");
Ok(())
}
fn start_test_thread(entry: extern "C" fn(usize), stack: &mut [u8]) -> Result<()> {
let initial_pc = entry as *const () as usize;
let initial_sp = unsafe { stack.as_mut_ptr().add(stack.len()) as usize };
syscall::thread_start(handle::TEST_THREAD, initial_pc, initial_sp)
}
fn cleanup_test_thread() -> Result<()> {
let _ = syscall::task_terminate(handle::TEST_THREAD);
syscall::object_wait(
handle::TEST_THREAD,
syscall::Signals::JOINABLE,
Instant::MAX,
)?;
syscall::task_join(handle::TEST_THREAD)?;
Ok(())
}
fn wait_and_join_task(handle: u32) -> Result<syscall::ExitStatus> {
syscall::object_wait(handle, syscall::Signals::JOINABLE, Instant::MAX)?;
syscall::task_join(handle)
}
fn send_control_command(cmd: u8, reply: &mut [u8]) -> Result<()> {
syscall::channel_transact(handle::IPC_CONTROL_INITIATOR, &[cmd], reply, Instant::MAX)?;
Ok(())
}
/// Tests that syscalls correctly reject invalid handles or handles of the wrong type.
fn test_invalid_handles() -> Result<()> {
// We cannot test successful process_terminate on our own process because
// it kills the main thread, resulting in a kernel panic (run queue empty),
// and thus a failed test. Furthermore, userspace apps cannot spawn new processes
// or hold handles to other processes.
// So we test that process_terminate correctly rejects invalid handles.
test_logger::step_start!("Testing task_terminate on an invalid handle");
let result = syscall::task_terminate(0xdeadbeef);
if result != Err(pw_status::Error::OutOfRange) {
test_logger::step_failed!("Expected OutOfRange on an invalid handle");
return Err(pw_status::Error::Internal);
}
test_logger::step_passed!("Received Error::OutOfRange response");
Ok(())
}
/// Tests normal process termination (clean exit) and forced termination (kill).
fn test_clean_and_forced_exit() -> Result<()> {
for pass in 0..2 {
test_logger::step_start!("Testing clean exit iteration {}", pass as u32);
test_logger::step_info!("Waiting for clean exit process to become joinable");
if let Err(err) = syscall::object_wait(
handle::CLEAN_EXIT_PROCESS,
syscall::Signals::JOINABLE,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
) {
test_logger::step_failed!("Failed to wait for extra process to become joinable");
return Err(err);
}
test_logger::step_info!("Joining clean exit process");
match syscall::task_join(handle::CLEAN_EXIT_PROCESS) {
Err(err) => return Err(err),
Ok(syscall::ExitStatus::Success(42)) => (),
Ok(_) => {
test_logger::step_failed!("Clean exit process joined with unexpected status");
return Err(pw_status::Error::Internal);
}
}
test_logger::step_passed!("Clean exit process joined");
test_logger::step_start!("Testing forced exit iteration {}", pass as u32);
// The forced_exit process is started automatically by the kernel on boot.
// On subsequent iterations, we start it manually below.
test_logger::step_info!("Terminating forced exit process");
syscall::task_terminate(handle::FORCED_EXIT_PROCESS)?;
test_logger::step_info!("Waiting for forced exit process to become joinable");
if let Err(err) = syscall::object_wait(
handle::FORCED_EXIT_PROCESS,
syscall::Signals::JOINABLE,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
) {
test_logger::step_failed!("Error waiting for process to be joinable");
return Err(err);
}
test_logger::step_info!("Joining forced exit process");
let status = syscall::task_join(handle::FORCED_EXIT_PROCESS)?;
if status != syscall::ExitStatus::TerminatedBySyscall {
test_logger::step_failed!(
"Process joined with unexpected status (expected TerminatedBySyscall)"
);
return Err(pw_status::Error::Internal);
}
test_logger::step_passed!("Forced exit process joined");
test_logger::step_start!("Testing exception exit iteration {}", pass as u32);
test_logger::step_info!("Waiting for exception exit process to become joinable");
if let Err(err) = syscall::object_wait(
handle::EXCEPTION_EXIT_PROCESS,
syscall::Signals::JOINABLE,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
) {
test_logger::step_failed!("Error waiting for exception process to be joinable");
return Err(err);
}
test_logger::step_info!("Joining exception exit process");
let status = syscall::task_join(handle::EXCEPTION_EXIT_PROCESS)?;
if status != syscall::ExitStatus::UnhandledException(0) {
test_logger::step_failed!(
"Process joined with unexpected status (expected UnhandledException(0))"
);
return Err(pw_status::Error::Internal);
}
test_logger::step_passed!("Exception exit process joined");
test_logger::info!("Restarting test processes iteration {}", pass as u32);
syscall::process_start(handle::CLEAN_EXIT_PROCESS)?;
syscall::process_start(handle::FORCED_EXIT_PROCESS)?;
syscall::process_start(handle::EXCEPTION_EXIT_PROCESS)?;
// Give the other processes a chance to start for the next iteration.
//
// TODO: https://pwbug.dev/505490714 - Query process state to ensure it's running
if let Err(err) = sleep_until(SystemClock::now() + TEST_THREAD_SPAWN_DELAY) {
test_logger::step_failed!("Failed to sleep for 200ms");
return Err(err);
}
}
Ok(())
}
/// Tests that objects are correctly reset when a process terminates.
fn test_object_reset_basic(reply: &mut [u8]) -> Result<()> {
test_logger::step_start!("Testing Object Reset");
test_logger::step_info!("Sending 'Modify State' command to restart process");
syscall::channel_transact(
handle::IPC_CONTROL_INITIATOR,
&[Command::ModifyState as u8],
reply,
Instant::MAX,
)?;
test_logger::step_info!("Terminating restart_process");
syscall::task_terminate(handle::RESTART_PROCESS)?;
test_logger::step_info!("Waiting for restart_process to become joinable and joining");
wait_and_join_task(handle::RESTART_PROCESS)?;
test_logger::step_info!("Restarting restart_process");
syscall::process_start(handle::RESTART_PROCESS)?;
test_logger::step_info!("Sending 'Verify Reset' command to restart_process");
syscall::channel_transact(
handle::IPC_CONTROL_INITIATOR,
&[Command::VerifyReset as u8],
reply,
Instant::MAX,
)?;
Ok(())
}
/// Tests that an object's own USER signal is preserved on process termination and
/// restart when it's peer remains alive.
fn test_own_user_signal_preserved_on_terminate(reply: &mut [u8]) -> Result<()> {
test_logger::step_start!("Receiver termination preserves USER signal upon restart");
test_logger::step_info!("Setting peer USER signal to true on RESTART_PROCESS handler");
syscall::object_set_peer_user_signal(handle::IPC_CONTROL_INITIATOR, true)?;
test_logger::step_info!("Terminating RESTART_PROCESS");
syscall::task_terminate(handle::RESTART_PROCESS)?;
test_logger::step_info!("Waiting for RESTART_PROCESS to become joinable and joining");
wait_and_join_task(handle::RESTART_PROCESS)?;
// Restart RESTART_PROCESS
test_logger::step_info!("Restarting RESTART_PROCESS");
syscall::process_start(handle::RESTART_PROCESS)?;
// Request RESTART_PROCESS to verify USER signal remained active on restart
test_logger::step_info!("Requesting RESTART_PROCESS to verify USER signal is preserved");
syscall::channel_transact(
handle::IPC_CONTROL_INITIATOR,
&[Command::VerifyUserSignalPreserved as u8],
reply,
Instant::MAX,
)?;
// Reset our USER signal to false for subsequent tests
syscall::object_set_peer_user_signal(handle::IPC_CONTROL_INITIATOR, false)?;
Ok(())
}
/// Tests that an object's peer's USER signal is cleared on process termination and
/// restart.
fn test_peer_user_signal_cleared_on_terminate(reply: &mut [u8]) -> Result<()> {
test_logger::step_start!("Process termination resets peer's USER signals to false");
test_logger::step_info!("Requesting RESTART_PROCESS to set USER signal on IPC_RESET handler");
syscall::channel_transact(
handle::IPC_CONTROL_INITIATOR,
&[Command::SetResetUserSignal as u8],
reply,
Instant::MAX,
)?;
// Verify USER signal is active on IPC_RESET handler before termination
let wait_res = syscall::object_wait(
handle::IPC_RESET,
syscall::Signals::USER,
SystemClock::now(),
);
if wait_res.is_err() {
test_logger::step_failed!("Expected Signals::USER to be set on IPC_RESET handler");
return Err(pw_status::Error::Internal);
}
test_logger::step_info!("Terminating RESTART_PROCESS");
syscall::task_terminate(handle::RESTART_PROCESS)?;
test_logger::step_info!("Waiting for RESTART_PROCESS to become joinable and joining");
wait_and_join_task(handle::RESTART_PROCESS)?;
// Verify USER signal is reset to false on IPC_RESET handler after termination!
test_logger::step_info!("Checking if USER signal is de-asserted on IPC_RESET handler");
let wait_res = syscall::object_wait(
handle::IPC_RESET,
syscall::Signals::USER,
SystemClock::now(),
);
// If it was reset to false, waiting for USER signal should return Err(DeadlineExceeded) (4) immediately!
if wait_res != Err(pw_status::Error::DeadlineExceeded) {
test_logger::step_failed!("Signals::USER was NOT de-asserted on process termination!");
return Err(pw_status::Error::Internal);
}
test_logger::step_passed!("Peer's USER signal correctly de-asserted after termination!");
// Restart RESTART_PROCESS
syscall::process_start(handle::RESTART_PROCESS)?;
Ok(())
}
/// Tests that an initiator receives `Unavailable` when the handler terminates.
fn test_initiator_unavailable_on_terminate(stack: &mut [u8]) -> Result<()> {
test_logger::step_start!("Initiator receives Unavailable when handler terminates");
// test_thread_entry_initiator calls channel_transact and asserts that Unavailable is returned
start_test_thread(test_thread_entry_initiator, stack)?;
// Give the test thread a chance to print its log
sleep_until(SystemClock::now() + TEST_THREAD_SPAWN_DELAY)?;
test_logger::step_info!("Terminating restart_process");
syscall::task_terminate(handle::RESTART_PROCESS)?;
test_logger::step_info!("Waiting for restart_process to become joinable and joining");
wait_and_join_task(handle::RESTART_PROCESS)?;
// Give the test thread a chance to print its log
sleep_until(SystemClock::now() + TEST_THREAD_SPAWN_DELAY)?;
cleanup_test_thread()?;
Ok(())
}
/// Tests that a handler receives `ERROR` signal when the initiator terminates.
fn test_handler_error_on_terminate(_stack: &mut [u8]) -> Result<()> {
test_logger::step_start!("Handler receives ERROR signal when initiator terminates");
syscall::process_start(handle::RESTART_PROCESS)?;
// Tell OBJECT_RESET to initiate async transaction
let mut reply = [0u8; 1];
test_logger::step_info!("main thread: Sending command 4 to initiate async transaction");
send_control_command(Command::AsyncTransact as u8, &mut reply)?;
// Wait for the async transaction to arrive
syscall::object_wait(handle::IPC_RESET, syscall::Signals::READABLE, Instant::MAX)?;
// Wait for the restart process to exit (it will do so after 500ms via command 4)
wait_and_join_task(handle::RESTART_PROCESS)?;
// Now check if the handler object received the ERROR signal
let wait_res = syscall::object_wait(
handle::IPC_RESET,
syscall::Signals::ERROR,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
);
if wait_res.is_ok() {
test_logger::step_passed!("Handler thread correctly received ERROR signal after join!");
} else {
test_logger::step_failed!("Handler thread expected ERROR signal");
return Err(pw_status::Error::Internal);
}
Ok(())
}
/// Tests that the Wait Group receives `ERROR` when the initiator terminates.
fn test_wait_group_error_on_initiator_terminate(_stack: &mut [u8], reply: &mut [u8]) -> Result<()> {
test_logger::step_start!("Wait Group receives ERROR when initiator terminates");
syscall::process_start(handle::RESTART_PROCESS)?;
// Tell OBJECT_RESET to initiate async transaction
test_logger::step_info!("main thread: Sending command 4 to initiate async transaction");
send_control_command(Command::AsyncTransact as u8, reply)?;
// Add IPC_RESET to WAIT_GROUP waiting for ERROR
syscall::wait_group_add(
handle::WAIT_GROUP,
handle::IPC_RESET,
syscall::Signals::ERROR,
99,
)?;
// Wait for the restart process to exit
wait_and_join_task(handle::RESTART_PROCESS)?;
// Now wait on the WAIT_GROUP (it should be ready immediately since cleanup happened)
let wait_return = syscall::object_wait(
handle::WAIT_GROUP,
syscall::Signals::READABLE,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
)?;
if wait_return.user_data == 99
&& wait_return
.pending_signals
.contains(syscall::Signals::ERROR)
{
test_logger::step_passed!("Wait Group correctly woke up on member ERROR signal!");
} else {
test_logger::step_failed!(
"Wait Group woke up but unexpected result: user_data={}, signals={:#x}",
wait_return.user_data as u32,
wait_return.pending_signals.bits() as u32
);
return Err(pw_status::Error::Internal);
}
// Clean up: remove from wait group
syscall::wait_group_remove(handle::WAIT_GROUP, handle::IPC_RESET)?;
Ok(())
}
/// Tests that the Wait Group receives `ERROR` when the handler terminates.
fn test_wait_group_error_on_handler_terminate(_stack: &mut [u8]) -> Result<()> {
test_logger::step_start!("Wait Group receives ERROR when handler terminates");
syscall::process_start(handle::RESTART_PROCESS)?;
// Initiate async transaction on IPC_CONTROL
let msg = [Command::SleepAndExit as u8];
let mut reply_buf = [0u8; 1];
test_logger::step_info!("main thread: Initiating async transaction on IPC_CONTROL");
// NOTE: Async channel syscalls are not meant to be used directly by normal
// programs without wrappers handling buffer lifetimes.
unsafe {
syscall::channel_async_transact(
handle::IPC_CONTROL_INITIATOR,
msg.as_ptr(),
msg.len(),
reply_buf.as_mut_ptr(),
reply_buf.len(),
)?;
}
// Add IPC_CONTROL to WAIT_GROUP waiting for ERROR
syscall::wait_group_add(
handle::WAIT_GROUP,
handle::IPC_CONTROL_INITIATOR,
syscall::Signals::ERROR,
99,
)?;
// Wait for the restart process to exit
wait_and_join_task(handle::RESTART_PROCESS)?;
// Now wait on the WAIT_GROUP
let wait_return = syscall::object_wait(
handle::WAIT_GROUP,
syscall::Signals::READABLE,
SystemClock::now() + PROCESS_JOIN_TIMEOUT,
)?;
if (wait_return.user_data == 89 || wait_return.user_data == 99)
&& wait_return
.pending_signals
.contains(syscall::Signals::ERROR)
{
test_logger::step_passed!(
"Wait Group correctly woke up on member ERROR and READABLE (Initiator case)!"
);
} else {
test_logger::step_failed!(
"Wait Group woke up but unexpected result: user_data={}, signals={:#x}",
wait_return.user_data as u32,
wait_return.pending_signals.bits() as u32
);
return Err(pw_status::Error::Internal);
}
// Clean up: remove from wait group
syscall::wait_group_remove(handle::WAIT_GROUP, handle::IPC_CONTROL_INITIATOR)?;
// Cancel the async transaction to clean up channel state
if let Err(e) = syscall::channel_async_cancel(handle::IPC_CONTROL_INITIATOR) {
if e != pw_status::Error::Unavailable {
return Err(e);
}
}
Ok(())
}
#[process_entry("main")]
fn main() -> Result<()> {
let ret = do_test().inspect_err(|e| {
test_logger::failed("User Process Termination");
test_logger::step_failed!("status code: {}", *e as u32);
});
let _ = syscall::debug_shutdown(ret);
ret
}