/*************************************************************************** * 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()); }