From 78ad8b451fd11726e596c75662b37c13b4abe161 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Desbiens?= Date: Thu, 3 Sep 2026 14:14:06 -0400 Subject: [PATCH] Refused thread delete and reset while an exit transition is in progress _tx_thread_shell_entry and _tx_thread_terminate both publish a thread's terminal state -- TX_COMPLETED or TX_TERMINATED -- and then call that thread's exit notification callback, before the thread has been detached from the ready list and before either service has finished with the pointer it holds to the control block. That terminal state is exactly the state _tx_thread_delete and _tx_thread_reset accept as authorization to invalidate or rebuild the control block, and neither service tested whether the transition producing it had finished. A callback could therefore delete the thread it was called for -- and then lawfully recreate it over the same memory, since delete exists to permit that -- while the scheduler was still linked to the old incarnation. tx_thread_create zeroes the whole control block and can auto-start the new one, so the old priority list is left heading at a block whose own priority field names a different list, with the old priority's map bit set behind nothing. Alternatively a callback could reset a terminated thread, which moves it out of the terminal state, and then resume it: the interrupted- suspension logic in _tx_thread_system_resume refuses to void a suspension only while the state is still terminal, so with the reset allowed first the resume clears the suspending flag and restores TX_READY, the outer service then finds the flag clear and skips the removal, and tx_thread_terminate returns TX_SUCCESS for a thread that is runnable again. Interrupt masking does not close the window, because the kernel restores the prior posture before invoking the callback deliberately. On SMP TX_RESTORE also releases the global protection, so the target can be executing on another core while its callback runs -- and a reset there memsets the stack a live core is running on. On the Linux, Win32 and Win64 host simulation ports the consequence is more immediate than corruption: TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the host thread backing the deleted thread, so a callback-side delete of the completing thread destroys the host thread the callback is running on. The fix marks the transition and has the two services refuse a marked target, returning the errors they already document, TX_DELETE_ERROR and TX_NOT_DONE. The refusal is transient and the same call succeeds once the transition has completed, so no documented lifecycle is lost; and it is in the core services rather than the _txe_ wrappers, so disabling error checking cannot disable it. Refusing the reset is also what closes the resume path, without touching _tx_thread_system_resume: its existing terminal-state test is sufficient once nothing can turn the terminal state into TX_SUSPENDED from inside the window. tx_thread_suspending is the marker, rather than a new control-block field. It already means "a suspension is in progress" and is already true across the callback in the two interruptable paths, so no field is added, the public structure is unchanged, and sizeof(TX_THREAD) is unchanged -- which matters, because the Module Manager's object handling depends on the sizes of the control blocks. Widening its lifetime was checked against every reader rather than assumed. There are four: two in _tx_thread_system_suspend and two in _tx_thread_system_resume. In every window this change widens, the state is TX_COMPLETED or TX_TERMINATED, and both resume readers already refuse to void a suspension for exactly those two states, so their behaviour is unchanged; and no suspension routine is called on the target in those windows, so the suspend readers never see them. No suspension-initiating service can set the marker again inside a window either: every one of them acts on a thread that is ready or suspended. Three sites needed changing beyond the two refusals, and the shape of each was decided by where the marker can safely be cleared: - The non-ready branch of _tx_thread_terminate cleared the marker before the terminated extension and the callback, which is what left them free to act on a control block the service still had mutex-release processing to do against. The clear moves to the common tail, after the last dereference of the target, and becomes the single clear site for the whole service. In the interruptable ready branch the flag is already false there, because _tx_thread_system_suspend cleared it when it detached the thread, so the tail store is a second store of a value the flag already holds -- cheaper than testing for it, and it keeps one clear site. - Under TX_NOT_INTERRUPTABLE neither path set the marker at all, because that configuration does not use the interruptable suspension path that sets it. Both now set it before the callback. Interrupts being disabled there does not help: the callback is reached by a direct call. - In the TX_NOT_INTERRUPTABLE completion path the marker is cleared before _tx_thread_system_ni_suspend rather than after it. That call returns to the scheduler for a thread that is the current thread, which a completing thread is, and does not come back; clearing afterwards would leave a normally completed thread marked for ever and therefore permanently undeletable. Nothing is lost by clearing early there, because everything from that point to the detachment runs with interrupts disabled and calls no application code. The change is the same change twice. All four files are byte-for-byte identical between common and common_smp at this commit and stay so after it, so common_smp was written by copying rather than by repeating the edits. tx_thread_system_suspend.c and tx_thread_system_resume.c, which do differ between the kernels, are deliberately untouched. Tests. The in-tree regression test goes to both trees and is byte-for-byte identical between them. It drives seven scenarios: terminating a ready non-current target with two peers ready at the same priority, with the callback attempting the delete and recreating the block if it succeeded; the same with the callback attempting the reset and then the resume; terminating a target suspended on a semaphore while owning a mutex, which is the non-ready branch; natural completion alone at its priority, including the safe post-completion reset, terminate, delete and recreate at another priority; self termination; a benign callback, whose notification count and ordering are unchanged; and the state and boundary cases, where the new refusal must not fire. It measures rather than describes. The callback records the published state, the marker, and the status of every lifecycle service it can reach, calling the core service as well as the wrapper wherever a refusal is expected. A snapshot taken under interrupt lockout -- which is the global SMP protection on an SMP port -- checks that every ready list agrees with the control blocks it heads, that the priority map agrees with the lists, and that each execute pointer is a member of the list its own priority field names. Every walk is bounded, so a corrupted ring costs an assertion and not a hang, and no test in the suite can hang. Expectations are counted inside a scenario and gated between scenarios, so a failing kernel reports how much it failed by without being driven further into its own corruption. The consequences are demonstrated from the terminator's context rather than the completing thread's, which is what makes the pre-fix behaviour an assertion instead of a wedged simulator. Compiled against the unfixed sources the test fails 9 of the 24 expectations it reaches in the uniprocessor tree and 10 of 24 in the SMP tree, and the failures are the finding: the callback-side delete succeeds, the recreate succeeds, the consistency snapshot disagrees, the target is neither terminal nor detached when the service returns, and a peer has left the ready ring the recreated block hijacked. The SMP tree gets a second test for the case that needs concurrency. The victim is excluded to core 1 and spins there without relinquishing while the controller, excluded to core 0, terminates it, so the callback runs on one core while the target executes on another. The callback-side reset and delete must both be refused, and a sentinel written into the unused low end of the victim's stack must survive -- a reset would have memset the whole stack before rebuilding the frame. Every wait is bounded, and if the remote precondition cannot be established the test says so and drops only the assertions that depend on it rather than reporting a pass it did not earn; measured over twenty consecutive runs it established the precondition every time. TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five build configurations either tree compiles, and each tree builds the whole library once per configuration, so neither can be reached from inside the suites. The lines this change adds under TX_NOT_INTERRUPTABLE are therefore in no configuration the trees build, and they are where the permanent- undeletability failure mode lives, so they get their own harness rather than a compile check: the four sources plus the two error wrappers are compiled directly into a test executable, once per combination, with recorders standing behind the scheduler services they call. That is what makes the marker's value at the moment of detachment directly observable. It runs 66 expectations under TX_NOT_INTERRUPTABLE, 66 under that with error checking disabled, 45 under that with notification disabled, and 63 under error checking disabled alone; against the unfixed sources those fail 25, 22, 6 and 20 respectively. The harness lives in the uniprocessor tree only, because the four sources are identical between the kernels and the shim replaces the very primitive the SMP port differs in, so a second copy would compile the same text under the same macros. It is deliberately left out of the coverage instrumentation, since the same source under different feature macros has a different line set and merging those would confuse the union rather than add to it. Results. Both suites pass in all five configurations with GCC 14: 103 of 103 in the uniprocessor tree, up from 98, and 116 of 116 in the SMP tree, up from 114. Merged line coverage is 100% in the uniprocessor tree and 5172 of 5183 in the SMP tree, whose eleven uncovered lines are the same eleven that were uncovered before this change and are in tx_byte_pool_search and tx_thread_smp_utilities; all four changed files are at 100% line coverage in both trees, and SMP branch coverage rises from 2819 of 3548 to 2831 of 3556. Cross-compiled with arm-none-eabi-gcc at -Wall -Wextra for Cortex-M4 against common and for Cortex-A7 SMP against common_smp, all four files produce no diagnostics at all and an identical warning set to before the change, under -std=gnu99 and -std=c99 alike -- unlike the module ports, -std=c99 does not fail on these base ports, and even -Wconversion is clean. No MISRA deviation is required: explicit comparisons to TX_TRUE, existing ThreadX types, single-entry and single-exit control flow, no goto, and two added constant-time tests that change no real-time complexity. Assisted-by: Claude Code (Opus 5) --- common/src/tx_thread_delete.c | 27 + common/src/tx_thread_reset.c | 24 + common/src/tx_thread_shell_entry.c | 22 + common/src/tx_thread_terminate.c | 47 +- common_smp/src/tx_thread_delete.c | 27 + common_smp/src/tx_thread_reset.c | 24 + common_smp/src/tx_thread_shell_entry.c | 22 + common_smp/src/tx_thread_terminate.c | 47 +- test/smp/cmake/regression/CMakeLists.txt | 2 + test/smp/regression/testcontrol.c | 4 + ...adx_smp_thread_exit_callback_remote_test.c | 514 ++++++ ...adx_thread_exit_callback_transition_test.c | 1375 +++++++++++++++++ test/tx/cmake/CMakeLists.txt | 9 + test/tx/cmake/regression/CMakeLists.txt | 1 + .../tx/cmake/thread_transition/CMakeLists.txt | 115 ++ test/tx/regression/testcontrol.c | 2 + ...adx_thread_exit_callback_transition_test.c | 1375 +++++++++++++++++ ...adx_thread_transition_configuration_test.c | 607 ++++++++ ...threadx_thread_transition_host_test_port.h | 120 ++ 19 files changed, 4336 insertions(+), 28 deletions(-) create mode 100644 test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c create mode 100644 test/smp/regression/threadx_thread_exit_callback_transition_test.c create mode 100644 test/tx/cmake/thread_transition/CMakeLists.txt create mode 100644 test/tx/regression/threadx_thread_exit_callback_transition_test.c create mode 100644 test/tx/thread_transition/threadx_thread_transition_configuration_test.c create mode 100644 test/tx/thread_transition/threadx_thread_transition_host_test_port.h diff --git a/common/src/tx_thread_delete.c b/common/src/tx_thread_delete.c index 510f6565e..e73fdd319 100644 --- a/common/src/tx_thread_delete.c +++ b/common/src/tx_thread_delete.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -95,6 +97,31 @@ UINT status; } } + /* The state is terminal, but a terminal state on its own is not authorization to + release the control block. Both paths that produce one -- thread completion in + _tx_thread_shell_entry and thread termination in _tx_thread_terminate -- publish + TX_COMPLETED or TX_TERMINATED, and then run this thread's exit notification + callback, before the thread has been detached from the ready list and before + those services have finished with the pointer they hold to it. The suspending + flag is set for exactly that interval, so a thread whose flag is still set is + part-way through the transition. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Restore interrupts. */ + TX_RESTORE + + /* Yes, refuse the delete rather than unlink a thread the scheduler is + still holding. The condition is transient: the caller may retry once + the transition has finished. */ + status = TX_DELETE_ERROR; + } + } + /* Determine if the delete operation is okay. */ if (status == TX_SUCCESS) { diff --git a/common/src/tx_thread_reset.c b/common/src/tx_thread_reset.c index da56daf57..e87de26be 100644 --- a/common/src/tx_thread_reset.c +++ b/common/src/tx_thread_reset.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -104,6 +106,28 @@ UINT status; status = TX_NOT_DONE; } } + + /* The state is terminal, but a terminal state on its own is not authorization + to rebuild the stack and move the thread back to TX_SUSPENDED. Both paths + that produce one -- thread completion in _tx_thread_shell_entry and thread + termination in _tx_thread_terminate -- publish TX_COMPLETED or + TX_TERMINATED, and then run this thread's exit notification callback, before + the thread has been detached from the ready list. The suspending flag is + set for exactly that interval. Resetting inside it would also defeat the + protection _tx_thread_system_resume relies on, which refuses to cancel a + suspension only while the state is still terminal. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Yes, refuse the reset. The condition is transient: the caller may + retry once the transition has finished. */ + status = TX_NOT_DONE; + } + } } /* Is the request valid? */ diff --git a/common/src/tx_thread_shell_entry.c b/common/src/tx_thread_shell_entry.c index 45fe9d6da..c079f18e1 100644 --- a/common/src/tx_thread_shell_entry.c +++ b/common/src/tx_thread_shell_entry.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -134,6 +136,17 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the completion transition is marked as in + progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to cancel + an interrupted suspension -- interrupts stay disabled through the whole + transition -- but the notification callback and the completion extension + below are application code, reached by a direct call, and interrupt lockout + does not stop either of them from calling a thread lifecycle service on this + same control block. The flag is what _tx_thread_delete and _tx_thread_reset + test in order to refuse one. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -148,6 +161,15 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); /* Perform any additional activities for tool or user purpose. */ TX_THREAD_COMPLETED_EXTENSION(thread_ptr) + /* Clear the suspending flag. It has to be cleared here rather than after the + call below, because _tx_thread_system_ni_suspend returns to the scheduler for + a thread that is the current thread, which this one is, and so does not come + back. Clearing it afterwards would leave a normally completed thread marked + as transitioning for ever and therefore permanently undeletable. Clearing it + here loses nothing: everything from this point to the detachment runs with + interrupts disabled and calls no application code. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Call actual non-interruptable thread suspension routine. */ _tx_thread_system_ni_suspend(thread_ptr, ((ULONG) 0)); diff --git a/common/src/tx_thread_terminate.c b/common/src/tx_thread_terminate.c index 86e34baeb..05ec91a1f 100644 --- a/common/src/tx_thread_terminate.c +++ b/common/src/tx_thread_terminate.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -133,6 +135,16 @@ ULONG suspension_sequence; #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the termination transition is marked + as in progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to + cancel an interrupted suspension -- interrupts stay disabled through + the whole transition -- but the notification callback below is + application code, reached by a direct call, and interrupt lockout does + not stop it from calling a thread lifecycle service on this same + control block. The flag is cleared in the common tail below. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -225,20 +237,13 @@ ULONG suspension_sequence; (suspend_cleanup)(thread_ptr, suspension_sequence); } -#ifndef TX_NOT_INTERRUPTABLE - - /* Disable interrupts. */ - TX_DISABLE -#endif - - /* Clear the suspending flag. */ - thread_ptr -> tx_thread_suspending = TX_FALSE; - -#ifndef TX_NOT_INTERRUPTABLE - - /* Restore interrupts. */ - TX_RESTORE -#endif + /* The suspending flag deliberately stays set here. It used to be + cleared at this point, which left the terminated extension and the + notification callback below -- both application code -- free to delete + or reset this control block while this service still held a pointer to + it and still had mutex-release processing to do. It is now cleared + once, in the common tail below, after the last dereference of the + target. */ /* Perform any additional activities for tool or user purpose. */ TX_THREAD_TERMINATED_EXTENSION(thread_ptr) @@ -282,6 +287,20 @@ ULONG suspension_sequence; TX_DISABLE #endif + /* Clear the suspending flag. This is the one place the end of the + termination transition is published, and it is reached from both branches + above, after the notification callback and after the mutex-release + processing, which is the last thing in this service to dereference the + target. Until this store, _tx_thread_delete and _tx_thread_reset refuse + the target. In the interruptable ready branch the flag is already false, + because _tx_thread_system_suspend cleared it when it detached the thread, + so this is a second store of a value the flag already holds; that is + cheaper than testing for it and it keeps the transition to a single clear + site. No suspension-initiating service can have set it again in between: + every one of them acts on a thread that is ready or suspended, and this + thread is terminated. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Enable preemption. */ _tx_thread_preempt_disable--; diff --git a/common_smp/src/tx_thread_delete.c b/common_smp/src/tx_thread_delete.c index 510f6565e..e73fdd319 100644 --- a/common_smp/src/tx_thread_delete.c +++ b/common_smp/src/tx_thread_delete.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -95,6 +97,31 @@ UINT status; } } + /* The state is terminal, but a terminal state on its own is not authorization to + release the control block. Both paths that produce one -- thread completion in + _tx_thread_shell_entry and thread termination in _tx_thread_terminate -- publish + TX_COMPLETED or TX_TERMINATED, and then run this thread's exit notification + callback, before the thread has been detached from the ready list and before + those services have finished with the pointer they hold to it. The suspending + flag is set for exactly that interval, so a thread whose flag is still set is + part-way through the transition. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Restore interrupts. */ + TX_RESTORE + + /* Yes, refuse the delete rather than unlink a thread the scheduler is + still holding. The condition is transient: the caller may retry once + the transition has finished. */ + status = TX_DELETE_ERROR; + } + } + /* Determine if the delete operation is okay. */ if (status == TX_SUCCESS) { diff --git a/common_smp/src/tx_thread_reset.c b/common_smp/src/tx_thread_reset.c index da56daf57..e87de26be 100644 --- a/common_smp/src/tx_thread_reset.c +++ b/common_smp/src/tx_thread_reset.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -104,6 +106,28 @@ UINT status; status = TX_NOT_DONE; } } + + /* The state is terminal, but a terminal state on its own is not authorization + to rebuild the stack and move the thread back to TX_SUSPENDED. Both paths + that produce one -- thread completion in _tx_thread_shell_entry and thread + termination in _tx_thread_terminate -- publish TX_COMPLETED or + TX_TERMINATED, and then run this thread's exit notification callback, before + the thread has been detached from the ready list. The suspending flag is + set for exactly that interval. Resetting inside it would also defeat the + protection _tx_thread_system_resume relies on, which refuses to cancel a + suspension only while the state is still terminal. */ + if (status == TX_SUCCESS) + { + + /* Is the completion or termination transition still in progress? */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + + /* Yes, refuse the reset. The condition is transient: the caller may + retry once the transition has finished. */ + status = TX_NOT_DONE; + } + } } /* Is the request valid? */ diff --git a/common_smp/src/tx_thread_shell_entry.c b/common_smp/src/tx_thread_shell_entry.c index 45fe9d6da..c079f18e1 100644 --- a/common_smp/src/tx_thread_shell_entry.c +++ b/common_smp/src/tx_thread_shell_entry.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -134,6 +136,17 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the completion transition is marked as in + progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to cancel + an interrupted suspension -- interrupts stay disabled through the whole + transition -- but the notification callback and the completion extension + below are application code, reached by a direct call, and interrupt lockout + does not stop either of them from calling a thread lifecycle service on this + same control block. The flag is what _tx_thread_delete and _tx_thread_reset + test in order to refuse one. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -148,6 +161,15 @@ VOID (*entry_exit_notify)(TX_THREAD *notify_thread_ptr, UINT type); /* Perform any additional activities for tool or user purpose. */ TX_THREAD_COMPLETED_EXTENSION(thread_ptr) + /* Clear the suspending flag. It has to be cleared here rather than after the + call below, because _tx_thread_system_ni_suspend returns to the scheduler for + a thread that is the current thread, which this one is, and so does not come + back. Clearing it afterwards would leave a normally completed thread marked + as transitioning for ever and therefore permanently undeletable. Clearing it + here loses nothing: everything from this point to the detachment runs with + interrupts disabled and calls no application code. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Call actual non-interruptable thread suspension routine. */ _tx_thread_system_ni_suspend(thread_ptr, ((ULONG) 0)); diff --git a/common_smp/src/tx_thread_terminate.c b/common_smp/src/tx_thread_terminate.c index 86e34baeb..05ec91a1f 100644 --- a/common_smp/src/tx_thread_terminate.c +++ b/common_smp/src/tx_thread_terminate.c @@ -9,6 +9,8 @@ * SPDX-License-Identifier: MIT **************************************************************************/ +// Some portions generated by Claude Code (Opus 5). + /**************************************************************************/ /**************************************************************************/ @@ -133,6 +135,16 @@ ULONG suspension_sequence; #ifdef TX_NOT_INTERRUPTABLE + /* Set the suspending flag, so that the termination transition is marked + as in progress for the same interval it is marked in the interruptable + configuration below. Nothing in this configuration needs the flag to + cancel an interrupted suspension -- interrupts stay disabled through + the whole transition -- but the notification callback below is + application code, reached by a direct call, and interrupt lockout does + not stop it from calling a thread lifecycle service on this same + control block. The flag is cleared in the common tail below. */ + thread_ptr -> tx_thread_suspending = TX_TRUE; + #ifndef TX_DISABLE_NOTIFY_CALLBACKS /* Determine if an application callback routine is specified. */ @@ -225,20 +237,13 @@ ULONG suspension_sequence; (suspend_cleanup)(thread_ptr, suspension_sequence); } -#ifndef TX_NOT_INTERRUPTABLE - - /* Disable interrupts. */ - TX_DISABLE -#endif - - /* Clear the suspending flag. */ - thread_ptr -> tx_thread_suspending = TX_FALSE; - -#ifndef TX_NOT_INTERRUPTABLE - - /* Restore interrupts. */ - TX_RESTORE -#endif + /* The suspending flag deliberately stays set here. It used to be + cleared at this point, which left the terminated extension and the + notification callback below -- both application code -- free to delete + or reset this control block while this service still held a pointer to + it and still had mutex-release processing to do. It is now cleared + once, in the common tail below, after the last dereference of the + target. */ /* Perform any additional activities for tool or user purpose. */ TX_THREAD_TERMINATED_EXTENSION(thread_ptr) @@ -282,6 +287,20 @@ ULONG suspension_sequence; TX_DISABLE #endif + /* Clear the suspending flag. This is the one place the end of the + termination transition is published, and it is reached from both branches + above, after the notification callback and after the mutex-release + processing, which is the last thing in this service to dereference the + target. Until this store, _tx_thread_delete and _tx_thread_reset refuse + the target. In the interruptable ready branch the flag is already false, + because _tx_thread_system_suspend cleared it when it detached the thread, + so this is a second store of a value the flag already holds; that is + cheaper than testing for it and it keeps the transition to a single clear + site. No suspension-initiating service can have set it again in between: + every one of them acts on a thread that is ready or suspended, and this + thread is terminated. */ + thread_ptr -> tx_thread_suspending = TX_FALSE; + /* Enable preemption. */ _tx_thread_preempt_disable--; diff --git a/test/smp/cmake/regression/CMakeLists.txt b/test/smp/cmake/regression/CMakeLists.txt index af33ca3f0..dd1d48a30 100644 --- a/test/smp/cmake/regression/CMakeLists.txt +++ b/test/smp/cmake/regression/CMakeLists.txt @@ -115,6 +115,8 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c ${SOURCE_DIR}/threadx_thread_stack_checking_test.c ${SOURCE_DIR}/threadx_thread_terminate_delete_test.c + ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c + ${SOURCE_DIR}/threadx_smp_thread_exit_callback_remote_test.c ${SOURCE_DIR}/threadx_thread_time_slice_change_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_test.c diff --git a/test/smp/regression/testcontrol.c b/test/smp/regression/testcontrol.c index c0ba34636..59d8e69a7 100644 --- a/test/smp/regression/testcontrol.c +++ b/test/smp/regression/testcontrol.c @@ -240,6 +240,8 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *); void threadx_thread_sleep_for_100ticks_application_define(void *); void threadx_thread_multiple_sleep_application_define(void *); void threadx_thread_terminate_delete_application_define(void *); +void threadx_thread_exit_callback_transition_application_define(void *); +void threadx_smp_thread_exit_callback_remote_application_define(void *); void threadx_thread_preemption_change_application_define(void *); void threadx_thread_priority_change_application_define(void *); void threadx_thread_time_slice_change_application_define(void *); @@ -384,6 +386,8 @@ TEST_ENTRY test_control_tests[] = threadx_thread_sleep_for_100ticks_application_define, threadx_thread_multiple_sleep_application_define, threadx_thread_terminate_delete_application_define, + threadx_thread_exit_callback_transition_application_define, + threadx_smp_thread_exit_callback_remote_application_define, threadx_thread_priority_change_application_define, diff --git a/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c new file mode 100644 index 000000000..488c28226 --- /dev/null +++ b/test/smp/regression/threadx_smp_thread_exit_callback_remote_test.c @@ -0,0 +1,514 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/* Define the ThreadX SMP thread exit callback remote-core termination test. + + This is the SMP-specific half of the exit-callback transition coverage. The + deterministic cases are in threadx_thread_exit_callback_transition_test, + which confines every thread it drives to core 0 precisely so that they + cannot be made flaky by concurrency; this test supplies the case that needs + it. + + The victim is excluded from every core but core 1 and spins there without + relinquishing, so it is genuinely executing on another core when the + controller -- excluded to core 0 -- terminates it. _tx_thread_terminate + publishes TX_TERMINATED, releases the global SMP protection at TX_RESTORE, + and calls the victim's exit notification callback in the controller's + context on core 0. On an unfixed kernel a callback-side reset of the victim + succeeds at that point, and _tx_thread_reset both memsets the victim's stack + to the fill pattern and rebuilds its initial frame -- over the stack a live + core is running on. + + What is measured: + + - that the victim really was mapped to and executing on another core when + termination began, which is the precondition; if it cannot be + established within a bounded wait the test says so and drops only the + assertions that depend on it, rather than reporting a pass it did not + earn or spinning until something changes, + - that the callback-side delete and reset are refused, from the wrapper + and from the core service alike, + - that the victim's stack is untouched, checked with a sentinel written + into the unused low end of the stack, which a reset's fill would erase, + - that after the service returns the victim is in no core's execute slot + and on no ready list. + + Every wait is bounded and every list walk is bounded, so neither a failing + kernel nor an unlucky schedule can turn this into a hang. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 + +/* The core the victim is pinned to, and the exclusion maps that pin it there + and the controller to core 0. */ + +#define VICTIM_CORE 1 +#define EXCLUDE_ALL_BUT_CORE_0 0xE +#define EXCLUDE_ALL_BUT_CORE_1 0xD + +/* Bound on the wait for the victim to be mapped to its core. */ + +#define REMOTE_WAIT_TICKS 200 + +/* Sentinel written into the unused low end of the victim's stack. A reset + memsets the whole stack to the fill pattern before rebuilding the frame, so + the sentinel is gone if a reset ran. Its offset keeps it clear of both the + frame a downward-growing stack builds at the top and the stack-checking + pattern the kernel inspects at the very bottom. */ + +#define SENTINEL_OFFSET 64 +#define SENTINEL_WORDS 8 +#define SENTINEL_VALUE 0xA5A5A5A5UL + +#define READY_RING_LIMIT 32 + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + +#define FAILED_TAG_LIMIT 16 + + +static TX_THREAD controller; +static TX_THREAD victim; + +static CHAR *victim_stack; + +static volatile ULONG victim_spins = 0; +static volatile UINT victim_run = TX_FALSE; + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_core_mapped; +static UINT probe_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_delete_status = PROBE_NOT_ATTEMPTED; +static UINT probe_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_core_reset_status = PROBE_NOT_ATTEMPTED; +static UINT probe_ran = TX_FALSE; +static UINT remote_confirmed = TX_FALSE; +static UINT sentinel_intact_after = TX_FALSE; + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define thread prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void write_sentinel(void) +{ + +ULONG *words; +UINT index; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + + for (index = 0; index < SENTINEL_WORDS; index++) + { + words[index] = SENTINEL_VALUE; + } +} + + +static UINT sentinel_intact(void) +{ + +ULONG *words; +UINT index; +UINT intact; + + + words = (ULONG *) (victim_stack + SENTINEL_OFFSET); + intact = TX_TRUE; + + for (index = 0; index < SENTINEL_WORDS; index++) + { + if (words[index] != SENTINEL_VALUE) + { + intact = TX_FALSE; + } + } + + return(intact); +} + + +/* Report whether thread_ptr appears in any core's execute slot or on any ready + list. Both walks are bounded. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT priority; +UINT steps; +UINT core; +UINT detached; + + + detached = TX_TRUE; + + TX_DISABLE + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + if (walk_ptr == thread_ptr) + { + detached = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + } + + TX_RESTORE + + return(detached); +} + + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + if (thread_ptr != &victim) + { + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + probe_ran = TX_TRUE; + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + probe_core_mapped = thread_ptr -> tx_thread_smp_core_mapped; + + /* The delete first, then the reset. Each core service is called only if + its wrapper already refused, so that a kernel which allows the operation + is not asked to perform it twice. */ + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status != TX_SUCCESS) + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status != TX_SUCCESS) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + } + } +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_smp_thread_exit_callback_remote_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_smp_core_exclude(&controller, EXCLUDE_ALL_BUT_CORE_0); + + if (status != TX_SUCCESS) + { + + printf("Running SMP Thread Exit Callback Remote Test............... ERROR #1\n"); + test_control_return(1); + } +} + + +static void controller_entry(ULONG thread_input) +{ + +UINT status; +UINT notify_status; +UINT index; +ULONG waited; +ULONG spins_before; + + + (void) thread_input; + + printf("Running SMP Thread Exit Callback Remote Test............... "); + + victim_run = TX_TRUE; + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + + status += tx_thread_smp_core_exclude(&victim, EXCLUDE_ALL_BUT_CORE_1); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + + expect_status(status, TX_SUCCESS, 10); + + /* The sentinel goes in after the create, because the create fills the whole + stack with the pattern the stack-checking configurations inspect. */ + write_sentinel(); + expect(sentinel_intact(), 11); + + if (expectations_failed == 0) + { + expect_status(tx_thread_resume(&victim), TX_SUCCESS, 12); + } + + /* Wait, bounded, for the victim to be both mapped to its core and selected + to execute there, and for it to have actually spun. Those three + together are the precondition this test exists for. */ + + waited = 0; + + while ((waited < REMOTE_WAIT_TICKS) && (remote_confirmed == TX_FALSE)) + { + + if ((_tx_thread_execute_ptr[VICTIM_CORE] == &victim) && + (victim.tx_thread_smp_core_mapped == ((UINT) VICTIM_CORE)) && + (victim_spins > 0)) + { + remote_confirmed = TX_TRUE; + } + else + { + tx_thread_sleep(1); + waited++; + } + } + + /* Confirm it is still spinning, so that the termination below really does + start against a running remote thread rather than a stalled one. */ + + spins_before = victim_spins; + + if (remote_confirmed == TX_TRUE) + { + tx_thread_sleep(2); + expect(((victim_spins > spins_before) ? TX_TRUE : TX_FALSE), 13); + } + else + { + + /* Say so rather than assert it. The refusal assertions below do not + depend on the victim being remote, so they still run; the ones that + do are dropped, and this line records that the environment did not + provide the precondition. */ + printf("(remote precondition not established in %u ticks; remote-only checks skipped) ", + (UINT) REMOTE_WAIT_TICKS); + } + + if (expectations_failed == 0) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 20); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 21); + expect_status(probe_state, TX_TERMINATED, 22); + + /* The transition marker is set across the callback, which is what the + two refusals below rest on. */ + expect_status(probe_suspending, TX_TRUE, 23); + + expect_status(probe_delete_status, TX_DELETE_ERROR, 24); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 25); + expect_status(probe_reset_status, TX_NOT_DONE, 26); + expect_status(probe_core_reset_status, TX_NOT_DONE, 27); + + if (remote_confirmed == TX_TRUE) + { + + /* The callback saw the victim still mapped to the remote core. */ + expect_status(probe_core_mapped, (UINT) VICTIM_CORE, 28); + } +#endif + + /* The reset was refused, so nothing memset the stack a live core was + running on. */ + sentinel_intact_after = sentinel_intact(); + expect_status(sentinel_intact_after, TX_TRUE, 29); + + /* And the termination completed: the marker is clear, and the victim is + in no core's execute slot and on no ready list. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 30); + expect_status(victim.tx_thread_suspending, TX_FALSE, 31); + expect(fully_detached(&victim), 32); + } + + /* The victim's loop exits on this, for the case where it is still between + the terminate and its core acting on the preemption. */ + victim_run = TX_FALSE; + + if (expectations_failed == 0) + { + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 40); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + /* Spin without relinquishing, so this thread occupies its core rather than + yielding it. */ + while (victim_run == TX_TRUE) + { + victim_spins++; + } +} diff --git a/test/smp/regression/threadx_thread_exit_callback_transition_test.c b/test/smp/regression/threadx_thread_exit_callback_transition_test.c new file mode 100644 index 000000000..c931085c5 --- /dev/null +++ b/test/smp/regression/threadx_thread_exit_callback_transition_test.c @@ -0,0 +1,1375 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/* This test covers the completion and termination transitions that a thread's + TX_THREAD_EXIT notification callback runs inside. + + _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal + state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit + notification callback, before the thread has been detached from the ready + list and before either service has finished with the pointer it holds to + the control block. That terminal state is the same state _tx_thread_delete + and _tx_thread_reset accept as authorization to invalidate or rebuild the + control block, so a callback used to be able to delete the object -- and + then lawfully recreate it over the same memory -- while the scheduler was + still linked to the old incarnation, or to reset a terminated thread and + resume it back onto the ready list after termination had reported success. + + The tx_thread_suspending flag marks the transition for exactly its + duration, and delete and reset now refuse a target whose flag is still set. + The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those + two services already document, and the same call succeeds once the + transition has finished. + + What the test measures, in every scenario: + + 1. what the callback observes -- the published state, the transition + flag, and the status of each lifecycle service it can reach, + 2. that the scheduler's ready lists, priority map and execute pointer + stay consistent with the control blocks they point at, taken as one + snapshot under interrupt lockout so that the reading is not itself a + race, + 3. that the documented behaviour still works afterwards: the same delete + and reset succeed once the transition has completed, so nothing has + become permanently undeletable. + + The consequences are demonstrated rather than described, and deliberately + from the terminator's context rather than the completing thread's: + + - scenario A has the callback recreate the control block if its delete + unexpectedly succeeded, at a lower priority, so that the outcome is an + assertable disagreement -- the old priority list still heading at a + control block whose own priority field names a different list -- rather + than a fault; + - scenario B has the callback reset and then resume the target, so that a + kernel which allows the reset is caught reporting a successful + termination for a thread that is runnable again. + + Both run while the victim is a ready thread that is not the current thread. + That ordering is not cosmetic. On the Linux, Win32 and Win64 host + simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the + host thread backing the deleted thread, so a callback-side delete of the + *completing* thread destroys the host thread the callback is running on and + wedges the simulator instead of reaching an assertion. The completion + scenarios therefore come last, and the scenario gate below stops the run + before them if anything earlier has already failed. + + Every list walk is bounded, so a corrupted ring costs an assertion and not + a hang. Expectations are counted rather than short-circuited within a + scenario, so a failing kernel reports how much it failed by; progression + between scenarios is gated on there being no failures yet, so a kernel that + has already corrupted the scheduler is not driven further into it. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +/* Priorities. The controller sits above every thread it drives. The victim + shares its priority with two peers, so that both the multi-member and the + single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is + below VICTIM_PRIORITY on purpose: see the note in the callback. */ + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 +#define RECREATE_PRIORITY 21 + +/* Upper bound on a ready-ring walk. The suite never has this many threads at + one priority, so exceeding it means the ring is broken rather than long. */ + +#define READY_RING_LIMIT 32 + +/* Sentinel for a probe slot the callback did not reach. */ + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + + +static unsigned long controller_counter = 0; +static unsigned long victim_counter = 0; +static unsigned long peer_a_counter = 0; +static unsigned long peer_b_counter = 0; +static unsigned long replacement_counter = 0; +static unsigned long callback_entry_counter = 0; +static unsigned long callback_exit_counter = 0; + +static TX_THREAD controller; +static TX_THREAD victim; +static TX_THREAD peer_a; +static TX_THREAD peer_b; +static TX_SEMAPHORE victim_semaphore; +static TX_MUTEX victim_mutex; + +static CHAR *victim_stack; +static CHAR *replacement_stack; + +/* What the victim's entry function should do before it returns. */ + +static volatile UINT victim_hold = TX_FALSE; +static volatile UINT victim_self_terminate = TX_FALSE; +static volatile UINT victim_take_mutex = TX_FALSE; +static volatile UINT victim_wait_semaphore = TX_FALSE; +static volatile UINT peers_run = TX_FALSE; + +/* What the exit callback should attempt the next time it runs. Independent + flags rather than one mode, because a delete that unexpectedly succeeds + destroys the object and would mask the reset measurement behind it. */ + +static volatile UINT probe_delete_enabled = TX_FALSE; +static volatile UINT probe_reset_enabled = TX_FALSE; +static volatile UINT probe_misc_enabled = TX_FALSE; +static volatile UINT probe_reuse_enabled = TX_FALSE; + +/* What the callback saw the last time it probed. */ + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_delete_status; +static UINT probe_core_delete_status; +static UINT probe_reset_status; +static UINT probe_core_reset_status; +static UINT probe_resume_status; +static UINT probe_suspend_status; +static UINT probe_terminate_status; +static UINT probe_create_status; +static UINT probe_consistent; +static UINT probe_ran; +static UINT delete_succeeded; + +/* Ordering witness for the notification pair. Entry and exit counts cannot + simply be compared: a victim created with TX_DONT_START never reports an + entry, and an incarnation that is reset and terminated again reports a + second exit with no second entry. What must hold is that an exit for a + thread that ran is preceded by that thread's entry. */ + +static UINT last_notify_type = PROBE_NOT_ATTEMPTED; +static UINT exit_saw_entry_first = TX_FALSE; + +/* Expectation accounting. */ + +#define FAILED_TAG_LIMIT 32 + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define task prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); +static void peer_entry(ULONG thread_input); +static void replacement_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +/* Record one expectation. tag identifies it in the failure report; the first + failing tag is the number the suite prints, so tags are allocated in the + order the expectations are written. */ + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + /* Keep the tags, so that a failing kernel reports which expectations + it failed rather than only the first and a total. */ + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_pointer(void *actual, void *wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_count(unsigned long actual, unsigned long wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +/* Walk one ready ring and report whether thread_ptr is a member of it. The + walk is bounded, so a ring that does not close is reported as a + non-membership rather than spun on for ever. */ + +static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr) +{ + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT steps; +UINT found; + + + found = TX_FALSE; + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE)) + { + + if (walk_ptr == thread_ptr) + { + found = TX_TRUE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + /* The ring has closed. */ + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + return(found); +} + + +/* Take one snapshot of the scheduler's ready structures and check that they + agree with the control blocks they point at. + + This is the assertion the pre-fix behaviour fails. A callback that deletes + the victim and recreates it at another priority leaves the old priority list + heading at that control block while the block's own priority field names the + new list, and leaves the old priority's map bit set with nothing behind it. + Neither disagreement needs a fault to be visible. + + The whole snapshot is taken under interrupt lockout -- which is the global + SMP protection on an SMP port -- so that reading the scheduler's state is + not itself a race against another core. + + transitioning names the one thread that is allowed to be on a ready list + without being ready, which is precisely the thread whose completion or + termination transition is in progress: the terminal state is published + before the detachment, so inside the callback the victim is legitimately a + TX_COMPLETED or TX_TERMINATED member of its ready ring. That single + exemption is from the state clause only. Every other clause, and in + particular the clause that requires a ready-list member to agree that this + is its priority -- the one a callback-side delete and recreate breaks -- + still applies to it. */ + +static UINT scheduler_consistent_except(TX_THREAD *transitioning) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *head_ptr; +TX_THREAD *walk_ptr; +TX_THREAD *execute_ptr; +UINT priority; +UINT steps; +UINT consistent; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + consistent = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + +#if TX_MAX_PRIORITIES <= 32 + + /* A priority with a ready list must have its map bit set, and a + priority with no ready list must not. */ + if (head_ptr == TX_NULL) + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } + else + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } +#endif + + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + /* Every thread on a ready list must be a live thread, must be + ready, and must agree that this is its priority. */ + if (walk_ptr -> tx_thread_id != TX_THREAD_ID) + { + consistent = TX_FALSE; + } + + if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning)) + { + consistent = TX_FALSE; + } + + if (walk_ptr -> tx_thread_priority != priority) + { + consistent = TX_FALSE; + } + + /* The ring must be doubly linked. */ + if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr) + { + consistent = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + /* A ring that did not close inside the bound is broken. */ + if (walk_ptr != TX_NULL) + { + consistent = TX_FALSE; + } + } + + /* Whatever the scheduler has selected to execute must be a member of the + ready list that its own priority field names. */ +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + + execute_ptr = _tx_thread_execute_ptr[core]; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } + } +#else + + execute_ptr = _tx_thread_execute_ptr; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } +#endif + + TX_RESTORE + + return(consistent); +} + + +static UINT scheduler_consistent(void) +{ + return(scheduler_consistent_except(TX_NULL)); +} + + +/* Report whether thread_ptr is absent from every ready list and from every + execute slot, which is what a completed or terminated thread must be once + its transition has finished. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +UINT priority; +UINT detached; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + detached = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + if (ready_ring_contains(priority, thread_ptr) == TX_TRUE) + { + detached = TX_FALSE; + } + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } +#else + + if (_tx_thread_execute_ptr == thread_ptr) + { + detached = TX_FALSE; + } +#endif + + TX_RESTORE + + return(detached); +} + + +static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse) +{ + + probe_state = PROBE_NOT_ATTEMPTED; + probe_suspending = PROBE_NOT_ATTEMPTED; + probe_delete_status = PROBE_NOT_ATTEMPTED; + probe_core_delete_status = PROBE_NOT_ATTEMPTED; + probe_reset_status = PROBE_NOT_ATTEMPTED; + probe_core_reset_status = PROBE_NOT_ATTEMPTED; + probe_resume_status = PROBE_NOT_ATTEMPTED; + probe_suspend_status = PROBE_NOT_ATTEMPTED; + probe_terminate_status = PROBE_NOT_ATTEMPTED; + probe_create_status = PROBE_NOT_ATTEMPTED; + probe_consistent = PROBE_NOT_ATTEMPTED; + probe_ran = TX_FALSE; + delete_succeeded = TX_FALSE; + + probe_delete_enabled = probe_delete; + probe_reset_enabled = probe_reset; + probe_misc_enabled = probe_misc; + probe_reuse_enabled = probe_reuse; +} + + +/* The exit notification callback. Everything it learns is recorded for the + controller to check; it prints nothing and it never suspends, so it obeys + the one restriction the manual places on a notification callback. */ + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + /* Only the victim's notifications are of interest. Its control block is + reused across scenarios, so the pointer is the identity. */ + if (thread_ptr != &victim) + { + return; + } + + if (type == TX_THREAD_ENTRY) + { + callback_entry_counter++; + last_notify_type = TX_THREAD_ENTRY; + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_exit_counter++; + + if (last_notify_type == TX_THREAD_ENTRY) + { + exit_saw_entry_first = TX_TRUE; + } + + last_notify_type = TX_THREAD_EXIT; + + if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) && + (probe_misc_enabled == TX_FALSE)) + { + return; + } + + probe_ran = TX_TRUE; + + /* What the kernel has published about the thread at this point. */ + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + + /* None of the services below has a suspension option, which is the only + thing the manual forbids a callback from using, so every one of them is + reachable from here. + + Where a service is expected to refuse, the core service is called after + the _txe_ wrapper as well. The wrapper is what an ordinary build + reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build + reaches directly, and the refusal has to live there so that disabling + error checking cannot disable it. The core call is made only when the + wrapper already refused, so that a kernel which allows the operation is + not asked to perform it twice. */ + + if (probe_delete_enabled == TX_TRUE) + { + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + else + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + } + + /* Recreate the control block only if the delete unexpectedly + succeeded. On a fixed kernel this never runs. On an unfixed one it + is the lawful next step after a successful delete, and it is what + turns the window into an observable inconsistency: tx_thread_create + zeroes the whole control block, and the auto-start then links it + onto RECREATE_PRIORITY's ready list while the old incarnation is + still the head of VICTIM_PRIORITY's. + + RECREATE_PRIORITY is the lower of the two so that the stale head is + the one the scheduler selects from, and so that the ring at the new + priority has a single member. Recreating at the same priority would + instead have the auto-start write through the control block's + freshly zeroed ready_previous pointer, which is a fault rather than + an assertion. */ + if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE)) + { + probe_create_status = tx_thread_create(thread_ptr, "replacement", + replacement_entry, 0, + replacement_stack, + TEST_STACK_SIZE_PRINTF, + RECREATE_PRIORITY, RECREATE_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + } + } + + if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + + /* Follow the reset with the resume that the reset would have unlocked. + On a fixed kernel the state is still terminal and the resume is + refused. On an unfixed one the reset has moved the state to + TX_SUSPENDED, the resume finds a suspending thread whose state is no + longer terminal, clears the flag and puts the thread back on the + ready list -- and the outer service then skips the removal and + reports success for a termination that did not happen. */ + probe_resume_status = tx_thread_resume(thread_ptr); + } + + if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + probe_suspend_status = tx_thread_suspend(thread_ptr); + probe_terminate_status = tx_thread_terminate(thread_ptr); + } + + probe_consistent = scheduler_consistent_except(thread_ptr); +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Put first available memory address into a character pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_a, "peer a", peer_entry, 1, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_b, "peer b", peer_entry, 2, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + /* The victim's control block is created and destroyed repeatedly, so its + two stacks are set aside here rather than per scenario. */ + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + replacement_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0); + status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #1\n"); + test_control_return(1); + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + /* Confine every thread this test drives to core 0. The measurements below + are of global scheduler structures at chosen moments, and letting the + peers and the victim run on other cores would make them a race against + the test rather than a property of it. A victim executing on a + different core when termination begins is deliberately covered by a + separate SMP test, so that the deterministic cases here cannot be made + flaky by it. */ + status = tx_thread_smp_core_exclude(&controller, 0xE); + status += tx_thread_smp_core_exclude(&peer_a, 0xE); + status += tx_thread_smp_core_exclude(&peer_b, 0xE); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #2\n"); + test_control_return(1); + } +#endif +} + + +/* Create the victim over its own control block, with the exit callback + registered. The notify status is folded into the result according to what + the configuration supports. */ + +static UINT create_victim(UINT priority, UINT auto_start) +{ + +UINT status; +UINT notify_status; + + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + priority, priority, TX_NO_TIME_SLICE, auto_start); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + /* Notification is compiled out, so the registration is expected to be + refused and the callback never runs. */ + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + +#ifdef TX_THREAD_SMP_MAX_CORES + + if (status == TX_SUCCESS) + { + status = tx_thread_smp_core_exclude(&victim, 0xE); + } +#endif + + return(status); +} + + +/* The gate between scenarios. A kernel that has already failed an expectation + has, in these scenarios, already left the scheduler inconsistent, and + driving it further would report nothing more while risking a wedge instead + of a verdict. */ + +static UINT ok_so_far(void) +{ + +UINT result; + + + result = TX_FALSE; + + if (expectations_failed == 0) + { + result = TX_TRUE; + } + + return(result); +} + + +static void controller_entry(ULONG thread_input) +{ + +ULONG expected_exits; +ULONG expected_entries; +UINT index; + + + (void) thread_input; + + controller_counter++; + expected_exits = 0; + expected_entries = 0; + + /* Inform user. */ + printf("Running Thread Exit Callback Transition Test............... "); + + /* The scheduler must be self-consistent before anything is measured, so + that a later failure is attributable to this test. */ + expect(scheduler_consistent(), 10); + + /* ------------------------------------------------------------------- */ + /* Scenario A: terminate a ready target that is not the current thread, */ + /* with two peers ready at the same priority. The callback attempts */ + /* the delete, and recreates the control block if it succeeded. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_TRUE; + victim_hold = TX_TRUE; + + expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11); + expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12); + + arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim and both peers become ready and start spinning, so + that the victim's unlink is from a multi-member ring. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 14); + expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 19); + + /* The callback runs after the terminal state has been published, and + inside the transition, which is what the flag records. */ + expect_status(probe_state, TX_TERMINATED, 20); + expect_status(probe_suspending, TX_TRUE, 21); + + /* Both the wrapper and the core service must refuse. */ + expect_status(probe_delete_status, TX_DELETE_ERROR, 22); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23); + + /* So the recreate never happened and nothing is inconsistent. */ + expect_status(delete_succeeded, TX_FALSE, 24); + expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25); + expect_status(probe_consistent, TX_TRUE, 26); + expect_count(replacement_counter, 0, 27); +#endif + + /* After the service returns the termination is complete: the state is + terminal, the marker is clear, the target is on no ready list and in + no execute slot, and the peers are undisturbed. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 28); + expect_status(victim.tx_thread_suspending, TX_FALSE, 29); + expect(fully_detached(&victim), 30); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32); + expect(scheduler_consistent(), 33); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* The documented behaviour is available again. */ + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34); + } + + /* ------------------------------------------------------------------- */ + /* Scenario B: the same termination, but the callback attempts a reset */ + /* and then the resume that a successful reset would have unlocked. */ + /* */ + /* This is the leg where the transition check is the only thing */ + /* standing between the callback and a reset of a thread that is still */ + /* on the ready list. Without it the reset succeeds, the resume */ + /* clears the suspending flag and restores TX_READY, the outer service */ + /* then finds the flag clear and skips the removal, and */ + /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 41); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 43); + expect_status(probe_state, TX_TERMINATED, 44); + expect_status(probe_suspending, TX_TRUE, 45); + + /* The discriminating leg. The target is not the current thread and + its published state is terminal, so both of the conditions the + service tested before this fix are satisfied. */ + expect_status(probe_reset_status, TX_NOT_DONE, 46); + expect_status(probe_core_reset_status, TX_NOT_DONE, 47); + + /* With the reset refused, the state is still terminal, so the resume + cannot reach the branch that would void the termination. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 48); + expect_status(probe_consistent, TX_TRUE, 49); +#endif + + /* And the termination stands. A kernel that let the callback reset + and resume the target arrives here with the victim ready to run. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 50); + expect_status(victim.tx_thread_suspending, TX_FALSE, 51); + expect(fully_detached(&victim), 52); + expect(scheduler_consistent(), 53); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56); + } + + /* Retire the peers, so that the ring drops to a single member and then to + none, and check consistency across both unlinks. */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_FALSE; + tx_thread_sleep(3); + + expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57); + expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58); + expect(scheduler_consistent(), 59); + expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60); + expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61); + } + + /* ------------------------------------------------------------------- */ + /* Scenario C: terminate a target suspended on an object, so that the */ + /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */ + /* the terminated extension, the callback, and mutex release, in that */ + /* order. That branch used to clear the marker before the callback */ + /* rather than after the last dereference of the target. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + victim_hold = TX_FALSE; + victim_take_mutex = TX_TRUE; + victim_wait_semaphore = TX_TRUE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim take the mutex and suspend on the semaphore. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71); + expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 74); + expect_status(probe_state, TX_TERMINATED, 75); + + /* The discriminating leg for this branch. The marker used to be + cleared before this point, which left the callback free to delete or + reset a control block that the service still had mutex-release + processing to do against. */ + expect_status(probe_suspending, TX_TRUE, 76); + expect_status(probe_delete_status, TX_DELETE_ERROR, 77); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78); + expect_status(probe_reset_status, TX_NOT_DONE, 79); + expect_status(probe_core_reset_status, TX_NOT_DONE, 80); + expect_status(probe_resume_status, TX_RESUME_ERROR, 81); + expect_status(delete_succeeded, TX_FALSE, 82); + expect_status(probe_consistent, TX_TRUE, 83); +#endif + + /* The marker is cleared at the safe point, after the mutex release, + and the mutex the terminated thread owned has been given up. The + value of the flag during the release itself is not observable from + application code; what is observable is that there is exactly one + clear site and that it sits downstream of the release. */ + expect_status(victim.tx_thread_suspending, TX_FALSE, 84); + expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85); + expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86); + expect(fully_detached(&victim), 87); + expect(scheduler_consistent(), 88); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89); + } + + /* ------------------------------------------------------------------- */ + /* Scenario D: natural completion, victim alone at its priority. */ + /* */ + /* The callback here runs on the completing thread itself, so a */ + /* callback-side delete that succeeds destroys the host thread on the */ + /* simulation ports. This scenario is therefore placed after the ones */ + /* that reach an assertion, and the gate above stops the run before it */ + /* if any of them has already failed. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE); + victim_take_mutex = TX_FALSE; + victim_wait_semaphore = TX_FALSE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim run to completion, so that its callback runs inside + the completion transition. */ + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 101); + expect_status(probe_state, TX_COMPLETED, 102); + expect_status(probe_suspending, TX_TRUE, 103); + expect_status(probe_delete_status, TX_DELETE_ERROR, 104); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105); + + /* Reset of the running thread is refused for two independent reasons + here -- it is the current thread, and its transition is in progress + -- so this leg is a guard rather than a discriminator. Scenario B + is where the transition check is the only thing refusing a reset. */ + expect_status(probe_reset_status, TX_NOT_DONE, 106); + expect_status(probe_core_reset_status, TX_NOT_DONE, 107); + + /* A completed thread is neither resumable nor suspendable, and + terminating one is a documented no-op. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 108); + expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109); + expect_status(probe_terminate_status, TX_SUCCESS, 110); + expect_status(delete_succeeded, TX_FALSE, 111); + expect_status(probe_consistent, TX_TRUE, 112); +#endif + + /* The transition has finished, so the calls the callback was refused + must succeed. The refusal is transient and nothing has become + permanently undeletable. */ + expect_status(victim.tx_thread_state, TX_COMPLETED, 113); + expect_status(victim.tx_thread_suspending, TX_FALSE, 114); + expect(fully_detached(&victim), 115); + expect(scheduler_consistent(), 116); + expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* Reset, so that the documented reset of a completed thread is covered + on a target whose transition has finished, and so that the + reset/terminate/delete order is exercised on this branch too. */ + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118); + expect_status(victim.tx_thread_state, TX_SUSPENDED, 119); + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121); + expect_status(victim.tx_thread_suspending, TX_FALSE, 122); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123); + } + + /* A safe post-completion recreate at a different priority, which is the + lifecycle the callback was refused, performed at the point it is + legitimate. It exercises the priority map and the list heads for a + recreate that moves priority as well as for one that does not. */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124); + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_COMPLETED, 125); + expect(scheduler_consistent(), 126); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127); + } + + /* ------------------------------------------------------------------- */ + /* Scenario E: self termination. The callback fires once, in the */ + /* victim's own context, the lifecycle mutation is refused there too, */ + /* and another thread can complete the lifecycle afterwards. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_TRUE; + expected_exits = callback_exit_counter; + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Exactly one exit notification, even though the thread terminated + itself from inside its own entry function. */ + expect_count(callback_exit_counter, expected_exits + 1, 131); + expect_status(probe_state, TX_TERMINATED, 132); + expect_status(probe_suspending, TX_TRUE, 133); + expect_status(probe_delete_status, TX_DELETE_ERROR, 134); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135); + expect_status(probe_reset_status, TX_NOT_DONE, 136); + expect_status(delete_succeeded, TX_FALSE, 137); + expect_status(probe_consistent, TX_TRUE, 138); +#else + + expect_count(callback_exit_counter, expected_exits, 139); +#endif + + expect_status(victim.tx_thread_state, TX_TERMINATED, 140); + expect_status(victim.tx_thread_suspending, TX_FALSE, 141); + expect(fully_detached(&victim), 142); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145); + expect(scheduler_consistent(), 146); + } + + /* ------------------------------------------------------------------- */ + /* Scenario F: a benign callback is unaffected. The notification */ + /* ordering and counts are what they were. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_FALSE; + expected_exits = callback_exit_counter; + expected_entries = callback_entry_counter; + exit_saw_entry_first = TX_FALSE; + last_notify_type = PROBE_NOT_ATTEMPTED; + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* This incarnation ran and completed, so it reported exactly one entry + and one exit, and the entry came first. The transition marker has + changed neither how often notifications are delivered nor in what + order. */ + expect_count(callback_exit_counter, expected_exits + 1, 151); + expect_count(callback_entry_counter, expected_entries + 1, 152); + expect_status(exit_saw_entry_first, TX_TRUE, 153); +#else + + expect_count(callback_exit_counter, expected_exits, 154); + expect_count(callback_entry_counter, expected_entries, 155); +#endif + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156); + expect(scheduler_consistent(), 157); + } + + /* ------------------------------------------------------------------- */ + /* Scenario G: state and boundary cases. A thread that never entered a */ + /* terminal state, and one that is already fully detached, behave as */ + /* documented -- the new refusal applies only while the marker is set. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170); + + /* Suspended, never run: neither state is terminal, so both services + are refused for the reason they always were, and the marker is not + set on a thread that is not transitioning. */ + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171); + expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172); + expect_status(victim.tx_thread_suspending, TX_FALSE, 173); + + /* Terminated from a suspended state: deletable immediately, and + repeatedly resettable and re-terminable in between. */ + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174); + expect_status(victim.tx_thread_suspending, TX_FALSE, 175); + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179); + expect(scheduler_consistent(), 180); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + if (expectations_failed > ((unsigned long) failed_tag_count)) + { + printf(" ..."); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + victim_counter++; + + if (victim_take_mutex == TX_TRUE) + { + tx_mutex_get(&victim_mutex, TX_NO_WAIT); + } + + if (victim_wait_semaphore == TX_TRUE) + { + + /* Suspend on an object, so that a termination takes the non-ready + branch and runs the suspension cleanup routine. */ + tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER); + } + + if (victim_self_terminate == TX_TRUE) + { + + /* This does not return: the termination detaches this thread and the + preemption check at the end of the service switches away from it. */ + tx_thread_terminate(tx_thread_identify()); + } + + while (victim_hold == TX_TRUE) + { + + /* Stay ready, so that a terminate issued by another thread finds this + thread on the ready list. */ + tx_thread_relinquish(); + } + + /* Returning enters the completion transition. */ +} + + +static void peer_entry(ULONG thread_input) +{ + + if (thread_input == 1) + { + peer_a_counter++; + } + else + { + peer_b_counter++; + } + + while (peers_run == TX_TRUE) + { + + /* Stay ready at the victim's priority, so that the victim's ready ring + has more than one member. */ + tx_thread_relinquish(); + } +} + + +static void replacement_entry(ULONG thread_input) +{ + + (void) thread_input; + + replacement_counter++; + + /* Only reached on a kernel that let the callback recreate the control + block. Suspend rather than return, so that the run does not depend on a + second completion transition over a control block already known to be + inconsistent. */ + tx_thread_suspend(tx_thread_identify()); +} diff --git a/test/tx/cmake/CMakeLists.txt b/test/tx/cmake/CMakeLists.txt index b2f0ab1fe..235a28d2e 100644 --- a/test/tx/cmake/CMakeLists.txt +++ b/test/tx/cmake/CMakeLists.txt @@ -61,6 +61,15 @@ add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/../../.. threadx) add_subdirectory(regression) add_subdirectory(samples) +# The two configurations this change has to reach, TX_NOT_INTERRUPTABLE and +# TX_DISABLE_ERROR_CHECKING, are not among the five above and cannot be reached +# from the regression suite, which links one library per configuration. This +# directory compiles the sources concerned directly. It is listed last rather than +# beside regression on purpose: every branch in the current fix set adds an +# add_subdirectory line immediately after regression, and putting a second one +# there turns a set of clean merges into a set of one-line conflicts. +add_subdirectory(thread_transition) + # Coverage # # The gate here used to be the build type alone, and only one of the five diff --git a/test/tx/cmake/regression/CMakeLists.txt b/test/tx/cmake/regression/CMakeLists.txt index 7a0296dc1..1479a93e0 100644 --- a/test/tx/cmake/regression/CMakeLists.txt +++ b/test/tx/cmake/regression/CMakeLists.txt @@ -98,6 +98,7 @@ set(regression_test_cases ${SOURCE_DIR}/threadx_thread_sleep_terminate_test.c ${SOURCE_DIR}/threadx_thread_stack_checking_test.c ${SOURCE_DIR}/threadx_thread_terminate_delete_test.c + ${SOURCE_DIR}/threadx_thread_exit_callback_transition_test.c ${SOURCE_DIR}/threadx_thread_time_slice_change_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_and_isr_test.c ${SOURCE_DIR}/threadx_thread_wait_abort_test.c diff --git a/test/tx/cmake/thread_transition/CMakeLists.txt b/test/tx/cmake/thread_transition/CMakeLists.txt new file mode 100644 index 000000000..f414ed675 --- /dev/null +++ b/test/tx/cmake/thread_transition/CMakeLists.txt @@ -0,0 +1,115 @@ +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) +cmake_policy(SET CMP0057 NEW) + +project(thread_transition_test LANGUAGES C) + +set(REPO_ROOT ${CMAKE_CURRENT_LIST_DIR}/../../../..) +set(SOURCE_DIR ${REPO_ROOT}/test/tx/thread_transition) + +# TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five build +# configurations this tree compiles, and the tree builds the whole ThreadX library +# once per configuration, so neither can be reached from inside the regression +# suite. Both matter to the completion and termination transitions: +# +# - TX_NOT_INTERRUPTABLE selects a different transition, in which the exit +# notification callback runs with interrupts disabled and the interruptable +# suspension path -- which is what sets and clears the transition marker in the +# configurations the tree does build -- is not used at all. The marker is set +# and cleared there by stores that exist only in that configuration, and a +# marker left set would make a normally completed thread permanently +# undeletable. +# +# - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public names bind +# straight to the core services. The refusal lives in the core services for that +# reason, and the way to show it is to call the public names in a build with no +# wrappers. +# +# So the four sources that change are compiled directly into a test executable, once +# per combination, with recorders standing behind the scheduler services they call. +# That is the same technique the module manager tests in this tree use, for the same +# reason: the code under test cannot be reached through the library the suite links. +# +# These executables are deliberately left out of the coverage instrumentation. The +# same source compiled under different feature macros has a different line set, and +# merging those into the union the coverage report takes would confuse the figure +# rather than add to it. The coverage figure belongs to the five configurations the +# tree builds, and every line this change adds outside a TX_NOT_INTERRUPTABLE guard +# is in them. + +set(transition_kernel_sources + ${REPO_ROOT}/common/src/tx_thread_shell_entry.c + ${REPO_ROOT}/common/src/tx_thread_terminate.c + ${REPO_ROOT}/common/src/tx_thread_delete.c + ${REPO_ROOT}/common/src/tx_thread_reset.c + ${REPO_ROOT}/common/src/txe_thread_delete.c + ${REPO_ROOT}/common/src/txe_thread_reset.c + ${REPO_ROOT}/common/src/txe_thread_terminate.c + ${REPO_ROOT}/common/src/tx_thread_initialize.c) + +set(transition_sources + ${SOURCE_DIR}/threadx_thread_transition_configuration_test.c + ${transition_kernel_sources}) + +# The shim goes on the kernel sources only, not on the test. It defines +# TX_SOURCE_CODE, which is what tells tx_api.h to leave the public service names +# unmapped so that internal sources can use the core names; the test needs the +# opposite, because calling the public names and letting tx_api.h decide what they +# bind to is exactly what the TX_DISABLE_ERROR_CHECKING configuration is here to +# demonstrate. +set_source_files_properties( + ${transition_kernel_sources} + DIRECTORY ${CMAKE_CURRENT_LIST_DIR} + PROPERTIES COMPILE_OPTIONS + "-include;${SOURCE_DIR}/threadx_thread_transition_host_test_port.h") + +# Each entry is a test-name suffix, a colon, and the feature macros that define the +# configuration, separated by "|". A semicolon cannot be used as that separator: it +# is CMake's own list separator, so the foreach below would iterate the macros +# instead of the configurations. +set(transition_configurations + "ni:TX_NOT_INTERRUPTABLE" + "ni_no_error_checking:TX_NOT_INTERRUPTABLE|TX_DISABLE_ERROR_CHECKING" + "ni_no_notify:TX_NOT_INTERRUPTABLE|TX_DISABLE_NOTIFY_CALLBACKS" + "no_error_checking:TX_DISABLE_ERROR_CHECKING") + +foreach(configuration ${transition_configurations}) + + string(REPLACE ":" ";" configuration_parts ${configuration}) + list(GET configuration_parts 0 configuration_name) + list(GET configuration_parts 1 configuration_macro_text) + string(REPLACE "|" ";" configuration_macros ${configuration_macro_text}) + + set(test_name threadx_thread_transition_${configuration_name}_test) + + add_executable(${test_name} ${transition_sources}) + + target_include_directories( + ${test_name} + PRIVATE ${SOURCE_DIR} + ${REPO_ROOT}/common/inc + ${REPO_ROOT}/ports/${THREADX_ARCH}/${THREADX_TOOLCHAIN}/inc) + + target_compile_definitions(${test_name} PRIVATE ${configuration_macros}) + + # This directory is configured once per build configuration of the tree, so + # these executables inherit whichever feature macros that configuration sets -- + # which is useful, since it means the two configurations above are also seen in + # combination with stack checking and with the other four. The one that cannot + # be inherited is event tracing: the trace macros in these sources reference the + # trace component's buffer and registry, and this harness links four kernel + # sources rather than the library, so the references would not resolve. The + # trace insertions in these same sources are covered by the tree's own + # trace_build configuration, where the whole library is linked, so nothing is + # lost by turning tracing off here. Event logging is turned off for the same + # reason; no configuration of the tree enables it. + # + # The -U flags have to reach the test source as well as the kernel sources, + # because TX_ENABLE_EVENT_TRACE is visible to tx_api.h and the two must agree on + # what the headers declare. Target compile options land after the directory's + # -D flags on the command line, which is what makes the -U effective. + target_compile_options(${test_name} PRIVATE -UTX_ENABLE_EVENT_TRACE + -UTX_ENABLE_EVENT_LOG) + + add_test(${CMAKE_BUILD_TYPE}::${test_name} ${test_name}) + +endforeach() diff --git a/test/tx/regression/testcontrol.c b/test/tx/regression/testcontrol.c index e2b3f7253..00e31d9b0 100644 --- a/test/tx/regression/testcontrol.c +++ b/test/tx/regression/testcontrol.c @@ -218,6 +218,7 @@ void threadx_thread_simple_sleep_non_clear_application_define(void *); void threadx_thread_sleep_for_100ticks_application_define(void *); void threadx_thread_multiple_sleep_application_define(void *); void threadx_thread_terminate_delete_application_define(void *); +void threadx_thread_exit_callback_transition_application_define(void *); void threadx_thread_preemption_change_application_define(void *); void threadx_thread_priority_change_application_define(void *); void threadx_thread_time_slice_change_application_define(void *); @@ -335,6 +336,7 @@ TEST_ENTRY test_control_tests[] = threadx_thread_sleep_for_100ticks_application_define, threadx_thread_multiple_sleep_application_define, threadx_thread_terminate_delete_application_define, + threadx_thread_exit_callback_transition_application_define, threadx_thread_priority_change_application_define, diff --git a/test/tx/regression/threadx_thread_exit_callback_transition_test.c b/test/tx/regression/threadx_thread_exit_callback_transition_test.c new file mode 100644 index 000000000..c931085c5 --- /dev/null +++ b/test/tx/regression/threadx_thread_exit_callback_transition_test.c @@ -0,0 +1,1375 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + +/* This test covers the completion and termination transitions that a thread's + TX_THREAD_EXIT notification callback runs inside. + + _tx_thread_shell_entry and _tx_thread_terminate both publish the terminal + state -- TX_COMPLETED or TX_TERMINATED -- and then call the thread's exit + notification callback, before the thread has been detached from the ready + list and before either service has finished with the pointer it holds to + the control block. That terminal state is the same state _tx_thread_delete + and _tx_thread_reset accept as authorization to invalidate or rebuild the + control block, so a callback used to be able to delete the object -- and + then lawfully recreate it over the same memory -- while the scheduler was + still linked to the old incarnation, or to reset a terminated thread and + resume it back onto the ready list after termination had reported success. + + The tx_thread_suspending flag marks the transition for exactly its + duration, and delete and reset now refuse a target whose flag is still set. + The refusal is transient: TX_DELETE_ERROR and TX_NOT_DONE are errors those + two services already document, and the same call succeeds once the + transition has finished. + + What the test measures, in every scenario: + + 1. what the callback observes -- the published state, the transition + flag, and the status of each lifecycle service it can reach, + 2. that the scheduler's ready lists, priority map and execute pointer + stay consistent with the control blocks they point at, taken as one + snapshot under interrupt lockout so that the reading is not itself a + race, + 3. that the documented behaviour still works afterwards: the same delete + and reset succeed once the transition has completed, so nothing has + become permanently undeletable. + + The consequences are demonstrated rather than described, and deliberately + from the terminator's context rather than the completing thread's: + + - scenario A has the callback recreate the control block if its delete + unexpectedly succeeded, at a lower priority, so that the outcome is an + assertable disagreement -- the old priority list still heading at a + control block whose own priority field names a different list -- rather + than a fault; + - scenario B has the callback reset and then resume the target, so that a + kernel which allows the reset is caught reporting a successful + termination for a thread that is runnable again. + + Both run while the victim is a ready thread that is not the current thread. + That ordering is not cosmetic. On the Linux, Win32 and Win64 host + simulation ports TX_THREAD_DELETE_PORT_COMPLETION cancels and joins the + host thread backing the deleted thread, so a callback-side delete of the + *completing* thread destroys the host thread the callback is running on and + wedges the simulator instead of reaching an assertion. The completion + scenarios therefore come last, and the scenario gate below stops the run + before them if anything earlier has already failed. + + Every list walk is bounded, so a corrupted ring costs an assertion and not + a hang. Expectations are counted rather than short-circuited within a + scenario, so a failing kernel reports how much it failed by; progression + between scenarios is gated on there being no failures yet, so a kernel that + has already corrupted the scheduler is not driven further into it. */ + +#include +#include "tx_api.h" +#include "tx_thread.h" + + +/* Priorities. The controller sits above every thread it drives. The victim + shares its priority with two peers, so that both the multi-member and the + single-member ready-ring unlink paths are reached. RECREATE_PRIORITY is + below VICTIM_PRIORITY on purpose: see the note in the callback. */ + +#define CONTROLLER_PRIORITY 10 +#define VICTIM_PRIORITY 20 +#define RECREATE_PRIORITY 21 + +/* Upper bound on a ready-ring walk. The suite never has this many threads at + one priority, so exceeding it means the ring is broken rather than long. */ + +#define READY_RING_LIMIT 32 + +/* Sentinel for a probe slot the callback did not reach. */ + +#define PROBE_NOT_ATTEMPTED 0xFFFFFFFFUL + + +static unsigned long controller_counter = 0; +static unsigned long victim_counter = 0; +static unsigned long peer_a_counter = 0; +static unsigned long peer_b_counter = 0; +static unsigned long replacement_counter = 0; +static unsigned long callback_entry_counter = 0; +static unsigned long callback_exit_counter = 0; + +static TX_THREAD controller; +static TX_THREAD victim; +static TX_THREAD peer_a; +static TX_THREAD peer_b; +static TX_SEMAPHORE victim_semaphore; +static TX_MUTEX victim_mutex; + +static CHAR *victim_stack; +static CHAR *replacement_stack; + +/* What the victim's entry function should do before it returns. */ + +static volatile UINT victim_hold = TX_FALSE; +static volatile UINT victim_self_terminate = TX_FALSE; +static volatile UINT victim_take_mutex = TX_FALSE; +static volatile UINT victim_wait_semaphore = TX_FALSE; +static volatile UINT peers_run = TX_FALSE; + +/* What the exit callback should attempt the next time it runs. Independent + flags rather than one mode, because a delete that unexpectedly succeeds + destroys the object and would mask the reset measurement behind it. */ + +static volatile UINT probe_delete_enabled = TX_FALSE; +static volatile UINT probe_reset_enabled = TX_FALSE; +static volatile UINT probe_misc_enabled = TX_FALSE; +static volatile UINT probe_reuse_enabled = TX_FALSE; + +/* What the callback saw the last time it probed. */ + +static UINT probe_state; +static UINT probe_suspending; +static UINT probe_delete_status; +static UINT probe_core_delete_status; +static UINT probe_reset_status; +static UINT probe_core_reset_status; +static UINT probe_resume_status; +static UINT probe_suspend_status; +static UINT probe_terminate_status; +static UINT probe_create_status; +static UINT probe_consistent; +static UINT probe_ran; +static UINT delete_succeeded; + +/* Ordering witness for the notification pair. Entry and exit counts cannot + simply be compared: a victim created with TX_DONT_START never reports an + entry, and an incarnation that is reset and terminated again reports a + second exit with no second entry. What must hold is that an exit for a + thread that ran is preceded by that thread's entry. */ + +static UINT last_notify_type = PROBE_NOT_ATTEMPTED; +static UINT exit_saw_entry_first = TX_FALSE; + +/* Expectation accounting. */ + +#define FAILED_TAG_LIMIT 32 + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; +static UINT first_failure = 0; +static UINT failed_tags[FAILED_TAG_LIMIT]; +static UINT failed_tag_count = 0; + + +/* Define task prototypes. */ + +static void controller_entry(ULONG thread_input); +static void victim_entry(ULONG thread_input); +static void peer_entry(ULONG thread_input); +static void replacement_entry(ULONG thread_input); + + +/* Prototype for test control return. */ + +void test_control_return(UINT status); + + +/* Record one expectation. tag identifies it in the failure report; the first + failing tag is the number the suite prints, so tags are allocated in the + order the expectations are written. */ + +static void expect(UINT condition, UINT tag) +{ + + expectations_checked++; + + if (condition != TX_TRUE) + { + + expectations_failed++; + + if (first_failure == 0) + { + first_failure = tag; + } + + /* Keep the tags, so that a failing kernel reports which expectations + it failed rather than only the first and a total. */ + if (failed_tag_count < FAILED_TAG_LIMIT) + { + failed_tags[failed_tag_count] = tag; + failed_tag_count++; + } + } +} + + +static void expect_status(UINT actual, UINT wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_pointer(void *actual, void *wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +static void expect_count(unsigned long actual, unsigned long wanted, UINT tag) +{ + + if (actual == wanted) + { + expect(TX_TRUE, tag); + } + else + { + expect(TX_FALSE, tag); + } +} + + +/* Walk one ready ring and report whether thread_ptr is a member of it. The + walk is bounded, so a ring that does not close is reported as a + non-membership rather than spun on for ever. */ + +static UINT ready_ring_contains(UINT priority, TX_THREAD *thread_ptr) +{ + +TX_THREAD *walk_ptr; +TX_THREAD *head_ptr; +UINT steps; +UINT found; + + + found = TX_FALSE; + head_ptr = _tx_thread_priority_list[priority]; + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT) && (found == TX_FALSE)) + { + + if (walk_ptr == thread_ptr) + { + found = TX_TRUE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + /* The ring has closed. */ + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + return(found); +} + + +/* Take one snapshot of the scheduler's ready structures and check that they + agree with the control blocks they point at. + + This is the assertion the pre-fix behaviour fails. A callback that deletes + the victim and recreates it at another priority leaves the old priority list + heading at that control block while the block's own priority field names the + new list, and leaves the old priority's map bit set with nothing behind it. + Neither disagreement needs a fault to be visible. + + The whole snapshot is taken under interrupt lockout -- which is the global + SMP protection on an SMP port -- so that reading the scheduler's state is + not itself a race against another core. + + transitioning names the one thread that is allowed to be on a ready list + without being ready, which is precisely the thread whose completion or + termination transition is in progress: the terminal state is published + before the detachment, so inside the callback the victim is legitimately a + TX_COMPLETED or TX_TERMINATED member of its ready ring. That single + exemption is from the state clause only. Every other clause, and in + particular the clause that requires a ready-list member to agree that this + is its priority -- the one a callback-side delete and recreate breaks -- + still applies to it. */ + +static UINT scheduler_consistent_except(TX_THREAD *transitioning) +{ + +TX_INTERRUPT_SAVE_AREA + +TX_THREAD *head_ptr; +TX_THREAD *walk_ptr; +TX_THREAD *execute_ptr; +UINT priority; +UINT steps; +UINT consistent; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + consistent = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + + head_ptr = _tx_thread_priority_list[priority]; + +#if TX_MAX_PRIORITIES <= 32 + + /* A priority with a ready list must have its map bit set, and a + priority with no ready list must not. */ + if (head_ptr == TX_NULL) + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) != ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } + else + { + if ((_tx_thread_priority_maps[0] & (((ULONG) 1) << priority)) == ((ULONG) 0)) + { + consistent = TX_FALSE; + } + } +#endif + + walk_ptr = head_ptr; + steps = 0; + + while ((walk_ptr != TX_NULL) && (steps < READY_RING_LIMIT)) + { + + /* Every thread on a ready list must be a live thread, must be + ready, and must agree that this is its priority. */ + if (walk_ptr -> tx_thread_id != TX_THREAD_ID) + { + consistent = TX_FALSE; + } + + if ((walk_ptr -> tx_thread_state != TX_READY) && (walk_ptr != transitioning)) + { + consistent = TX_FALSE; + } + + if (walk_ptr -> tx_thread_priority != priority) + { + consistent = TX_FALSE; + } + + /* The ring must be doubly linked. */ + if (walk_ptr -> tx_thread_ready_next -> tx_thread_ready_previous != walk_ptr) + { + consistent = TX_FALSE; + } + + walk_ptr = walk_ptr -> tx_thread_ready_next; + steps++; + + if (walk_ptr == head_ptr) + { + walk_ptr = TX_NULL; + } + } + + /* A ring that did not close inside the bound is broken. */ + if (walk_ptr != TX_NULL) + { + consistent = TX_FALSE; + } + } + + /* Whatever the scheduler has selected to execute must be a member of the + ready list that its own priority field names. */ +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + + execute_ptr = _tx_thread_execute_ptr[core]; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } + } +#else + + execute_ptr = _tx_thread_execute_ptr; + + if (execute_ptr != TX_NULL) + { + if (ready_ring_contains(execute_ptr -> tx_thread_priority, execute_ptr) == TX_FALSE) + { + consistent = TX_FALSE; + } + } +#endif + + TX_RESTORE + + return(consistent); +} + + +static UINT scheduler_consistent(void) +{ + return(scheduler_consistent_except(TX_NULL)); +} + + +/* Report whether thread_ptr is absent from every ready list and from every + execute slot, which is what a completed or terminated thread must be once + its transition has finished. */ + +static UINT fully_detached(TX_THREAD *thread_ptr) +{ + +TX_INTERRUPT_SAVE_AREA + +UINT priority; +UINT detached; +#ifdef TX_THREAD_SMP_MAX_CORES +UINT core; +#endif + + + detached = TX_TRUE; + + TX_DISABLE + + for (priority = 0; priority < ((UINT) TX_MAX_PRIORITIES); priority++) + { + if (ready_ring_contains(priority, thread_ptr) == TX_TRUE) + { + detached = TX_FALSE; + } + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + for (core = 0; core < ((UINT) TX_THREAD_SMP_MAX_CORES); core++) + { + if (_tx_thread_execute_ptr[core] == thread_ptr) + { + detached = TX_FALSE; + } + } +#else + + if (_tx_thread_execute_ptr == thread_ptr) + { + detached = TX_FALSE; + } +#endif + + TX_RESTORE + + return(detached); +} + + +static void arm_probe(UINT probe_delete, UINT probe_reset, UINT probe_misc, UINT probe_reuse) +{ + + probe_state = PROBE_NOT_ATTEMPTED; + probe_suspending = PROBE_NOT_ATTEMPTED; + probe_delete_status = PROBE_NOT_ATTEMPTED; + probe_core_delete_status = PROBE_NOT_ATTEMPTED; + probe_reset_status = PROBE_NOT_ATTEMPTED; + probe_core_reset_status = PROBE_NOT_ATTEMPTED; + probe_resume_status = PROBE_NOT_ATTEMPTED; + probe_suspend_status = PROBE_NOT_ATTEMPTED; + probe_terminate_status = PROBE_NOT_ATTEMPTED; + probe_create_status = PROBE_NOT_ATTEMPTED; + probe_consistent = PROBE_NOT_ATTEMPTED; + probe_ran = TX_FALSE; + delete_succeeded = TX_FALSE; + + probe_delete_enabled = probe_delete; + probe_reset_enabled = probe_reset; + probe_misc_enabled = probe_misc; + probe_reuse_enabled = probe_reuse; +} + + +/* The exit notification callback. Everything it learns is recorded for the + controller to check; it prints nothing and it never suspends, so it obeys + the one restriction the manual places on a notification callback. */ + +static void entry_exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + /* Only the victim's notifications are of interest. Its control block is + reused across scenarios, so the pointer is the identity. */ + if (thread_ptr != &victim) + { + return; + } + + if (type == TX_THREAD_ENTRY) + { + callback_entry_counter++; + last_notify_type = TX_THREAD_ENTRY; + return; + } + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_exit_counter++; + + if (last_notify_type == TX_THREAD_ENTRY) + { + exit_saw_entry_first = TX_TRUE; + } + + last_notify_type = TX_THREAD_EXIT; + + if ((probe_delete_enabled == TX_FALSE) && (probe_reset_enabled == TX_FALSE) && + (probe_misc_enabled == TX_FALSE)) + { + return; + } + + probe_ran = TX_TRUE; + + /* What the kernel has published about the thread at this point. */ + probe_state = thread_ptr -> tx_thread_state; + probe_suspending = thread_ptr -> tx_thread_suspending; + + /* None of the services below has a suspension option, which is the only + thing the manual forbids a callback from using, so every one of them is + reachable from here. + + Where a service is expected to refuse, the core service is called after + the _txe_ wrapper as well. The wrapper is what an ordinary build + reaches; the core service is what a TX_DISABLE_ERROR_CHECKING build + reaches directly, and the refusal has to live there so that disabling + error checking cannot disable it. The core call is made only when the + wrapper already refused, so that a kernel which allows the operation is + not asked to perform it twice. */ + + if (probe_delete_enabled == TX_TRUE) + { + + probe_delete_status = tx_thread_delete(thread_ptr); + + if (probe_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + else + { + + probe_core_delete_status = _tx_thread_delete(thread_ptr); + + if (probe_core_delete_status == TX_SUCCESS) + { + delete_succeeded = TX_TRUE; + } + } + + /* Recreate the control block only if the delete unexpectedly + succeeded. On a fixed kernel this never runs. On an unfixed one it + is the lawful next step after a successful delete, and it is what + turns the window into an observable inconsistency: tx_thread_create + zeroes the whole control block, and the auto-start then links it + onto RECREATE_PRIORITY's ready list while the old incarnation is + still the head of VICTIM_PRIORITY's. + + RECREATE_PRIORITY is the lower of the two so that the stale head is + the one the scheduler selects from, and so that the ring at the new + priority has a single member. Recreating at the same priority would + instead have the auto-start write through the control block's + freshly zeroed ready_previous pointer, which is a fault rather than + an assertion. */ + if ((delete_succeeded == TX_TRUE) && (probe_reuse_enabled == TX_TRUE)) + { + probe_create_status = tx_thread_create(thread_ptr, "replacement", + replacement_entry, 0, + replacement_stack, + TEST_STACK_SIZE_PRINTF, + RECREATE_PRIORITY, RECREATE_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + } + } + + if ((probe_reset_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + + probe_reset_status = tx_thread_reset(thread_ptr); + + if (probe_reset_status != TX_SUCCESS) + { + probe_core_reset_status = _tx_thread_reset(thread_ptr); + } + + /* Follow the reset with the resume that the reset would have unlocked. + On a fixed kernel the state is still terminal and the resume is + refused. On an unfixed one the reset has moved the state to + TX_SUSPENDED, the resume finds a suspending thread whose state is no + longer terminal, clears the flag and puts the thread back on the + ready list -- and the outer service then skips the removal and + reports success for a termination that did not happen. */ + probe_resume_status = tx_thread_resume(thread_ptr); + } + + if ((probe_misc_enabled == TX_TRUE) && (delete_succeeded == TX_FALSE)) + { + probe_suspend_status = tx_thread_suspend(thread_ptr); + probe_terminate_status = tx_thread_terminate(thread_ptr); + } + + probe_consistent = scheduler_consistent_except(thread_ptr); +} + + +/* Define what the initial system looks like. */ + +#ifdef CTEST +void test_application_define(void *first_unused_memory) +#else +void threadx_thread_exit_callback_transition_application_define(void *first_unused_memory) +#endif +{ + +UINT status; +CHAR *pointer; + + + /* Put first available memory address into a character pointer. */ + pointer = (CHAR *) first_unused_memory; + + status = tx_thread_create(&controller, "controller", controller_entry, 0, + pointer, TEST_STACK_SIZE_PRINTF, + CONTROLLER_PRIORITY, CONTROLLER_PRIORITY, + TX_NO_TIME_SLICE, TX_AUTO_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_a, "peer a", peer_entry, 1, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_thread_create(&peer_b, "peer b", peer_entry, 2, + pointer, TEST_STACK_SIZE_PRINTF, + VICTIM_PRIORITY, VICTIM_PRIORITY, + TX_NO_TIME_SLICE, TX_DONT_START); + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + /* The victim's control block is created and destroyed repeatedly, so its + two stacks are set aside here rather than per scenario. */ + victim_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + replacement_stack = pointer; + pointer = pointer + TEST_STACK_SIZE_PRINTF; + + status += tx_semaphore_create(&victim_semaphore, "victim semaphore", 0); + status += tx_mutex_create(&victim_mutex, "victim mutex", TX_INHERIT); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #1\n"); + test_control_return(1); + } + +#ifdef TX_THREAD_SMP_MAX_CORES + + /* Confine every thread this test drives to core 0. The measurements below + are of global scheduler structures at chosen moments, and letting the + peers and the victim run on other cores would make them a race against + the test rather than a property of it. A victim executing on a + different core when termination begins is deliberately covered by a + separate SMP test, so that the deterministic cases here cannot be made + flaky by it. */ + status = tx_thread_smp_core_exclude(&controller, 0xE); + status += tx_thread_smp_core_exclude(&peer_a, 0xE); + status += tx_thread_smp_core_exclude(&peer_b, 0xE); + + if (status != TX_SUCCESS) + { + + printf("Running Thread Exit Callback Transition Test............... ERROR #2\n"); + test_control_return(1); + } +#endif +} + + +/* Create the victim over its own control block, with the exit callback + registered. The notify status is folded into the result according to what + the configuration supports. */ + +static UINT create_victim(UINT priority, UINT auto_start) +{ + +UINT status; +UINT notify_status; + + + status = tx_thread_create(&victim, "victim", victim_entry, 0, + victim_stack, TEST_STACK_SIZE_PRINTF, + priority, priority, TX_NO_TIME_SLICE, auto_start); + + notify_status = tx_thread_entry_exit_notify(&victim, entry_exit_notify); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + if (notify_status != TX_SUCCESS) + { + status = notify_status; + } +#else + + /* Notification is compiled out, so the registration is expected to be + refused and the callback never runs. */ + if (notify_status != TX_FEATURE_NOT_ENABLED) + { + status = notify_status; + } +#endif + +#ifdef TX_THREAD_SMP_MAX_CORES + + if (status == TX_SUCCESS) + { + status = tx_thread_smp_core_exclude(&victim, 0xE); + } +#endif + + return(status); +} + + +/* The gate between scenarios. A kernel that has already failed an expectation + has, in these scenarios, already left the scheduler inconsistent, and + driving it further would report nothing more while risking a wedge instead + of a verdict. */ + +static UINT ok_so_far(void) +{ + +UINT result; + + + result = TX_FALSE; + + if (expectations_failed == 0) + { + result = TX_TRUE; + } + + return(result); +} + + +static void controller_entry(ULONG thread_input) +{ + +ULONG expected_exits; +ULONG expected_entries; +UINT index; + + + (void) thread_input; + + controller_counter++; + expected_exits = 0; + expected_entries = 0; + + /* Inform user. */ + printf("Running Thread Exit Callback Transition Test............... "); + + /* The scheduler must be self-consistent before anything is measured, so + that a later failure is attributable to this test. */ + expect(scheduler_consistent(), 10); + + /* ------------------------------------------------------------------- */ + /* Scenario A: terminate a ready target that is not the current thread, */ + /* with two peers ready at the same priority. The callback attempts */ + /* the delete, and recreates the control block if it succeeded. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_TRUE; + victim_hold = TX_TRUE; + + expect_status(tx_thread_resume(&peer_a), TX_SUCCESS, 11); + expect_status(tx_thread_resume(&peer_b), TX_SUCCESS, 12); + + arm_probe(TX_TRUE, TX_FALSE, TX_FALSE, TX_TRUE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 13); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim and both peers become ready and start spinning, so + that the victim's unlink is from a multi-member ring. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 14); + expect(ready_ring_contains(VICTIM_PRIORITY, &victim), 15); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 16); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 17); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 18); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 19); + + /* The callback runs after the terminal state has been published, and + inside the transition, which is what the flag records. */ + expect_status(probe_state, TX_TERMINATED, 20); + expect_status(probe_suspending, TX_TRUE, 21); + + /* Both the wrapper and the core service must refuse. */ + expect_status(probe_delete_status, TX_DELETE_ERROR, 22); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 23); + + /* So the recreate never happened and nothing is inconsistent. */ + expect_status(delete_succeeded, TX_FALSE, 24); + expect_status(probe_create_status, PROBE_NOT_ATTEMPTED, 25); + expect_status(probe_consistent, TX_TRUE, 26); + expect_count(replacement_counter, 0, 27); +#endif + + /* After the service returns the termination is complete: the state is + terminal, the marker is clear, the target is on no ready list and in + no execute slot, and the peers are undisturbed. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 28); + expect_status(victim.tx_thread_suspending, TX_FALSE, 29); + expect(fully_detached(&victim), 30); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_a), 31); + expect(ready_ring_contains(VICTIM_PRIORITY, &peer_b), 32); + expect(scheduler_consistent(), 33); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* The documented behaviour is available again. */ + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 34); + } + + /* ------------------------------------------------------------------- */ + /* Scenario B: the same termination, but the callback attempts a reset */ + /* and then the resume that a successful reset would have unlocked. */ + /* */ + /* This is the leg where the transition check is the only thing */ + /* standing between the callback and a reset of a thread that is still */ + /* on the ready list. Without it the reset succeeds, the resume */ + /* clears the suspending flag and restores TX_READY, the outer service */ + /* then finds the flag clear and skips the removal, and */ + /* tx_thread_terminate returns TX_SUCCESS for a runnable thread. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 40); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_READY, 41); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 42); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 43); + expect_status(probe_state, TX_TERMINATED, 44); + expect_status(probe_suspending, TX_TRUE, 45); + + /* The discriminating leg. The target is not the current thread and + its published state is terminal, so both of the conditions the + service tested before this fix are satisfied. */ + expect_status(probe_reset_status, TX_NOT_DONE, 46); + expect_status(probe_core_reset_status, TX_NOT_DONE, 47); + + /* With the reset refused, the state is still terminal, so the resume + cannot reach the branch that would void the termination. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 48); + expect_status(probe_consistent, TX_TRUE, 49); +#endif + + /* And the termination stands. A kernel that let the callback reset + and resume the target arrives here with the victim ready to run. */ + expect_status(victim.tx_thread_state, TX_TERMINATED, 50); + expect_status(victim.tx_thread_suspending, TX_FALSE, 51); + expect(fully_detached(&victim), 52); + expect(scheduler_consistent(), 53); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 54); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 55); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 56); + } + + /* Retire the peers, so that the ring drops to a single member and then to + none, and check consistency across both unlinks. */ + + if (ok_so_far() == TX_TRUE) + { + + peers_run = TX_FALSE; + tx_thread_sleep(3); + + expect_status(peer_a.tx_thread_state, TX_COMPLETED, 57); + expect_status(peer_b.tx_thread_state, TX_COMPLETED, 58); + expect(scheduler_consistent(), 59); + expect_status(tx_thread_delete(&peer_a), TX_SUCCESS, 60); + expect_status(tx_thread_delete(&peer_b), TX_SUCCESS, 61); + } + + /* ------------------------------------------------------------------- */ + /* Scenario C: terminate a target suspended on an object, so that the */ + /* non-ready branch of _tx_thread_terminate runs -- suspension cleanup, */ + /* the terminated extension, the callback, and mutex release, in that */ + /* order. That branch used to clear the marker before the callback */ + /* rather than after the last dereference of the target. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + victim_hold = TX_FALSE; + victim_take_mutex = TX_TRUE; + victim_wait_semaphore = TX_TRUE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 70); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim take the mutex and suspend on the semaphore. */ + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_SEMAPHORE_SUSP, 71); + expect_pointer(victim_mutex.tx_mutex_owner, &victim, 72); + } + + if (ok_so_far() == TX_TRUE) + { + + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 73); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 74); + expect_status(probe_state, TX_TERMINATED, 75); + + /* The discriminating leg for this branch. The marker used to be + cleared before this point, which left the callback free to delete or + reset a control block that the service still had mutex-release + processing to do against. */ + expect_status(probe_suspending, TX_TRUE, 76); + expect_status(probe_delete_status, TX_DELETE_ERROR, 77); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 78); + expect_status(probe_reset_status, TX_NOT_DONE, 79); + expect_status(probe_core_reset_status, TX_NOT_DONE, 80); + expect_status(probe_resume_status, TX_RESUME_ERROR, 81); + expect_status(delete_succeeded, TX_FALSE, 82); + expect_status(probe_consistent, TX_TRUE, 83); +#endif + + /* The marker is cleared at the safe point, after the mutex release, + and the mutex the terminated thread owned has been given up. The + value of the flag during the release itself is not observable from + application code; what is observable is that there is exactly one + clear site and that it sits downstream of the release. */ + expect_status(victim.tx_thread_suspending, TX_FALSE, 84); + expect_pointer(victim_mutex.tx_mutex_owner, TX_NULL, 85); + expect_count((unsigned long) victim_semaphore.tx_semaphore_suspended_count, 0, 86); + expect(fully_detached(&victim), 87); + expect(scheduler_consistent(), 88); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 89); + } + + /* ------------------------------------------------------------------- */ + /* Scenario D: natural completion, victim alone at its priority. */ + /* */ + /* The callback here runs on the completing thread itself, so a */ + /* callback-side delete that succeeds destroys the host thread on the */ + /* simulation ports. This scenario is therefore placed after the ones */ + /* that reach an assertion, and the gate above stops the run before it */ + /* if any of them has already failed. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_TRUE, TX_TRUE, TX_TRUE, TX_FALSE); + victim_take_mutex = TX_FALSE; + victim_wait_semaphore = TX_FALSE; + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 100); + } + + if (ok_so_far() == TX_TRUE) + { + + /* Let the victim run to completion, so that its callback runs inside + the completion transition. */ + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect(probe_ran, 101); + expect_status(probe_state, TX_COMPLETED, 102); + expect_status(probe_suspending, TX_TRUE, 103); + expect_status(probe_delete_status, TX_DELETE_ERROR, 104); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 105); + + /* Reset of the running thread is refused for two independent reasons + here -- it is the current thread, and its transition is in progress + -- so this leg is a guard rather than a discriminator. Scenario B + is where the transition check is the only thing refusing a reset. */ + expect_status(probe_reset_status, TX_NOT_DONE, 106); + expect_status(probe_core_reset_status, TX_NOT_DONE, 107); + + /* A completed thread is neither resumable nor suspendable, and + terminating one is a documented no-op. */ + expect_status(probe_resume_status, TX_RESUME_ERROR, 108); + expect_status(probe_suspend_status, TX_SUSPEND_ERROR, 109); + expect_status(probe_terminate_status, TX_SUCCESS, 110); + expect_status(delete_succeeded, TX_FALSE, 111); + expect_status(probe_consistent, TX_TRUE, 112); +#endif + + /* The transition has finished, so the calls the callback was refused + must succeed. The refusal is transient and nothing has become + permanently undeletable. */ + expect_status(victim.tx_thread_state, TX_COMPLETED, 113); + expect_status(victim.tx_thread_suspending, TX_FALSE, 114); + expect(fully_detached(&victim), 115); + expect(scheduler_consistent(), 116); + expect(((victim_counter > 0) ? TX_TRUE : TX_FALSE), 117); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + /* Reset, so that the documented reset of a completed thread is covered + on a target whose transition has finished, and so that the + reset/terminate/delete order is exercised on this branch too. */ + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 118); + expect_status(victim.tx_thread_state, TX_SUSPENDED, 119); + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 120); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 121); + expect_status(victim.tx_thread_suspending, TX_FALSE, 122); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 123); + } + + /* A safe post-completion recreate at a different priority, which is the + lifecycle the callback was refused, performed at the point it is + legitimate. It exercises the priority map and the list heads for a + recreate that moves priority as well as for one that does not. */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(RECREATE_PRIORITY, TX_AUTO_START), TX_SUCCESS, 124); + + tx_thread_sleep(3); + + expect_status(victim.tx_thread_state, TX_COMPLETED, 125); + expect(scheduler_consistent(), 126); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 127); + } + + /* ------------------------------------------------------------------- */ + /* Scenario E: self termination. The callback fires once, in the */ + /* victim's own context, the lifecycle mutation is refused there too, */ + /* and another thread can complete the lifecycle afterwards. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_TRUE; + expected_exits = callback_exit_counter; + + arm_probe(TX_TRUE, TX_TRUE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 130); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* Exactly one exit notification, even though the thread terminated + itself from inside its own entry function. */ + expect_count(callback_exit_counter, expected_exits + 1, 131); + expect_status(probe_state, TX_TERMINATED, 132); + expect_status(probe_suspending, TX_TRUE, 133); + expect_status(probe_delete_status, TX_DELETE_ERROR, 134); + expect_status(probe_core_delete_status, TX_DELETE_ERROR, 135); + expect_status(probe_reset_status, TX_NOT_DONE, 136); + expect_status(delete_succeeded, TX_FALSE, 137); + expect_status(probe_consistent, TX_TRUE, 138); +#else + + expect_count(callback_exit_counter, expected_exits, 139); +#endif + + expect_status(victim.tx_thread_state, TX_TERMINATED, 140); + expect_status(victim.tx_thread_suspending, TX_FALSE, 141); + expect(fully_detached(&victim), 142); + } + + if (ok_so_far() == TX_TRUE) + { + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 143); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 144); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 145); + expect(scheduler_consistent(), 146); + } + + /* ------------------------------------------------------------------- */ + /* Scenario F: a benign callback is unaffected. The notification */ + /* ordering and counts are what they were. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + victim_self_terminate = TX_FALSE; + expected_exits = callback_exit_counter; + expected_entries = callback_entry_counter; + exit_saw_entry_first = TX_FALSE; + last_notify_type = PROBE_NOT_ATTEMPTED; + + arm_probe(TX_FALSE, TX_FALSE, TX_FALSE, TX_FALSE); + + expect_status(create_victim(VICTIM_PRIORITY, TX_AUTO_START), TX_SUCCESS, 150); + } + + if (ok_so_far() == TX_TRUE) + { + + tx_thread_sleep(3); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* This incarnation ran and completed, so it reported exactly one entry + and one exit, and the entry came first. The transition marker has + changed neither how often notifications are delivered nor in what + order. */ + expect_count(callback_exit_counter, expected_exits + 1, 151); + expect_count(callback_entry_counter, expected_entries + 1, 152); + expect_status(exit_saw_entry_first, TX_TRUE, 153); +#else + + expect_count(callback_exit_counter, expected_exits, 154); + expect_count(callback_entry_counter, expected_entries, 155); +#endif + + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 156); + expect(scheduler_consistent(), 157); + } + + /* ------------------------------------------------------------------- */ + /* Scenario G: state and boundary cases. A thread that never entered a */ + /* terminal state, and one that is already fully detached, behave as */ + /* documented -- the new refusal applies only while the marker is set. */ + /* ------------------------------------------------------------------- */ + + if (ok_so_far() == TX_TRUE) + { + + expect_status(create_victim(VICTIM_PRIORITY, TX_DONT_START), TX_SUCCESS, 170); + + /* Suspended, never run: neither state is terminal, so both services + are refused for the reason they always were, and the marker is not + set on a thread that is not transitioning. */ + expect_status(tx_thread_delete(&victim), TX_DELETE_ERROR, 171); + expect_status(tx_thread_reset(&victim), TX_NOT_DONE, 172); + expect_status(victim.tx_thread_suspending, TX_FALSE, 173); + + /* Terminated from a suspended state: deletable immediately, and + repeatedly resettable and re-terminable in between. */ + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 174); + expect_status(victim.tx_thread_suspending, TX_FALSE, 175); + expect_status(tx_thread_reset(&victim), TX_SUCCESS, 176); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 177); + expect_status(tx_thread_terminate(&victim), TX_SUCCESS, 178); + expect_status(tx_thread_delete(&victim), TX_SUCCESS, 179); + expect(scheduler_consistent(), 180); + } + + if (expectations_failed != 0) + { + + printf("ERROR #%u (%lu of %lu expectations failed:", + first_failure, expectations_failed, expectations_checked); + + for (index = 0; index < failed_tag_count; index++) + { + printf(" %u", failed_tags[index]); + } + + if (expectations_failed > ((unsigned long) failed_tag_count)) + { + printf(" ..."); + } + + printf(")\n"); + fflush(stdout); + test_control_return(1); + } + else + { + printf("SUCCESS!\n"); + test_control_return(0); + } +} + + +static void victim_entry(ULONG thread_input) +{ + + (void) thread_input; + + victim_counter++; + + if (victim_take_mutex == TX_TRUE) + { + tx_mutex_get(&victim_mutex, TX_NO_WAIT); + } + + if (victim_wait_semaphore == TX_TRUE) + { + + /* Suspend on an object, so that a termination takes the non-ready + branch and runs the suspension cleanup routine. */ + tx_semaphore_get(&victim_semaphore, TX_WAIT_FOREVER); + } + + if (victim_self_terminate == TX_TRUE) + { + + /* This does not return: the termination detaches this thread and the + preemption check at the end of the service switches away from it. */ + tx_thread_terminate(tx_thread_identify()); + } + + while (victim_hold == TX_TRUE) + { + + /* Stay ready, so that a terminate issued by another thread finds this + thread on the ready list. */ + tx_thread_relinquish(); + } + + /* Returning enters the completion transition. */ +} + + +static void peer_entry(ULONG thread_input) +{ + + if (thread_input == 1) + { + peer_a_counter++; + } + else + { + peer_b_counter++; + } + + while (peers_run == TX_TRUE) + { + + /* Stay ready at the victim's priority, so that the victim's ready ring + has more than one member. */ + tx_thread_relinquish(); + } +} + + +static void replacement_entry(ULONG thread_input) +{ + + (void) thread_input; + + replacement_counter++; + + /* Only reached on a kernel that let the callback recreate the control + block. Suspend rather than return, so that the run does not depend on a + second completion transition over a control block already known to be + inconsistent. */ + tx_thread_suspend(tx_thread_identify()); +} diff --git a/test/tx/thread_transition/threadx_thread_transition_configuration_test.c b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c new file mode 100644 index 000000000..fe6acb7e5 --- /dev/null +++ b/test/tx/thread_transition/threadx_thread_transition_configuration_test.c @@ -0,0 +1,607 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Test */ +/** */ +/** Thread lifecycle transition configuration coverage */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +/* The two configurations that matter to this change and that neither + regression tree builds are TX_NOT_INTERRUPTABLE and + TX_DISABLE_ERROR_CHECKING. This executable is built once for each + combination of them, against the four sources that change, and drives the + completion and termination transitions directly. + + Why each one needs its own build rather than a case in the in-tree test: + + - TX_NOT_INTERRUPTABLE selects a different transition. There the exit + notification callback runs with interrupts still disabled and the + interruptable suspension path -- which is what sets and clears the + transition marker in the configurations the trees do build -- is not + used at all, so the marker is set and cleared by explicit stores that + exist only in this configuration. A marker left set there would make a + normally completed thread permanently undeletable, which is the failure + mode worth more than a compile. + + - TX_DISABLE_ERROR_CHECKING removes the _txe_ wrappers, so the public + names bind straight to the core services. The refusal has to live in + the core services for exactly that reason, and the way to demonstrate it + is to call the public names in a build that has no wrappers. + + The scheduler is not linked. _tx_thread_system_ni_suspend and + _tx_thread_system_suspend are recorders, which is what makes the marker's + value at the moment of detachment directly observable -- and, in the + TX_NOT_INTERRUPTABLE completion path, the reason the marker has to be + cleared before the call rather than after it: on a real port that call + returns to the scheduler and never comes back. + + Everything here is straight-line: there are no threads, no timers and no + waits, so there is nothing that can hang. */ + +#include +#include + +/* tx_api.h is included without TX_SOURCE_CODE, so that the public service names + are mapped the way an application sees them: to the _txe_ wrappers with error + checking on, and straight to the core services with it off. Letting the header + make that choice is the point -- the refusal has to survive the choice. Only + then is TX_SOURCE_CODE defined, for the internal declarations the recorders + below implement. */ + +#include "tx_api.h" + +/* tx_thread_entry_exit_notify is both a service name and a TX_THREAD field name, + and it is the only name in this file that is both, so the mapping above would + rewrite the field assignment further down into a member that does not exist. + Dropping just that one mapping keeps the mapping that matters -- the one on the + three services this test calls -- while leaving the field reachable. The test + never calls the notify service; it plants the callback in the control block + directly, because the create path that would otherwise do it is not linked. */ + +#undef tx_thread_entry_exit_notify + +#define TX_SOURCE_CODE +#include "tx_thread.h" + + +/* Counters the force-included port shim's macros drive. */ + +unsigned int test_interrupt_disable_depth = 0; +unsigned int test_interrupt_disable_max_depth = 0; +unsigned int test_interrupt_restore_underflows = 0; +unsigned int test_delete_port_completion_count = 0; +unsigned int test_reset_port_completion_count = 0; + + +#define TEST_STACK_BYTES 512 + +static TX_THREAD victim; +static TX_THREAD other; +static UCHAR victim_stack[TEST_STACK_BYTES]; + +/* What the recorders saw. */ + +static unsigned int ni_suspend_calls = 0; +static UINT ni_suspend_suspending = 0xFFFFFFFFU; +static UINT ni_suspend_state = 0xFFFFFFFFU; +static unsigned int ni_suspend_depth = 0xFFFFFFFFU; + +static unsigned int suspend_calls = 0; +static UINT suspend_suspending = 0xFFFFFFFFU; + +static unsigned int stack_build_calls = 0; +static unsigned int preempt_check_calls = 0; +static unsigned int timer_deactivate_calls = 0; + +/* What the exit callback saw. */ + +static unsigned int callback_calls = 0; +static UINT callback_state = 0xFFFFFFFFU; +static UINT callback_suspending = 0xFFFFFFFFU; +static unsigned int callback_depth = 0xFFFFFFFFU; +static UINT callback_delete_status = 0xFFFFFFFFU; +static UINT callback_reset_status = 0xFFFFFFFFU; +static UINT callback_delete_completions = 0xFFFFFFFFU; +static UINT callback_reset_completions = 0xFFFFFFFFU; + +static unsigned int entry_calls = 0; + +static unsigned long expectations_checked = 0; +static unsigned long expectations_failed = 0; + + +static void expect(int condition, const char *what) +{ + + expectations_checked++; + + if (condition == 0) + { + expectations_failed++; + printf(" FAILED: %s\n", what); + } +} + + +static void expect_uint(UINT actual, UINT wanted, const char *what) +{ + + expectations_checked++; + + if (actual != wanted) + { + expectations_failed++; + printf(" FAILED: %s (got %lu, wanted %lu)\n", what, + (unsigned long) actual, (unsigned long) wanted); + } +} + + +/* ------------------------------------------------------------------------ */ +/* Recorders standing in for the scheduler. */ +/* ------------------------------------------------------------------------ */ + +VOID _tx_thread_system_ni_suspend(TX_THREAD *thread_ptr, ULONG wait_option) +{ + + (void) wait_option; + + ni_suspend_calls++; + ni_suspend_suspending = thread_ptr -> tx_thread_suspending; + ni_suspend_state = thread_ptr -> tx_thread_state; + ni_suspend_depth = test_interrupt_disable_depth; +} + + +VOID _tx_thread_system_suspend(TX_THREAD *thread_ptr) +{ + + suspend_calls++; + suspend_suspending = thread_ptr -> tx_thread_suspending; + + /* The interruptable suspension path is where the marker is cleared in the + configurations the regression trees build, so the recorder clears it as + the real one does, on the same condition. */ + if (thread_ptr -> tx_thread_suspending == TX_TRUE) + { + thread_ptr -> tx_thread_suspending = TX_FALSE; + } + + /* It also decrements the preemption disable count, which its callers have + raised on its behalf. The recorder has to do the same, or the balance + this test asserts across the whole transition would be off by one for a + reason that belongs to the recorder rather than to the code under + test. */ + _tx_thread_preempt_disable--; +} + + +VOID _tx_thread_stack_build(TX_THREAD *thread_ptr, VOID (*function_ptr)(VOID)) +{ + + (void) thread_ptr; + (void) function_ptr; + + stack_build_calls++; +} + + +VOID _tx_thread_system_preempt_check(VOID) +{ + preempt_check_calls++; +} + + +/* _tx_thread_terminate deactivates the target's timeout timer, and + _txe_thread_reset refuses a call made from the timer thread. Neither the + timer component nor the timer thread is linked, so both are supplied here: + the deactivation as a recorder, and the timer thread as a control block that + is never the current thread, so the wrapper's check is exercised and takes + the not-the-timer-thread branch. */ + +VOID _tx_timer_system_deactivate(TX_TIMER_INTERNAL *timer_ptr) +{ + + (void) timer_ptr; + + timer_deactivate_calls++; +} + + +TX_THREAD _tx_timer_thread; + + +/* ------------------------------------------------------------------------ */ +/* The thread under test. */ +/* ------------------------------------------------------------------------ */ + +static void reset_records(void) +{ + + ni_suspend_calls = 0; + ni_suspend_suspending = 0xFFFFFFFFU; + ni_suspend_state = 0xFFFFFFFFU; + ni_suspend_depth = 0xFFFFFFFFU; + suspend_calls = 0; + suspend_suspending = 0xFFFFFFFFU; + stack_build_calls = 0; + preempt_check_calls = 0; + timer_deactivate_calls = 0; + callback_calls = 0; + callback_state = 0xFFFFFFFFU; + callback_suspending = 0xFFFFFFFFU; + callback_depth = 0xFFFFFFFFU; + callback_delete_status = 0xFFFFFFFFU; + callback_reset_status = 0xFFFFFFFFU; + callback_delete_completions = 0xFFFFFFFFU; + callback_reset_completions = 0xFFFFFFFFU; + entry_calls = 0; + + test_interrupt_disable_depth = 0; + test_interrupt_disable_max_depth = 0; + test_interrupt_restore_underflows = 0; + test_delete_port_completion_count = 0; + test_reset_port_completion_count = 0; +} + + +static VOID victim_entry(ULONG input) +{ + (void) input; + entry_calls++; +} + + +static VOID exit_notify(TX_THREAD *thread_ptr, UINT type) +{ + + if (type != TX_THREAD_EXIT) + { + return; + } + + callback_calls++; + callback_state = thread_ptr -> tx_thread_state; + callback_suspending = thread_ptr -> tx_thread_suspending; + callback_depth = test_interrupt_disable_depth; + + /* The public names. With error checking on these are the _txe_ wrappers; + with it off they are the core services. Either way the refusal has to + come back. */ + callback_delete_status = tx_thread_delete(thread_ptr); + callback_reset_status = tx_thread_reset(thread_ptr); + + /* A refused delete or reset must not have run the port's completion + processing, which on a real port tears the thread's port resources + down. */ + callback_delete_completions = test_delete_port_completion_count; + callback_reset_completions = test_reset_port_completion_count; +} + + +/* Put the victim's control block into the shape the kernel would have left it + in after tx_thread_create, for the fields the four sources under test read. + tx_thread_create itself is not linked, because the scheduler it resumes into + is not linked either. */ + +static void make_victim(UINT state) +{ + + memset(&victim, 0, sizeof(victim)); + memset(victim_stack, 0xEF, sizeof(victim_stack)); + + victim.tx_thread_id = TX_THREAD_ID; + victim.tx_thread_state = state; + victim.tx_thread_suspending = TX_FALSE; + victim.tx_thread_entry = victim_entry; + victim.tx_thread_entry_parameter = 0; +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + /* The field itself does not exist when notification is compiled out. */ + victim.tx_thread_entry_exit_notify = exit_notify; +#endif + victim.tx_thread_stack_start = victim_stack; + victim.tx_thread_stack_size = sizeof(victim_stack); + victim.tx_thread_priority = 16; + victim.tx_thread_preempt_threshold = 16; + victim.tx_thread_suspend_cleanup = TX_NULL; + + /* A created list with the victim and one other thread on it, so that + delete takes the link-up-the-neighbours branch rather than the + only-thread branch. */ + memset(&other, 0, sizeof(other)); + other.tx_thread_id = TX_THREAD_ID; + other.tx_thread_state = TX_SUSPENDED; + other.tx_thread_created_next = &victim; + other.tx_thread_created_previous = &victim; + + victim.tx_thread_created_next = &other; + victim.tx_thread_created_previous = &other; + + _tx_thread_created_ptr = &victim; + _tx_thread_created_count = 2; + + _tx_thread_current_ptr = &victim; + _tx_thread_execute_ptr = &victim; + _tx_thread_preempt_disable = 0; + _tx_thread_system_state = 0; +} + + +static void scenario_completion(void) +{ + + printf("Completion transition:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + + /* _tx_thread_shell_entry picks the thread up from the current pointer, + calls its entry function, and then runs the completion transition. */ + _tx_thread_shell_entry(); + + expect_uint((UINT) entry_calls, 1, "the entry function ran exactly once"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + + /* The terminal state is published before the callback. */ + expect_uint(callback_state, TX_COMPLETED, "the callback saw TX_COMPLETED"); + + /* And the transition is marked while the callback runs, in this + configuration as in the interruptable one. */ + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + +#ifdef TX_NOT_INTERRUPTABLE + + /* In this configuration the callback runs inside the interrupt lock. */ + expect(callback_depth > 0, "the callback ran with interrupts disabled"); +#else + expect_uint((UINT) callback_depth, 0, "the callback ran with interrupts enabled"); +#endif + + /* The two refusals, from whichever of the wrapper and the core service + this build binds the public name to. */ + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + + /* A refused delete or reset ran no port completion processing. */ + expect_uint(callback_delete_completions, 0, "the refused delete ran no port completion"); + expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion"); +#endif + + /* The detachment saw the marker in the state that keeps the thread + deletable afterwards. This is the failure mode the configuration exists + to catch: on a real port the suspension call below returns to the + scheduler and does not come back, so a marker cleared after it rather + than before it would stay set for ever and the completed thread could + never be deleted. */ +#ifdef TX_NOT_INTERRUPTABLE + + expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once"); + expect_uint(ni_suspend_state, TX_COMPLETED, "it was entered with the terminal state"); + expect_uint(ni_suspend_suspending, TX_FALSE, + "the marker was already cleared when the suspension was entered"); + expect(ni_suspend_depth > 0, "the suspension was entered with interrupts disabled"); + expect_uint((UINT) suspend_calls, 0, "the interruptable suspension was not used"); +#else + + expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once"); + expect_uint(suspend_suspending, TX_TRUE, + "the marker was still set when the suspension was entered, for it to clear"); + expect_uint((UINT) ni_suspend_calls, 0, "the non-interruptable suspension was not used"); +#endif + + /* Either way the thread is deletable once the transition has finished. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the completed thread deletes afterwards"); + expect_uint((UINT) test_delete_port_completion_count, 1, + "the successful delete ran the port completion once"); + + /* And the interrupt lock is balanced across the whole transition. */ + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); +} + + +static void scenario_terminate_ready(void) +{ + + printf("Termination of a ready thread:\n"); + + reset_records(); + make_victim(TX_READY); + + /* Terminate a target that is not the current thread, so that the reset the + callback attempts is refused by the transition check alone rather than + by the current-thread rule. */ + _tx_thread_current_ptr = &other; + + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED"); + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + expect_uint(callback_reset_completions, 0, "the refused reset ran no port completion"); + expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack"); +#endif + +#ifdef TX_NOT_INTERRUPTABLE + + expect_uint((UINT) ni_suspend_calls, 1, "the non-interruptable suspension ran once"); + expect(callback_depth > 0, "the callback ran with interrupts disabled"); +#else + + expect_uint((UINT) suspend_calls, 1, "the interruptable suspension ran once"); +#endif + + /* The marker is cleared in the common tail, after the callback and after + the mutex-release processing. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint(victim.tx_thread_state, TX_TERMINATED, "the state is terminal afterwards"); + expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced"); + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); + + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "the terminated thread deletes afterwards"); +} + + +static void scenario_terminate_suspended(void) +{ + +UINT reset_status; + + + printf("Termination of a suspended thread:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + _tx_thread_current_ptr = &other; + + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "the termination succeeded"); + +#ifndef TX_DISABLE_NOTIFY_CALLBACKS + + expect_uint((UINT) callback_calls, 1, "the exit callback ran exactly once"); + expect_uint(callback_state, TX_TERMINATED, "the callback saw TX_TERMINATED"); + + /* The non-ready branch used to clear the marker before this point. */ + expect_uint(callback_suspending, TX_TRUE, "the callback saw the transition marked"); + expect_uint(callback_delete_status, TX_DELETE_ERROR, "the callback's delete was refused"); + expect_uint(callback_reset_status, TX_NOT_DONE, "the callback's reset was refused"); + expect_uint((UINT) stack_build_calls, 0, "the refused reset rebuilt no stack"); +#endif + + /* This branch never calls a suspension routine: the thread was already off + the ready list. */ + expect_uint((UINT) ni_suspend_calls, 0, "no non-interruptable suspension on this branch"); + expect_uint((UINT) suspend_calls, 0, "no interruptable suspension on this branch"); + + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear afterwards"); + expect_uint((UINT) _tx_thread_preempt_disable, 0, "the preempt disable count is balanced"); + expect_uint((UINT) test_interrupt_disable_depth, 0, "the interrupt lock is balanced"); + expect_uint((UINT) test_interrupt_restore_underflows, 0, "no unmatched interrupt restore"); + + /* The documented lifecycle is available afterwards, on this branch too. */ + reset_status = tx_thread_reset(&victim); + expect_uint(reset_status, TX_SUCCESS, "the terminated thread resets afterwards"); + expect_uint(victim.tx_thread_state, TX_SUSPENDED, "the reset left it suspended"); + expect_uint((UINT) stack_build_calls, 1, "the successful reset rebuilt the stack once"); + expect_uint((UINT) test_reset_port_completion_count, 1, + "the successful reset ran the port completion once"); + + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "a reset thread is not deletable"); + expect_uint(tx_thread_terminate(&victim), TX_SUCCESS, "it terminates again"); + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "and then deletes"); +} + + +static void scenario_marker_not_set_outside_transition(void) +{ + + printf("Outside a transition:\n"); + + reset_records(); + make_victim(TX_SUSPENDED); + _tx_thread_current_ptr = &other; + + /* Neither state is terminal, so both services refuse for the reason they + always did, and the marker plays no part. */ + expect_uint(victim.tx_thread_suspending, TX_FALSE, "the marker is clear on a live thread"); + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a suspended thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a suspended thread"); + + /* A terminal state with the marker clear is the ordinary deletable case, + which is what the refusal must not have broken. */ + victim.tx_thread_state = TX_COMPLETED; + victim.tx_thread_suspending = TX_FALSE; + expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "reset accepts a settled completed thread"); + + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "delete accepts a settled terminated thread"); + + /* And a terminal state with the marker set is the new refusal, tested + directly rather than through a transition, so that both services are + covered on both terminal states. */ + make_victim(TX_COMPLETED); + victim.tx_thread_suspending = TX_TRUE; + _tx_thread_current_ptr = &other; + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked completed thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked completed thread"); + + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_DELETE_ERROR, "delete refuses a marked terminated thread"); + expect_uint(tx_thread_reset(&victim), TX_NOT_DONE, "reset refuses a marked terminated thread"); + + /* Clearing the marker makes both available again, which is the whole + claim about the refusal being transient. */ + victim.tx_thread_suspending = TX_FALSE; + expect_uint(tx_thread_reset(&victim), TX_SUCCESS, "clearing the marker restores reset"); + victim.tx_thread_state = TX_TERMINATED; + expect_uint(tx_thread_delete(&victim), TX_SUCCESS, "clearing the marker restores delete"); +} + + +int main(void) +{ + + printf("Thread lifecycle transition configuration test: "); + +#ifdef TX_NOT_INTERRUPTABLE + printf("TX_NOT_INTERRUPTABLE "); +#else + printf("interruptable "); +#endif +#ifdef TX_DISABLE_ERROR_CHECKING + printf("TX_DISABLE_ERROR_CHECKING "); +#else + printf("error checking "); +#endif +#ifdef TX_DISABLE_NOTIFY_CALLBACKS + printf("TX_DISABLE_NOTIFY_CALLBACKS"); +#else + printf("notify callbacks"); +#endif + printf("\n"); + + scenario_completion(); + scenario_terminate_ready(); + scenario_terminate_suspended(); + scenario_marker_not_set_outside_transition(); + + printf("%lu of %lu expectations passed.\n", + expectations_checked - expectations_failed, expectations_checked); + + if (expectations_failed != 0) + { + printf("FAILED: %lu expectations.\n", expectations_failed); + return(1); + } + + printf("SUCCESS!\n"); + return(0); +} diff --git a/test/tx/thread_transition/threadx_thread_transition_host_test_port.h b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h new file mode 100644 index 000000000..204933e3b --- /dev/null +++ b/test/tx/thread_transition/threadx_thread_transition_host_test_port.h @@ -0,0 +1,120 @@ +/*************************************************************************** + * Copyright (c) 2026 Eclipse ThreadX contributors + * + * This program and the accompanying materials are made available under the + * terms of the MIT License which is available at + * https://opensource.org/licenses/MIT. + * + * AI Disclosure: This file was largely AI-generated by Claude Code (Opus 5). + * The AI-generated portions may be considered public domain (CC0-1.0) + * and not subject to the project's licence. The human contributor has + * reviewed and verified that the code is correct. + * + * SPDX-License-Identifier: MIT and CC0-1.0 + **************************************************************************/ + + +/**************************************************************************/ +/**************************************************************************/ +/** */ +/** ThreadX Test */ +/** */ +/** Thread lifecycle transition host test port shim */ +/** */ +/**************************************************************************/ +/**************************************************************************/ + +/* TX_NOT_INTERRUPTABLE and TX_DISABLE_ERROR_CHECKING are not among the five + build configurations either regression tree compiles, and both trees build + the whole ThreadX library once per configuration, so there is no way to + reach those two from inside them. The completion and termination + transitions have code that only exists under TX_NOT_INTERRUPTABLE -- the + callback there runs with interrupts disabled and the transition marker has + to be set and cleared explicitly rather than by the interruptable + suspension path -- and that code is exactly where a marker cleared at the + wrong point would make a normally completed thread permanently undeletable. + It therefore needs a test rather than a compile. + + This harness compiles the four sources that change, plus the two error + wrappers whose absence a TX_DISABLE_ERROR_CHECKING build is defined by, + directly into a test executable, once per configuration. It is deliberately + not part of the coverage report: the same file compiled under different + feature macros has different line sets, and merging them would confuse the + union rather than add to it. The configurations the trees do build are what + the coverage figure is taken from. + + This header is force-included ahead of each source under test. It brings + the base port's headers in first, so every declaration a source needs is the + port's own, and then replaces the primitives that would otherwise reach the + host's pthread emulation: + + - the interrupt lock becomes a counter, which is also how the test asserts + that the transition really did run with interrupts disabled and left the + nesting balanced, + - the delete and reset port completions become recorders, because on this + port they cancel and join the pthread backing the thread, which is not + something a host test that owns no such thread can be asked to do. */ + +#ifndef THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H +#define THREADX_THREAD_TRANSITION_HOST_TEST_PORT_H + +#define TX_SOURCE_CODE + +#include "tx_api.h" + +#undef TX_INTERRUPT_SAVE_AREA +#undef TX_DISABLE +#undef TX_RESTORE +#undef TX_THREAD_DELETE_PORT_COMPLETION +#undef TX_THREAD_RESET_PORT_COMPLETION + +/* Nesting depth of the stand-in interrupt lock, the deepest it has reached, + and the number of times a restore was issued without a matching disable. + Defined by the test, which is the only translation unit that reads them. */ + +extern unsigned int test_interrupt_disable_depth; +extern unsigned int test_interrupt_disable_max_depth; +extern unsigned int test_interrupt_restore_underflows; + +/* Counts of the two port completions, and the interrupt depth each was + entered at. */ + +extern unsigned int test_delete_port_completion_count; +extern unsigned int test_reset_port_completion_count; + +#define TX_INTERRUPT_SAVE_AREA + +#define TX_DISABLE \ + { \ + test_interrupt_disable_depth++; \ + if (test_interrupt_disable_depth > test_interrupt_disable_max_depth) \ + { \ + test_interrupt_disable_max_depth = test_interrupt_disable_depth; \ + } \ + } + +#define TX_RESTORE \ + { \ + if (test_interrupt_disable_depth == 0U) \ + { \ + test_interrupt_restore_underflows++; \ + } \ + else \ + { \ + test_interrupt_disable_depth--; \ + } \ + } + +#define TX_THREAD_DELETE_PORT_COMPLETION(thread_ptr) \ + { \ + (void) (thread_ptr); \ + test_delete_port_completion_count++; \ + } + +#define TX_THREAD_RESET_PORT_COMPLETION(thread_ptr) \ + { \ + (void) (thread_ptr); \ + test_reset_port_completion_count++; \ + } + +#endif