mirror of
https://github.com/eclipse-threadx/threadx.git
synced 2026-10-06 06:59:08 +08:00
Refused thread delete and reset while an exit transition is in progress (#724)
_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) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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? */
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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--;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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? */
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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--;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user