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