diff --git a/.github/workflows/task.yaml b/.github/workflows/task.yaml index 8af250e..c6ab018 100644 --- a/.github/workflows/task.yaml +++ b/.github/workflows/task.yaml @@ -10,7 +10,12 @@ env: CARGO_TERM_COLOR: always jobs: - mailbox: + priority: uses: ./.github/workflows/priority.yaml secrets: cookie: ${{ secrets.cookie }} + + unwind: + uses: ./.github/workflows/unwind.yaml + secrets: + cookie: ${{ secrets.cookie }} diff --git a/.github/workflows/unwind.yaml b/.github/workflows/unwind.yaml new file mode 100644 index 0000000..809fb4d --- /dev/null +++ b/.github/workflows/unwind.yaml @@ -0,0 +1,67 @@ +name: Run Tests for Task Unwinding + +on: + workflow_call: + secrets: + cookie: + required: true + +env: + CARGO_TERM_COLOR: always + +jobs: + diverted: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run test diverted + uses: ./.github/workflows/actions/run-test + with: + cookie: ${{ secrets.cookie }} + category: task + sub-category: unwind + test-name: diverted + + deferred_direct_drop: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run test deferred_direct_drop + uses: ./.github/workflows/actions/run-test + with: + cookie: ${{ secrets.cookie }} + category: task + sub-category: unwind + test-name: deferred_direct_drop + + deferred_indirect_drop: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run test deferred_indirect_drop + uses: ./.github/workflows/actions/run-test + with: + cookie: ${{ secrets.cookie }} + category: task + sub-category: unwind + test-name: deferred_indirect_drop + + deferred_nested_drop: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run test deferred_nested_drop + uses: ./.github/workflows/actions/run-test + with: + cookie: ${{ secrets.cookie }} + category: task + sub-category: unwind + test-name: deferred_nested_drop diff --git a/Cargo.toml b/Cargo.toml index 71b492f..2d87520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,3 +117,19 @@ path = "examples/tests/sync/semaphore/initialization.rs" [[example]] name = "test-task-priority-reduce_priority" path = "examples/tests/task/priority/reduce_priority.rs" + +[[example]] +name = "test-task-unwind-diverted" +path = "examples/tests/task/unwind/diverted.rs" + +[[example]] +name = "test-task-unwind-deferred_direct_drop" +path = "examples/tests/task/unwind/deferred_direct_drop.rs" + +[[example]] +name = "test-task-unwind-deferred_indirect_drop" +path = "examples/tests/task/unwind/deferred_indirect_drop.rs" + +[[example]] +name = "test-task-unwind-deferred_nested_drop" +path = "examples/tests/task/unwind/deferred_nested_drop.rs" diff --git a/examples/tests/task/unwind/deferred_direct_drop.rs b/examples/tests/task/unwind/deferred_direct_drop.rs new file mode 100644 index 0000000..3fc4476 --- /dev/null +++ b/examples/tests/task/unwind/deferred_direct_drop.rs @@ -0,0 +1,88 @@ +//! A deferred forced unwinding should occur when a drop handler function +//! overflows the call stack of a task that does not enable dynamic stack +//! extension. + +#![no_std] +#![no_main] + +extern crate alloc; +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; +use hopter::{boot::main, debug::semihosting, hprintln, task}; + +#[main] +fn main(_: cortex_m::Peripherals) { + task::build() + .set_entry(test_task) + .deny_dynamic_stack() + .set_stack_size(512) + .spawn_restartable() + .unwrap(); +} + +fn test_task() { + // A persistent counter. + static CNT: AtomicUsize = AtomicUsize::new(0); + + // Every time the task runs we increment it by 1. + let cnt = CNT.fetch_add(1, Ordering::SeqCst); + + // When the task is executed for the first time, run the drop function. + // Even if this drop function uses a large stack frame and will overflow + // the task's stack while dynamic stack extension is not enabled, the + // segmented stack runtime should still allow the drop function to proceed + // because we cannot initiate an unwinding inside a drop function. A + // deferred forced unwinding will be executed after the drop handler + // finishes. + if cnt == 0 { + core::mem::drop(HasDrop); + } + + if cnt == 0 { + // The task should have been unwound so this print should not be + // reachable. + hprintln!("Should not print this."); + } + + if cnt > 0 { + hprintln!("Task successfully restarted after a deferred forced unwinding."); + semihosting::terminate(true); + } else { + semihosting::terminate(false); + } +} + +struct HasDrop; + +// A drop function that uses a large stack frame. +impl Drop for HasDrop { + #[inline(never)] + fn drop(&mut self) { + let _padding = StackFramePadding::new(); + hprintln!("Drop executed."); + } +} + +/// A padding that causes large stack frame. +struct StackFramePadding { + _padding: [u8; 1024], +} + +impl StackFramePadding { + /// Use volatile write to prevent the compiler from optimizing away the + /// padding. + fn new() -> Self { + let mut padding = MaybeUninit::<[u8; 1024]>::uninit(); + let mut ptr = unsafe { (*padding.as_mut_ptr()).as_mut_ptr() }; + for _ in 0..1024 { + unsafe { + ptr.write_volatile(0); + ptr = ptr.offset(1); + } + } + let padding = unsafe { padding.assume_init() }; + Self { _padding: padding } + } +} diff --git a/examples/tests/task/unwind/deferred_direct_drop.txt b/examples/tests/task/unwind/deferred_direct_drop.txt new file mode 100644 index 0000000..86165f7 --- /dev/null +++ b/examples/tests/task/unwind/deferred_direct_drop.txt @@ -0,0 +1,2 @@ +Drop executed. +Task successfully restarted after a deferred forced unwinding. diff --git a/examples/tests/task/unwind/deferred_indirect_drop.rs b/examples/tests/task/unwind/deferred_indirect_drop.rs new file mode 100644 index 0000000..2dcf226 --- /dev/null +++ b/examples/tests/task/unwind/deferred_indirect_drop.rs @@ -0,0 +1,94 @@ +//! A deferred forced unwinding should occur when a drop handler function +//! is active in the call stack and it further calls a function that overflows +//! the stack, and when the task does not enable dynamic stack extension. + +#![no_std] +#![no_main] + +extern crate alloc; +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; +use hopter::{boot::main, debug::semihosting, hprintln, task}; + +#[main] +fn main(_: cortex_m::Peripherals) { + task::build() + .set_entry(test_task) + .deny_dynamic_stack() + .set_stack_size(512) + .spawn_restartable() + .unwrap(); +} + +fn test_task() { + // A persistent counter. + static CNT: AtomicUsize = AtomicUsize::new(0); + + // Every time the task runs we increment it by 1. + let cnt = CNT.fetch_add(1, Ordering::SeqCst); + + // When the task is executed for the first time, run the drop function. + // Even if this drop function uses a large stack frame and will overflow + // the task's stack while dynamic stack extension is not enabled, the + // segmented stack runtime should still allow the drop function to proceed + // because we cannot initiate an unwinding inside a drop function. A + // deferred forced unwinding will be executed after the drop handler + // finishes. + if cnt == 0 { + core::mem::drop(HasDrop); + } + + if cnt == 0 { + // The task should have been unwound so this print should not be + // reachable. + hprintln!("Should not print this."); + } + + if cnt > 0 { + hprintln!("Task successfully restarted after a deferred forced unwinding."); + semihosting::terminate(true); + } else { + semihosting::terminate(false); + } +} + +#[inline(never)] +fn large_func() { + let _padding = StackFramePadding::new(); + hprintln!("Large function executed."); +} + +struct HasDrop; + +// A drop function that uses a large stack frame. +impl Drop for HasDrop { + #[inline(never)] + fn drop(&mut self) { + large_func(); + hprintln!("Drop executed."); + } +} + +/// A padding that causes large stack frame. +struct StackFramePadding { + _padding: [u8; 1024], +} + +impl StackFramePadding { + /// Use volatile write to prevent the compiler from optimizing away the + /// padding. + fn new() -> Self { + let mut padding = MaybeUninit::<[u8; 1024]>::uninit(); + let mut ptr = unsafe { (*padding.as_mut_ptr()).as_mut_ptr() }; + for _ in 0..1024 { + unsafe { + ptr.write_volatile(0); + ptr = ptr.offset(1); + } + } + let padding = unsafe { padding.assume_init() }; + Self { _padding: padding } + } +} diff --git a/examples/tests/task/unwind/deferred_indirect_drop.txt b/examples/tests/task/unwind/deferred_indirect_drop.txt new file mode 100644 index 0000000..9b3c52d --- /dev/null +++ b/examples/tests/task/unwind/deferred_indirect_drop.txt @@ -0,0 +1,3 @@ +Large function executed. +Drop executed. +Task successfully restarted after a deferred forced unwinding. diff --git a/examples/tests/task/unwind/deferred_nested_drop.rs b/examples/tests/task/unwind/deferred_nested_drop.rs new file mode 100644 index 0000000..19d5763 --- /dev/null +++ b/examples/tests/task/unwind/deferred_nested_drop.rs @@ -0,0 +1,100 @@ +//! A deferred forced unwinding should occur when a drop handler function +//! overflows the call stack of a task that does not enable dynamic stack +//! extension. This test further tests the case where the drop handler is +//! nested inside another drop handler. The forced unwinding should begin +//! only after the return of the outmost drop handler finishes. + +#![no_std] +#![no_main] + +extern crate alloc; +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; +use hopter::{boot::main, debug::semihosting, hprintln, task}; + +#[main] +fn main(_: cortex_m::Peripherals) { + task::build() + .set_entry(test_task) + .deny_dynamic_stack() + .set_stack_size(512) + .spawn_restartable() + .unwrap(); +} + +fn test_task() { + // A persistent counter. + static CNT: AtomicUsize = AtomicUsize::new(0); + + // Every time the task runs we increment it by 1. + let cnt = CNT.fetch_add(1, Ordering::SeqCst); + + // When the task is executed for the first time, run the drop function. + // Even if this drop function uses a large stack frame and will overflow + // the task's stack while dynamic stack extension is not enabled, the + // segmented stack runtime should still allow the drop function to proceed + // because we cannot initiate an unwinding inside a drop function. A + // deferred forced unwinding will be executed after the drop handler + // finishes. + if cnt == 0 { + core::mem::drop(OuterDrop(InnerDrop)); + } + + if cnt == 0 { + // The task should have been unwound so this print should not be + // reachable. + hprintln!("Should not print this."); + } + + if cnt > 0 { + hprintln!("Task successfully restarted after a deferred forced unwinding."); + semihosting::terminate(true); + } else { + semihosting::terminate(false); + } +} + +struct OuterDrop(InnerDrop); + +// Outter drop implicitly also invokes inner drop. +impl Drop for OuterDrop { + #[inline(never)] + fn drop(&mut self) { + hprintln!("Outter drop executed."); + } +} + +struct InnerDrop; + +// A drop function that uses a large stack frame. +impl Drop for InnerDrop { + #[inline(never)] + fn drop(&mut self) { + let _padding = StackFramePadding::new(); + hprintln!("Inner drop executed."); + } +} + +/// A padding that causes large stack frame. +struct StackFramePadding { + _padding: [u8; 1024], +} + +impl StackFramePadding { + /// Use volatile write to prevent the compiler from optimizing away the + /// padding. + fn new() -> Self { + let mut padding = MaybeUninit::<[u8; 1024]>::uninit(); + let mut ptr = unsafe { (*padding.as_mut_ptr()).as_mut_ptr() }; + for _ in 0..1024 { + unsafe { + ptr.write_volatile(0); + ptr = ptr.offset(1); + } + } + let padding = unsafe { padding.assume_init() }; + Self { _padding: padding } + } +} diff --git a/examples/tests/task/unwind/deferred_nested_drop.txt b/examples/tests/task/unwind/deferred_nested_drop.txt new file mode 100644 index 0000000..c75be1b --- /dev/null +++ b/examples/tests/task/unwind/deferred_nested_drop.txt @@ -0,0 +1,3 @@ +Outter drop executed. +Inner drop executed. +Task successfully restarted after a deferred forced unwinding. diff --git a/examples/tests/task/unwind/diverted.rs b/examples/tests/task/unwind/diverted.rs new file mode 100644 index 0000000..4425fae --- /dev/null +++ b/examples/tests/task/unwind/diverted.rs @@ -0,0 +1,75 @@ +//! A forced unwinding should occur when a task without dynamic stack extension +//! is going to overflow its stack. + +#![no_std] +#![no_main] + +extern crate alloc; +use core::{ + mem::MaybeUninit, + sync::atomic::{AtomicUsize, Ordering}, +}; +use hopter::{boot::main, debug::semihosting, hprintln, task}; + +#[main] +fn main(_: cortex_m::Peripherals) { + task::build() + .set_entry(test_task) + .deny_dynamic_stack() + .set_stack_size(512) + .spawn_restartable() + .unwrap(); +} + +fn test_task() { + // A persistent counter. + static CNT: AtomicUsize = AtomicUsize::new(0); + + // Every time the task is started we increment it by 1. + let cnt = CNT.fetch_add(1, Ordering::SeqCst); + + // Call the large function when the task is executed for the first time. + // The function will require a large stack frame causing a stack overflow. + // The function call should be diverted to a forced stack unwinding. + if cnt == 0 { + large_func(); + } + + if cnt == 0 { + // The task should have been unwound so this print should not be + // reachable. + hprintln!("Should not print this."); + } + + if cnt > 0 { + hprintln!("Task successfully restarted after a diverted forced unwinding."); + semihosting::terminate(true); + } else { + semihosting::terminate(false); + } +} + +/// A function that allocates a large stack frame. +#[inline(never)] +fn large_func() { + let _padding = StackFramePadding::new(); +} + +struct StackFramePadding { + _padding: [u8; 1024], +} + +impl StackFramePadding { + fn new() -> Self { + let mut padding = MaybeUninit::<[u8; 1024]>::uninit(); + let mut ptr = unsafe { (*padding.as_mut_ptr()).as_mut_ptr() }; + for _ in 0..1024 { + unsafe { + ptr.write_volatile(0); + ptr = ptr.offset(1); + } + } + let padding = unsafe { padding.assume_init() }; + Self { _padding: padding } + } +} diff --git a/examples/tests/task/unwind/diverted.txt b/examples/tests/task/unwind/diverted.txt new file mode 100644 index 0000000..4a7ef40 --- /dev/null +++ b/examples/tests/task/unwind/diverted.txt @@ -0,0 +1 @@ +Task successfully restarted after a diverted forced unwinding. diff --git a/run_tests.py b/run_tests.py index 4ce884c..2c076ee 100644 --- a/run_tests.py +++ b/run_tests.py @@ -75,10 +75,10 @@ def main(): '--example', f'test-{category}-{subcategory}-{file_no_ext}' ], capture_output=True) - # Error handling for execution error. + # If the test execution returns an error, report the error. if run_result.returncode != 0: print( - f'Error: Test case {category}-{subcategory}-{file_no_ext} failed to run.', + f'Error: Test case {category}-{subcategory}-{file_no_ext} failed through execution.', file=sys.stderr ) @@ -94,7 +94,7 @@ def main(): answer = f.read() if answer != run_result.stdout: print( - f'Error: Test case {category}-{subcategory}-{file_no_ext} failed.', + f'Error: Test case {category}-{subcategory}-{file_no_ext} failed to provide correct output.', file=sys.stderr ) sys.exit(1)