Commit Graph
502 Commits
Author SHA1 Message Date
Frédéric Desbiens e3ed79dbab Stopped tx_thread_relinquish discarding the rebalance it requests (#758)
_tx_thread_relinquish walks the ready list at the relinquishing thread's priority for a
thread to hand its core to. When the walk reaches one it cannot place, carrying
preemption-threshold or excluded from that thread's mapped core, it sets the rebalance
flag and breaks. That exit falls into the block concluding no other thread is ready,
which is not what the walk found, and that block sets the finished flag. The rebalance
at the end of the function is guarded on that flag, so it never runs.

Every rebalance requested from inside the walk is discarded, because that exit always
reaches the block first. A ready thread behind the obstacle then never reaches a core
while the others hold them and relinquish to each other. The tick keeps arriving, so it
is a livelock rather than a deadlock.

The early return now applies only when no rebalance was requested, which lets control
reach the rebalance path the other three request sites already use.

threadx_smp_relinquish_rebalance_test fills the cores with threads of one priority
relinquishing in a loop, places a thread excluded from every core behind them and a
runnable thread behind that, and fails if the runnable one has not run within 100 ticks.
It fails 3 of 3 before this change and passes 5 of 5 after. Instrumenting a passing run
of threadx_thread_relinquish_test counted 12 rebalance requests from the walk against 4
performed, all 4 from sites outside it. 109/109 in all eight configurations, twice from
clean.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 12:43:18 -04:00
Frédéric Desbiens b567428f2a Stopped the Linux port losing a mutex wake-up to a suspend signal (#754)
This is the ThreadX half of the defect fixed for ThreadX SMP in an earlier pull
request. The Linux port suspends a thread with a signal whose handler calls
sigsuspend and does not return until the thread is resumed, and it takes its
critical section with a bare pthread_mutex_lock through tx_linux_mutex_lock. A
thread can therefore be signalled while it is parked on _tx_linux_mutex.

glibc waits for a contended mutex in a loop that re-arms the futex wait after a
signal, and this handler never returns to it. The next unlock hands its wake-up to
that thread, which will not act on it, and any other thread parked on the mutex is
never woken, leaving the mutex free with waiters on it.

tx_linux_mutex_lock now calls a helper that waits with pthread_mutex_timedlock and
retries, so the wait is re-armed every TX_LINUX_MUTEX_RETRY_NSEC and a lost
wake-up costs one retry period instead of the process. The period is one
millisecond. pthread_mutex_timedlock needs _GNU_SOURCE under -std=c99, which both
of this port's build systems already define.

These are the only two ports affected: a sigsuspend-based suspend handler exists
in the Linux and SMP Linux ports and nowhere else, and those two are also the only
ports taking a critical section with pthread_mutex_lock.

Unlike the SMP port, this one has never been observed to deadlock, which is
consistent with one emulated core and far less suspend and resume traffic. The fix
is by inspection, and verified as breaking nothing: all seven configurations pass
with retries disabled, 105/105 in five and 100/100 in two.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 12:42:48 -04:00
Frédéric Desbiens 47a5259002 Stopped the SMP Linux port losing a mutex wake-up to a suspend signal (#753)
The SMP Linux port suspends a thread with a signal whose handler calls sigsuspend
and does not return until the thread is resumed. That signal can arrive while the
thread is parked in pthread_mutex_lock on _tx_linux_mutex; the port knows it can,
because _tx_linux_mutex_obtain sets tx_thread_linux_mutex_access around the lock
call for exactly this case, and nothing anywhere reads that flag.

glibc waits for a contended mutex in a loop that re-arms the futex wait after a
signal, and this handler never returns to it. The next release hands its wake-up
to that thread, which will not act on it, and any other thread parked on the
mutex is never woken, leaving the mutex free with waiters on it. That deadlocks
the process: the only thread that can resume the suspended one is the scheduler,
and the scheduler takes this mutex on every pass.

_tx_linux_mutex_obtain now waits with pthread_mutex_timedlock and retries, so the
wait is re-armed every TX_LINUX_MUTEX_RETRY_NSEC and a lost wake-up costs one
retry period instead of the process. The period is one millisecond, half the
scheduler's own idle period. Nothing else changes.

Four hung processes captured untraced, across three tests, show the same state: a
thread in sigsuspend on top of pthread_mutex_lock, the scheduler blocked in
pthread_mutex_lock, and the mutex reading free. Standalone,
threadx_smp_random_resume_suspend_exclusion_test hung 3 times in 100 runs and 2
in 55 before the change and 0 in 400 after it.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 12:42:20 -04:00
Frédéric Desbiens a9bd72de21 Merge commit from fork
* Fixed the Cortex-A7 module data check to validate whole ranges

The Cortex-A7 module port received a module instance, a start address and a
byte size from the common Module Manager, then discarded the instance and the
size and asked the MMU to translate the start address alone, for reading only.
A range was therefore accepted whenever its first byte happened to be readable
by the module, so privileged dispatch code could read or write past the end of
the module's mapping, or use read-only module code as a write destination.

The check now takes the size and an access intent. The module's own data region
is answered from the manager's records, which name it exactly and cost no
translations; everything else is answered by translating every page the range
touches with the requested unprivileged access. Empty ranges, ranges whose last
byte would wrap, and ranges that leave the recorded data region partway through
are all refused, and the walk is bounded by TXM_MODULE_MANAGER_DATA_CHECK_MAX_PAGES
so one module request cannot impose unbounded work on the kernel. Translation is
only believed while the requesting module's own context is loaded.

The outside direction gets its own check rather than negating the inside one:
a range that reaches into the module only partway is inside neither answer, and
negating a whole-range check would have let it pass as a kernel object.

Other ports keep their single data check through backward-compatible fallbacks
in the common header; their preprocessed dispatch output is byte-identical.

Regression coverage runs on the host by standing a simulated page map behind the
one architecture primitive, and reaches 100% line, branch and call coverage of
the new logic. Twenty-four of its expectations fail against the previous
implementation.

Assisted-by: Claude Code (Opus 5)

* Drove the Cortex-A7 range check through a live MMU

The host test for this check stands a simulated page map behind the port's CP15
primitive, so it never executes an address translation. That leaves the part
most easily got wrong unexercised: an encoding naming the wrong operation, or a
PAR fault bit read the wrong way round, passes it without complaint.

This adds a bare-metal image that builds a short-descriptor translation table,
enables the MMU, and drives the real check through the real translations on a
real Cortex-A7 translation regime, plus a script that builds and runs it under
an emulator. Sections are mapped for unprivileged read/write, unprivileged read
only, and privileged only, with an unmapped section behind the read-only one so
that a range can be made to leave its mapping partway through.

That last case is the one the replaced check accepted, and the test asserts
both answers against the same live MMU: the current check rejects the range,
and translating only its first address accepts it.

It also confirms what the simulated map could only assume, that ATS1CUW denies
a write to a read-only mapping while ATS1CUR allows the read. The write intent
is the reason the port asks for two translations rather than one.

The script skips with a notice when the cross toolchain or the emulator is
absent, so a machine without them does not fail the build.

Assisted-by: Claude Code (Opus 5)
2026-09-28 11:31:41 -04:00
Frédéric Desbiens 5f9ebde847 Merge commit from fork
A memory-protected module's queue request reaches the Module Manager's dispatch
layer, which decides how much of the module's buffer the privileged queue copy
may touch and hands that extent to the module port's range check. A queue's
tx_queue_message_size is a count of ULONGs and the copy moves that many words, so
the extent is message_size * sizeof(ULONG) bytes. Front-send passed the word count
itself. Send and receive, either side of it in the same header, have always
multiplied.

So a source buffer ending message_size bytes into memory the module may reach was
accepted, and _txe_queue_front_send then read message_size * sizeof(ULONG) bytes
from it in privileged mode: 3 * message_size bytes past the end of what the module
is allowed to read, which is 48 bytes at ThreadX's default message size limit and
96 at the limit this test tree builds with. The words land in the queue, and a
module that can receive from that queue reads them back, so the over-read is
disclosed rather than merely performed. Where the memory past the region is not
mapped for the requesting module, the privileged read faults instead.

The buffer cannot be aimed: it has to pass the same check to be accepted at all,
so it is anchored to the end of a region the module can already read and the
overrun is the fixed window immediately after it. That bounds what this reaches;
it does not make it the module's business.

The fix is the multiplication the other two services do, in the units the port
check has always expected. It changes nothing for a module loaded without memory
protection, because the check it corrects is inside the dispatcher's memory
protection block, and the tests hold that.

The regression test drives the three queue dispatchers directly, which nothing in
the tree did before, and measures the extent each one validates rather than
sampling either side of it: for a given message size it walks the room left
between the buffer and the end of the module's region and finds the smallest
amount the dispatcher accepts. That number is the extent. It has to be
message_size * sizeof(ULONG) for all three services, in the module's data, in a
shared region registered to it, and -- for the two services that read the buffer
rather than write it -- in the module's read-only image, which is where a module
sending a constant message sends it from. A receive destination in the read-only
image is refused at every extent.

Two things had to be arranged for a dispatcher to be testable on the host at all.
The dispatch table is a header of static functions wrapped one per service in
#ifndef TXM_<SERVICE>_CALL_NOT_USED, and at -O0 a compiler emits them all, so
linking the whole table would mean standing up every kernel entry point it
reaches. Defining the 93 guards the test does not exercise compiles the header
down to the three queue services, which leaves four symbols to stub and exercises
that conditional compilation, which nothing else in the tree does. The extent also
has to reach a check that honours it, so the test takes its module port headers
from a Cortex-M port, whose inline data check is the shape the memory-protected
module ports share. The Cortex-A7 port's data check takes the pointer alone and
translates a single address, so on that port no extent reaches anything and a test
built on it would pass equally with and without this change.

That is also the affected-port claim: the defect is in the portable dispatch layer
and was live on every memory-protected module port that honours the size it is
given, which is all of them except Cortex-A7, where the port's own check discarded
the size before this one's error could matter.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 11:07:09 -04:00
Frédéric Desbiens ee4c2fb6c7 Merge commit from fork
A memory-protected module's object lookup arrives at the Module Manager's dispatch
layer, which decides how much of the module's name buffer the privileged comparison
may touch. It checked one byte.

_txm_module_manager_object_name_compare reads a character from each name at the top
of every iteration and tests the remaining search length at the bottom, so a declared
length N permits reads at offsets 0 through N: N+1 bytes, the extra one being the
terminator a length excludes. The extended service receives that length in its extra
parameter array, validates the array correctly, and then never uses the length to
bound the buffer it describes. The deprecated service carries no length at all; its
manager wrapper calls the same implementation with the largest value a UINT can hold,
so it removes the bound rather than lacking one.

The comparison stops at the first differing character, so a walk past the end of the
module's memory looks as though it needs the bytes there to match a name the module
chose. It does not. A create service stores the name pointer a module supplies in the
control block rather than copying the string, so a module can register an object whose
name is the very buffer it then searches for. Both sides of the comparison are then
the same address, every character matches by construction, and the walk continues
until it meets a terminator in memory the module does not own and cannot see. That
walk runs in privileged mode with _tx_thread_preempt_disable raised, and none of the
27 memory fault handlers lowers it again, so a fault on it costs more than the
requesting thread.

The fix validates the range the comparison may reach. The extended dispatcher now
checks the name over name_length + 1 bytes, refusing a length whose range cannot be
expressed, and the extra parameter array is checked first because the length comes out
of it. The deprecated dispatcher refuses a memory-protected module outright, because
no range can be derived from a pointer alone; a module without protection is unchanged,
as it is for every other check in this layer.

_txm_module_manager_object_name_compare is left as it is. Reading N+1 bytes for a
declared length of N is the documented contract, and the defect is that the contract
was never checked.

The regression test drives both lookup dispatchers and measures two extents rather than
sampling either side of a boundary, because the finding is the relationship between
them. It anchors the name buffer to the end of one of the module's regions and walks it
backwards to find the smallest room the dispatcher accepts, which is the extent
validated; and it fills memory with a filler character, plants the only terminator at a
chosen depth, and asks for an object named with exactly the bytes up to it, so that the
deepest depth the lookup can be made to return from is the extent read. Against dev's
copy of the two headers the test exits 1 with 408 failed expectations of 1130: a
31-character name with one byte of room is accepted and read 31 bytes past the region,
and the aliased walk reaches every depth offered. With the fix it exits 0 with 1490
expectations and none failed.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 10:58:58 -04:00
Frédéric Desbiens a967d005f6 Merge commit from fork
_txm_module_manager_tx_block_pool_create_dispatch proved that two
ALIGN_TYPE words of the extra-parameter array a module supplies lay
inside the module's data, and then used indices 0 through 3. The two
words it had not proved were read twice each in privileged context:
once by the dispatcher's own buffer check, which takes index 1 as the
pool start and index 2 as the length to range-check it over, and once
when the argument list for _txe_block_pool_create was built. So the
out-of-bounds read was performed by the validation code itself, and
the size the caller's pool start was then validated against was a
word the manager had not proved the module owned.

A module reaches this by calling its kernel dispatcher directly with
an array that ends two words before the boundary of its data or of a
shared region, which the module library's own four-word array never
does. The read window is eight bytes on every module port and there
is nothing in it the module can lengthen.

The extent becomes sizeof(ALIGN_TYPE[4]), which is what the two
siblings of the same shape have always had: queue create validates
four words for four, and byte-pool create three for three. Both
checks on this path are data-only, with no fallback to the portable
code-region check, so the four Cortex-A35 and Cortex-A35 SMP module
port combinations whose data check is the constant (TX_SUCCESS) refuse
these create requests from a memory-protected module before and after
this change alike.

The regression test drives all three create dispatchers and measures
the extent each one validates rather than sampling either side of it:
it anchors the array at the end of each region a module owns and walks
it backwards until the dispatcher accepts, and the smallest room
accepted is the extent. It measures the highest index each dispatcher
uses through what the stubbed service records, so the invariant the
three are checked against is measured on both sides. It also measures
what a module can learn from the answers, since the buffer check is a
comparison against a threshold the module chooses and the dispatcher's
refusal is distinguishable from every status these services return.
Compiled against the previous dispatch header the test reports the
extent as two words and the service reached carrying a word planted
past the end of the region; against this one it reports four.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 10:52:12 -04:00
Frédéric Desbiens 35d4137126 Merge commit from fork
A module calls txm_module_object_allocate and chooses object_size. The manager
adds sizeof(TXM_MODULE_ALLOCATED_OBJECT) to it and asked the object pool for the
result without checking the addition, so a size near the top of a ULONG wrapped:
object_size 0xFFFFFFF8 asked for eight bytes, the pool served them, and the
manager then wrote its sixteen-byte header -- owner, list links and size -- into
that eight-byte block and linked it into the module's allocation list. Eight
bytes of the next block's contents or header are gone by the time the call
returns, and the shared object pool that every module allocates from is the thing
that was corrupted.

The addition is now made with the repository's own overflow-checked helper, which
was already used on both load paths and simply never reached this one, and it is
made before the protection mutex is taken so a refused request leaves the pool,
the allocation list and its count exactly as they were. Sizes that survive the
addition need no further bound here: _txe_byte_allocate refuses a request larger
than the pool, so the alignment round-up in _tx_byte_allocate is never reached
with a value that could wrap in its turn.

Regression coverage runs on the host by modelling the byte pool behind the
manager. The model applies the two bounds _txe_byte_allocate applies and hands
out a block of precisely the requested length with a guard band immediately
after it, so an undersized allocation is caught as the out-of-bounds write it is
rather than inferred from arithmetic. Thirty-two of its expectations fail against
the previous implementation, twenty reporting state a refused request must not
have touched and two reporting the eight and twelve bytes written past the end of
the block. It reaches 100% line, branch and call coverage of the changed
function; the lines it leaves uncovered are its own failure reporting, plus the
modelled pool's zero-size refusal, which only an unfixed build reaches.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 10:37:13 -04:00
Frédéric Desbiens ff4be30656 Covered the control block ID cleared when module memory is given back (#779)
The control block ID that _txm_module_manager_object_deallocate clears on its way
out is read by the size-based object check, which is the one kept for dispatchers
outside this repository. Nothing exercised that path. The expectations covering
the clearing all went through object authentication, which consults the kernel's
created list and would refuse a recycled address whatever its ID said, so they
would have passed with the clearing removed.

A section drives the size-based check directly. A module plants the value of an
ID into memory it owns, gives the allocation back, and is handed the same address
again; the check accepts the planted ID before the deallocation and refuses it
afterwards, which is the difference the clearing makes.

The authentication test holds 125 expectations and passes. Removing the ID
clearing fails it. The full suite passes 107/107 in default_build_coverage with
no warnings.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 10:22:07 -04:00
Frédéric Desbiens 2930618cfe Merge commit from fork
* Refused to give back the memory of a live kernel object

A module allocates the control blocks of its kernel objects from the Module
Manager's object pool and can ask for that memory back by address. The manager
released it whatever was in it, including a control block the kernel was still
using. Deallocation is not deletion: the object stays on the created list for its
type, a thread stays wherever it was on the ready or suspension lists and stays
schedulable, and an active timer stays on the timer list. Nothing on those paths
consults a control block ID, so nothing about the memory having been freed stops
the kernel from using it -- a created list walk reads a name pointer out of it and
follows that pointer, a create or delete of another object of the same type writes
through the created links in it, the scheduler switches the stack pointer to the
word at offset 8 of it and pops a saved processor state, and timer expiration
calls the function pointer in it. Meanwhile the byte pool is free to hand those
same bytes to the next allocation, so what the kernel goes on reading as a control
block becomes whatever the next owner of the memory puts there, through ordinary
create services.

The manager now refuses to release memory that holds an object which is still
created, and returns TX_DELETE_ERROR without touching the allocation, the object
or the memory. Deleting the object first is what makes its memory releasable,
which is the sequence the delete dispatchers already follow and the one module
authors are told to follow.

The question is answered from the kernel's created lists, not from the control
block. A control block ID is not evidence that an object is there: an allocation
that was never created can be carrying the value of an ID, and refusing on that
would strand memory a module is entitled to have back. Deallocation is also given
an address and nothing else, so unlike a typed service it cannot be told which
list to search, and each of the eight lists is searched in turn. The search is not
narrowed by the size of the allocation, because that would be sound only if every
object had been created through a size-checked path, and a module running without
memory protection creates objects through no such path -- a queue at the start of a
thread-sized allocation is a case the tests here cover. Each list is searched in
its own interrupts-disabled window, bounded by the count the kernel keeps beside
it, so the longest window is the length of one type's list and a list whose links
have been damaged cannot make the search run on. The whole search is one pass over
the objects the system has created, paid once per object deallocation.

Storage that is not an object is released exactly as before, which is what keeps
cleanup after a create that failed or was abandoned working, and what makes the
release each delete dispatcher performs after a successful delete go through.

The address the request arrives with is now checked before the manager's private
header in front of it is read, rather than partly after. The size of the
allocation comes from that header, so there is nothing to validate a size against
until the header has been read, and the previous order established only where the
header started: a header that began inside the pool and ended past it had its size
word read from outside the pool. That check has moved out of the dispatcher into
_txm_module_manager_param_check_object_for_deallocation, alongside the other
parameter checks the dispatch table uses and where a test can reach it, and it now
also refuses a size that would carry the end of the allocation past the top of the
address space instead of wrapping it, since a wrapped end compares as though the
allocation were inside the pool.

The 301 expectations in the new test drive all eight object types through allocate,
create, a refused deallocation, delete, and a deallocation that succeeds, and
assert after the refusal that nothing reached the pool, that the allocation is
still on the module's list at the head of it, that the control block still carries
its ID and that the object is still live. They cover storage that was never
created, storage carrying nothing but a plausible ID for each of the eight types,
an object at the start of an oversized allocation, every aligned interior offset
of a live object with that object's own ID planted at it, an application-owned
object outside the pool, another module's allocations both live and raw, releasing
the head of a list of several and releasing the same address twice, a type that has
no created list, the bounds on the search against a list longer than its count and
against a count larger than its list for every type, the boundary addresses at both
ends of the pool, a crafted size, and an object pool that was never created.
Removing the refusal fails 43 of them; removing the size wrap guard fails one.

Line and branch coverage of the three new functions and of the changed
_txm_module_manager_object_deallocate is 100%, with one exception that is test
scaffolding rather than product code: the host shim's stand-in for TX_RESTORE has
an underflow guard, and the test asserts that branch is never taken. All 99 tests
pass in each of the five configurations the tree builds with GCC 14.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Removed the object deallocation bounds check superseded by the search

_txm_module_manager_param_check_object_for_deallocation() bounded the private
header in front of a caller's address before the deallocator read it. The
deallocator no longer reads that header: it finds the allocation by searching
the module's own allocation list, which never dereferences the address, so the
bounds check now guards a read that does not happen.

The function, its prototype, its macro and its one call site in the
txm_module_object_deallocate dispatcher are removed, along with the twelve
expectations that covered it and three declarations left unused by their
removal. The search proves more than the check did: the bounds test established
only that the header lay inside the object pool, while the search establishes
that the address is the exact start of one of this module's allocations.

The live object deallocation test holds 289 expectations and passes. Reverting
the live-object guard still fails 51 of them, the same 51 as before the
deletion, so nothing the removed expectations covered was load-bearing. The
full suite passes 108/108 in default_build_coverage, with no warnings.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 10:13:22 -04:00
Frédéric Desbiens 3276b0efd5 Merge commit from fork
A module reaches a kernel object's delete service through the Module Manager's
dispatch layer, which asked one question about the pointer: does the object lie
outside the module's own code and data. That is the policy for using an object,
and using an object a module does not own is supported and intended --
txm_module_object_pointer_get_extended searches the system's created objects by
name and hands back objects the application created and objects other modules
created, so that a module can send to a shared queue, take a shared semaphore or
get a shared mutex. Destroying one of those is not sharing it. A module could
name any object it had a pointer to and have the privileged dispatcher delete it:
a host service lost its queue, waiters were resumed with TX_DELETED, an owned
mutex's priority inheritance was unwound, and an active timer stopped.

Nothing in the manager intended that. The deallocation the delete path performs
afterwards has always required the memory to belong to the calling module, so a
delete of anything else could only ever end in TX_PTR_ERROR -- the ownership
requirement was already there and was enforced one step too late, after the
kernel object had been irreversibly destroyed and its waiters woken. An error
returned at that point describes a cleanup failure and restores nothing.

The eight delete dispatchers now ask the question before the delete rather than
after it. A memory-protected module may delete an object only if the object is
one it allocated from the manager's object pool, at the exact address the manager
returned to it, for an allocation made for that type's control block size -- the
same conditions the create dispatchers already apply, since deletion is the
inverse of creation. Anything else returns TXM_MODULE_INVALID_MEMORY, which is
what every other parameter rejection in the dispatch table returns, so no new
value enters the module ABI and a module cannot use a delete request to tell
"not yours" apart from "not a usable address". The object stays created, nothing
waiting on it is resumed, and no created count moves, because the kernel was
never asked.

The ownership question is deliberately separate from whether an object is there
at all and of the expected type. It establishes nothing about liveness or type,
and it is composed with the checks that do rather than replacing them.

_txm_module_manager_object_deallocate reached the manager's private header by
subtracting from whatever address the caller supplied, and read it before
deciding whether it was a header. All eight delete dispatchers call that function
directly, so an object the module does not own arrived there as a matter of
course rather than exceptionally: for an object the application allocated
statically, or for any address outside the object pool, the words in front of it
were unrelated memory read in privileged mode -- and acted on, since an address
whose preceding words happened to name the calling module was unlinked from that
module's allocation list and handed to the byte pool.

It now finds the allocation by searching the module's own allocation list. The
caller's address is compared and never dereferenced, so an address that names
none of this module's allocations is refused without a privileged read of
anything in front of it, and the search establishes what the header read could
not: that the address is the exact start of an allocation rather than somewhere
inside one. The search is bounded by the count the manager keeps beside the list
and runs with interrupts disabled, so a damaged list cannot make it run on and it
cannot observe the list being changed under it. The fix is in the deallocation
itself rather than at a call site, so it covers all eight delete dispatchers, the
deallocation request a module can make directly, protected and unprotected
modules alike.

txm_module_manager_stop needs no exemption and was not given one. It deletes the
objects a module created by calling the internal _tx_*_delete services directly,
and identifies them with _txm_module_manager_created_object_check rather than
through a module request, so no caller-facing check stands in its way.

The 209 expectations in the new test drive all eight object types through
allocation, an ownership check by the owning module and by another, a check
against a larger and a smaller expected size, and a deallocation that succeeds.
They cover an object the application owns, deliberately preceded by a header
naming the requesting module so that reading it can be seen to have happened;
another module's object, checked and refused from both sides; every aligned
interior offset of an allocation and the addresses of its header, its end, the
pool's ends and one past the pool; a null pointer and an address with no room for
a header in front of it; a request from no module at all; memory that has changed
hands, where a stale pointer names an address that now belongs to another module;
the bounds on the search, against a count smaller than the list and against a
count larger than a list whose links are broken; releasing the head, the middle
and the last of a list of three; and an object pool that was never created. Every
call asserts that the interrupt lock came back balanced, and that the search ran
with interrupts disabled exactly one deep.

Restoring the previous deallocation fails four of them and then segmentation
faults, on the null-pointer case, where the header in front of address zero is
read; removing the ownership check fails 74.

Line and branch coverage of the two new functions and of the changed
_txm_module_manager_object_deallocate is 100%, with two branch outcomes excepted
that are test scaffolding rather than product code: the host shim's stand-ins for
TX_DISABLE and TX_RESTORE carry a maximum-depth test and an underflow guard, and
the real ports' primitives are inline assembly with no branch at all. All 99
tests pass in each of the five configurations the tree builds with GCC 14.

Nothing in the tree compiles the dispatch header, so the eight changed
dispatchers were cross-compiled by hand: the two changed C files and a
translation unit that includes the header build at -Werror with arm-none-eabi-gcc
13.2.1 for every GNU 32-bit module port -- Cortex-A7, M0+, M23, M3, M33, M4 and
M7 -- and produce the same -Wall -Wextra warning counts as before the change, 117
on Cortex-A7 and 116 on each of the others. Cortex-R4 and RXv2 have no GNU module
port, and the two AArch64 module ports have no toolchain available here; the new
arithmetic is a comparison of two addresses of the same type, so a wider
ALIGN_TYPE changes nothing about it.

MISRA: all if bodies are braced, the address comparison goes through ALIGN_TYPE,
no pointer the caller supplied is dereferenced, and no goto appears. No deviation
is required.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 09:23:49 -04:00
Frédéric Desbiens 7495b02374 Merge commit from fork
A memory-protected module names the kernel objects it wants operated on by
address, and the Module Manager decided whether a privileged service could
dereference that address by asking only whether it fell outside the module. The
object pool is outside every module, so that test was satisfied by an address
shifted into the interior of one of the module's own privileged allocations,
which denotes no object at all. The bytes such an address presents as a control
block are bytes the module put there through ordinary create and set services,
so the control block ID at the front of them could be made to read as any type
the module chose, and the _txe_ layer's ID test then agreed. A module could
therefore have the kernel read and write fields of an object that does not
exist, at an address it picked; the reported chain reaches a privileged memset
across an attacker-chosen range that way.

Authentication now answers the question the location test could not: is this the
exact address of a live kernel object of the type this service expects. It is
answered from the kernel's created list for the type, which the create and delete
services maintain, so membership establishes at once that the address is an
object start rather than an address inside an object, that the object is of this
type, and that it has not been deleted. That list is also what makes an object
the application created and shared with a module authenticable at all, since the
manager allocated no such object and has no record of its own to consult, so
sharing keeps working and needs no new registration API.

Nothing a module can influence is used to establish a type. An earlier form of
this fix accepted an address that was the exact start of one of the calling
module's own allocations of the right size and then took the type from the
control block ID, which is cheaper -- a module's allocation list is much shorter
than the system's created list for a type. The tests here refused it: a byte
pool, a mutex and a timer are all the same size on a 32-bit target, so an
allocation created as one of them and presented as another passed both the
address and the size test, leaving the ID as the only thing between the module
and a type confusion. The ID is still checked, after the created list has settled
the question, because it is the test the _txe_ services make and it is compiled
away with them under TX_DISABLE_ERROR_CHECKING. An address a module named is not
read at all until a kernel record says there is an object there.

All 58 object-using dispatchers now pass the object type rather than a control
block size, since a size cannot establish a type. Both scans are bounded by the
counts the kernel and the manager maintain beside their lists, so the cost of one
check is bounded and a list whose links have been damaged cannot make a scan run
on, and both run with interrupts disabled, which is the protection those lists
are maintained under and which adds no blocking point or priority inversion to a
kernel request. The cost is a walk of the created list for the type, paid only by
memory-protected modules; the size-based check is kept for dispatchers outside
this repository and hardened to refuse object pool interiors, which is the part
of the attack it can see without a type.

Two further changes close paths the authentication alone would leave open. Thread
reset now refuses a thread that carries no module instance: such a thread is
authentic, can be found by name and satisfies reset's own state test, and reset
reads the shell entry function out of that instance, so a null one was followed
in privileged mode. Object deallocation now clears the control block ID as it
gives memory back, so an object freed without being deleted first stops being
vouched for at the moment the manager stops owning its memory, rather than
returning to the pool still carrying a valid ID for the next allocation placed
there to present.

The 120 expectations in the new test offer every aligned interior offset of a
legitimate object as a foreign type, with that type's own ID planted at the
offset, and assert that none is accepted; they cover all eight object types
against each other, uncreated allocations, deleted objects, freed addresses that
have been handed out again, objects the application owns and memory that merely
carries a plausible ID, objects another module allocated, and the bounds on both
scans. Reverting the object checks to the permissive policy fails 89 of them;
removing the thread reset guard makes the test die with SIGSEGV inside the reset
path; removing the ID clearing in deallocation fails 2. All 99 tests pass in each
of the five configurations the tree builds with GCC 14.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 09:08:55 -04:00
Frédéric Desbiens d55fe1ef2f Stopped the release script adding a Co-authored-by trailer (#777)
prepare_release.sh committed the version passes with a Co-authored-by trailer naming an
AI. That trailer asserts authorship an AI cannot hold: the human contributor signs the
ECA and is solely responsible for the contribution. It is already in the published
history, on the 6.5.1.202602 and 6.5.1.202602a preparations.

The commits now carry their subject alone. A version pass is mechanical sed output, so
no agent produces it at run time; an agent that runs the script records its own
Assisted-by trailer on that run instead. The script also gains the AI disclosure line it
was missing.

Ran the patched script against a scratch clone targeting 6.5.2.202603. Both commits come
out with an empty trailer block, the version constants and 211 port version strings
update as before, and check_ports.sh passes.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-28 07:28:23 -04:00
Frédéric Desbiens b37cd4a81a Fixed RISC-V regression portability failures (#773)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
RISC-V regression builds ran only at -O0, leaving a timer callback counter that
spins forever at -O2. The trace regression was excluded because it referenced
a port-specific interrupt-save variable, and its adjacent pools could start
misaligned on RV64.

Made the timer counter volatile, gave the trace test aligned pool storage and a
portable saved-interrupt value, and enabled it on RISC-V. Added an -O2 QEMU
configuration to keep the optimized failure covered.

CMake/Ninja/QEMU: RV64 default, optimized and trace suites passed 97/97 each;
RV32 passed 96/96 each. Two ISR event tests passed 30 repeats each at -O2.
The Linux/GCC 14 trace test passed. The reported timing resonance did not recur.

Assisted-by: Codex (gpt-6-sol) <noreply@openai.com>
2026-09-23 16:52:44 -04:00
Frédéric Desbiens 70a5300977 Added a ThreadX module manager port for the Cortex-R52 (#639)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
Added the first GNU ThreadX module port for an Arm R-profile core.

  The port combines the PMSAv8-R MPU model from Cortex-M33 with the AArch32
  privilege and processor-mode handling from Cortex-R4. It provides the complete
  module path: headers, manager sources, scheduler integration, user-mode entry,
  SVC dispatch, data- and prefetch-abort capture, fault notification, relocatable
  module loading, and shared-memory regions.

  Module isolation uses eight MPU regions (8–15) with a 64-byte granule. The
  scheduler replaces those regions on each module switch and manages a separate
  privileged loading window in region 16. Assembly-visible structure offsets and
  region-layout assumptions are checked at build time.

  Added independent module demonstrations for the S32Z280-594EVB and Armv8-R AEM
  FVP. The automated FVP regressions cover:

  - Loading the same position-independent module at different addresses
  - User-mode data and instruction access violations
  - Fault capture and notification for both abort types
  - Shared-region access, alignment, exhaustion, empty-size, and overflow handling
  - Required module-property combinations
  - The GCC CLZ-based priority search

  The port requires user mode and memory protection together, at least 17 EL1 MPU
  regions, and currently validates A32 modules; Thumb module execution remains
  unvalidated.

  Validated with GNU Arm 14.3.1. The FVP suites pass 8/8 in the default
  configuration and 11/11 with hard-float, FIQ, and interrupt nesting enabled.
  The S32Z280 images build cleanly, and the module isolation and relocation paths
  were exercised on S32Z280 silicon during development.

  Matching user documentation is provided by rtos-docs-asciidoc PR #41.

  Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-17 15:39:30 -04:00
Frédéric Desbiens ad558a7b1f Fixed the zero trace time stamps in the Linux ports' MISRA builds (#749)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
Both Linux ports define TX_TRACE_TIME_SOURCE as _tx_misra_time_stamp_get() when
TX_MISRA_ENABLE is set, and neither implements that function, so both inherit
the generic `return(0);` from tx_misra.c. Every trace event is stamped zero.
The buffer carries no timing, and the kernel's own check for an entry having
been overwritten -- time_stamp against the entry's own stamp, in the block and
byte allocates and in the system suspend and resume -- compares zero with zero,
so it never fires and a service patches whatever now occupies the slot.

Both ports now read in MISRA builds the clock they already read otherwise,
_tx_linux_time_stamp.tv_nsec, which TX_TRACE_PORT_EXTENSION refreshes on every
recorded event in both forms of the insert. The non-SMP port's non-MISRA macro
carried a trailing semicolon, which made it a statement and is why the MISRA
insert -- which takes the time source as a function argument -- could not use
it; that is dropped and the two branches become one definition. Both headers
keep the _tx_misra_time_stamp_get declaration, because tx_misra.c still defines
it and is compiled for these ports.

The MISRA insert evaluates its time source before the callee refreshes the
clock, so each entry carries the reading taken at the previous recorded event.
Stamps are real, distinct and ordered, which is what the overwrite check needs.

The trace entry update test gains an assertion that the buffer holds an entry
the port actually stamped. It fails on dev with ERROR #13 under
misra_trace_build and passes with this change. Suites green: 7/7 ThreadX
configurations, 5/5 SMP.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-16 14:01:07 -04:00
Frédéric Desbiens e9b5aec65e Added MISRA build configurations to the ThreadX regression matrix (#747)
TX_MISRA_ENABLE routes the kernel's pointer conversions, its memset and its
stack check through the shim in common/src/tx_misra.c. No build configuration
defined it, so that file was never compiled or tested, and the source the MISRA
analysis runs against was not the source the suite exercises. Adding event
tracing alongside it reaches a further region of the shim that neither macro
alone compiles. Both defects found in this area were invisible for the same
reason: #741 broke under TX_MISRA_ENABLE and #745 under both macros together,
and neither combination was built anywhere.

misra_build and misra_trace_build fill that gap. Two things had to give way for
them. TX_POINTER_TO_ALIGN_TYPE_CONVERT exists only when TX_MISRA_ENABLE is
absent, so threadx_test_port.h writes the conversion out rather than taking it
from the API; it is test scaffolding storing a pointer in a word wide enough to
hold one, not kernel source. And thread_transition and module_manager compile
hand-picked common/src sources directly instead of linking the library, which is
what lets them reach feature macros the library-per-configuration model cannot;
under TX_MISRA_ENABLE those sources call into the shim, and pulling the shim in
pulls its own callees after it. Neither directory exists to test the shim, so the
MISRA configurations skip them.

100/100 tests pass in each of the two new configurations, against 105/105 in the
existing five - the five not run are the four thread transition tests and the
module manager test, exactly the two directories skipped. Build is 4 to 5
seconds and the suites 13 and 15 seconds, against 4 seconds and 9 to 17 for the
configurations already there, so the tx job grows by roughly forty seconds.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-16 09:55:24 -04:00
Frédéric Desbiens 24d1f278c2 Decorrelated the wait abort ISR test's interrupt source from the thread it samples (#743)
threadx_thread_wait_abort_and_isr_test waits for the periodic timer interrupt to
land while the preempt disable flag is set. The same interrupt also puts the
semaphore that wakes the thread being sampled, so the two run in lockstep: the tick
wakes thread 0, thread 0 does a fixed amount of work and suspends, and the next tick
arrives a fixed interval later with thread 0 in the same place every time. That is
the resonance the handler's own comment describes, and perturbing the handler's
duration only shifts the phase rather than breaking the correlation. The window
itself is a few instructions wide, so a sampler locked to the thread's own cycle can
miss it indefinitely, which is what #644 and #649 measured and worked around.

The simulator's timer thread waits on _tx_linux_timer_semaphore with a one-tick
deadline and delivers an interrupt early when the semaphore is posted, which the port
already relies on in _tx_thread_schedule. The test now runs a plain POSIX thread that
posts it, injecting interrupts at moments unrelated to the tick grid and sampling
thread 0 at arbitrary points in its cycle rather than the same one. The injector posts
only when nothing is outstanding, so interrupts can never be queued faster than they
are serviced. It is confined to the Linux simulation port and no port file changes.

The count of windows asked for goes back to ten, on the same reasoning that lowered
it to three: ask for what a run can actually reach. Measured over six build
configurations of a branch carrying this change, every one reaches ten of ten in under
a second, against one to three of three previously with three of the six pinned at the
180 second budget. The budget and the zero window ceiling are untouched, so a run that
somehow still falls behind behaves exactly as it does today.

The SMP copy deliberately does not get the injector, and its count stays at twenty. It
has never been in the slow mode, and injecting interrupts there measurably hurts:
8.9 seconds against 0 to 1 for the same twenty windows, which is the extra interrupt
traffic contending across the simulated cores. Only the tx copy has the problem this
solves, so only the tx copy changes; the budget and ceiling logic stays identical
between them.

Verified on this branch: the tx suite passes 105 of 105 with the test at 0.44 seconds,
and four direct runs reach ten of ten in 0 to 1 seconds. The SMP suite passes 117 of
117 unmodified, with its own test at 0 to 1 seconds over four runs.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-16 09:18:41 -04:00
Frédéric Desbiens b93d1ee92f Fixed the simulator ports and thread create paths so they compile when TX_MISRA_ENABLE is defined (#742)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
* Fixed the simulator ports so they compile when TX_MISRA_ENABLE is defined

_tx_thread_stack_build() in the four simulator ports converts the fake stack
pointer through TX_POINTER_TO_ALIGN_TYPE_CONVERT and
TX_ALIGN_TYPE_TO_POINTER_CONVERT. tx_api.h defines both macros only in the
non-MISRA branch of its #ifdef TX_MISRA_ENABLE, so with that macro defined the two
names are undeclared and none of the four files compiles.

Reproduced with:

    gcc -m32 -c -DTX_MISRA_ENABLE -I common/inc -I ports/linux/gnu/inc \
        ports/linux/gnu/src/tx_thread_stack_build.c -o /dev/null

which reports both names as implicit declarations and then an int to pointer
assignment. The same command without the define compiles cleanly.

No build configuration under test/tx/cmake or test/smp/cmake defines
TX_MISRA_ENABLE, so CI never compiles these files in that mode. It surfaced on a
branch that carries such a configuration.

The conversions are now written inline, which is what the non-MISRA macros expand
to and what the surrounding port code already does, including the line this
replaced.

The alternative would be the idiom common/src/tx_thread_create.c uses for the same
conversion: an explicit #ifdef selecting the ULONG pair under MISRA. That is not
equivalent here. On __x86_64__ this port defines ULONG as unsigned int and
ALIGN_TYPE as unsigned long long, so a pointer round-tripped through the ULONG pair
loses its top 32 bits. Writing the conversion inline keeps one form that is correct
in both modes and on both widths.

Verified by compiling the Linux port with and without TX_MISRA_ENABLE, both clean.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Fixed the same MISRA build break in the SMP and module manager thread create

_tx_thread_create() in common_smp and _txm_module_manager_thread_create() convert the
thread's stack start through TX_POINTER_TO_ALIGN_TYPE_CONVERT and
TX_ALIGN_TYPE_TO_POINTER_CONVERT with no conditional at all. tx_api.h defines both
macros only in the non-MISRA branch, so with TX_MISRA_ENABLE and
TX_ENABLE_STACK_CHECKING both defined neither file compiles. It is the same defect as
the simulator ports in the previous commit, in two more files.

Reproduced with:

    gcc -c -DTX_MISRA_ENABLE -DTX_ENABLE_STACK_CHECKING \
        -I common_smp/inc -I ports_smp/linux/gnu/inc \
        common_smp/src/tx_thread_create.c -o /dev/null

which reports both names as implicit declarations. Both files now compile with and
without TX_MISRA_ENABLE.

The conversions are written inline for the same reason as the ports: the #ifdef idiom
that common/src/tx_thread_create.c uses selects the ULONG pair under MISRA, which
truncates a pointer wherever ALIGN_TYPE is wider than ULONG. That is reported
separately.

Verified: the SMP regression suite passes 117 of 117, and the ThreadX suite still
builds.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-15 23:19:32 -04:00
Frédéric Desbiens 26e4aa182c Fixed the build of tx_misra.c with MISRA and event trace enabled (#746)
tx_misra.c defines five pointer conversions for the trace component, and their
prototypes sit in tx_trace.h behind TX_SOURCE_CODE. tx_misra.c is the one kernel
source that does not define that macro, so it defines all five with no previous
declaration. Under -Wmissing-declarations -Werror, which the kernel target is
built with, the file does not compile at all, so TX_MISRA_ENABLE and
TX_ENABLE_EVENT_TRACE cannot be enabled together. No build configuration defines
both, which is why this has never surfaced.

The five prototypes move out of the TX_SOURCE_CODE guard into their own block,
next to where tx_api.h already declares _tx_misra_trace_event_insert - the sixth
function of the same region, which escapes the problem for exactly that reason.
Declarations only; no definition moves and no macro changes meaning.

All 185 files in common/src now compile clean under all four combinations of the
two macros with -Wall -Wextra -Wmissing-declarations -Werror, against five errors
in tx_misra.c for the both-enabled case before. Object code is byte identical for
the three combinations that already built: 0 of 185 files differ in each.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-15 23:08:23 -04:00
Frédéric Desbiens 9e4c57138d Normalized the AI disclosure comment to one fixed line per file (#740)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
The per-edit disclosure named the product and model, so every agent and every
model version appended another line. 74 files carried two to four of them, and
the same five products had accumulated 13 spellings -- Copilot against GitHub
Copilot, Claude Sonnet 4.6 against claude-sonnet-4.6, four spellings of Codex.
Twenty assembly lines carried a doubled comment marker, `; //` or `@ //`.

Every file now carries exactly one line, fixed text naming no product:

    Portions of this file were generated with AI assistance.

It is written with the comment character that file already uses, so the `;`
and `@` assembly files keep theirs and the doubled markers are gone. Precise
attribution stays on the commit, where the Assisted-by trailer is per-change,
dated and attached to the diff it describes. A header line cannot hold that
record honestly, because the code it names gets rewritten and the line stays.
A file-level flag answers whether; the history answers who.

Comment-only. 455 files, 455 insertions and 574 deletions: every removed line
was a disclosure line, every added line is the fixed text, and no file is left
with zero or with more than one. `scripts/check_ports.sh` passes, including the
reproducibility check that would catch a ports_arch master and its generated
copies drifting apart. Recompiled against dev, every file that builds without a
vendor toolchain gives a byte-identical object: 19 of 19 C files under common,
100 of 100 GNU assembly files, and all 16 assemblable files whose comment
marker changed. The 10 remaining marker changes are ac5 and IAR sources where
`;` already started the comment and only the redundant `//` was removed.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-15 17:17:26 -04:00
Frédéric Desbiens f5e59a1dd3 Completed Windows simulator support and regression coverage (#736)
Completed and stabilized the Win32 and Win64 MSVC simulator ports.

- Replaced high-latency host synchronization with bounded scheduler handoffs and
  critical sections.
- Added a high-resolution timer, tick batching, idle fast-forward, shutdown
  coordination and Win64 extension-pointer support.
- Brought the Windows regression tooling and the new thread-transition tests up
  on CMake, Ninja and the Visual Studio Build Tools.
- Extended the SMP teardown diagnostics and corrected 64-bit trace-test handling.

This supersedes the historical `win64`, `win32-perf`, `windows-sim-ports` and
`windows-sim-ports-completion` branches; no unmerged change from them is missing
here. The original Win64 port landed in #529.

1,610 of 1,610 tests pass, across five configurations each: Win32 515, Win64 515,
Win64 SMP 580. No external dependency was added, and the existing MSVC warnings
in the trace configuration are unchanged. Hardware validation does not apply to
host simulator ports.

Assisted-by: Codex (gpt-5.6-sol) <codex@openai.com>
2026-09-15 16:47:08 -04:00
Frédéric Desbiens 30b3d22adc Restored the random stack fill value cleared during thread creation (#732)
Fixes #723

With `TX_ENABLE_STACK_CHECKING` and `TX_ENABLE_RANDOM_NUMBER_STACK_FILLING` both
enabled, `_tx_thread_create` picked a random byte, stored it in
`tx_thread_stack_fill_value`, filled the stack with it -- and then cleared the
whole control block with `TX_MEMSET`. The stack held the pattern while the
control block claimed zero, so `TX_THREAD_STACK_CHECK` and
`_tx_thread_stack_analyze` compared against the wrong value for the entire life
of the thread.

The value is now computed into a local and written back after the clear. The
clear stays where it is, because the module manager's error checking walks the
created list before the control block may be touched.

Fixed in `common/src/tx_thread_create.c`, `common_smp/src/tx_thread_create.c`
and `txm_module_manager_thread_create.c`, where it additionally left a user-mode
module thread's kernel stack filled with zeros.

`threadx_thread_stack_fill_value_test` creates sixteen unstarted threads and
checks each control block against the pattern in its stack, tolerating a random
zero byte without letting that hide the defect. Added to both suites: `ERROR #3`
on `dev` in `stack_checking_rand_fill_build`, green with the fix. Five tx
configurations at 104 tests, five SMP at 117, and all three sources clean under
`-Wall -Wextra` across every combination of the four stack-filling switches.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:58 -04:00
Frédéric Desbiens 201cc04609 Applied R_ARM_RELATIVE relocations at GNU ThreadX module startup (#731)
Fixes #230

`gcc_setup.s` rebases the GOT and copies initialized data into the module data
area, but never applies the relocations the linker records for words inside that
data. A global initialized with a link-time constant -- `char *pBuffer = buffer;`,
a function pointer, a struct member holding a string literal's address -- kept
its link address and pointed outside the loaded module.

`gcc_setup.s` now walks `.rel.dyn` after the data copy and adjusts every
`R_ARM_RELATIVE` entry targeting the module data area, reusing the GOT loop's
base arithmetic and skip rules, and reloading the flash base first because
`crt0_memory_copy` clobbers `r3`.

The table is only emitted with `-pie`, so the example build scripts now pass
`-pie --no-dynamic-linker` and the example linker scripts discard `.dynamic`,
which `-pie` would otherwise place at address zero and inflate the image to
nearly 200 KB. Without `-pie` the table is empty and the loop is a no-op.

Covers Cortex-M3, M4, M7, M33 and M0+. Cortex-M23 ships no module linker or
build script and AArch64 uses a different relocation format; both left for a
follow-up.

Verified on `qemu-system-arm` with a module linked at one address and loaded at
another: char, function, string and struct-member pointers all resolve to the
loaded addresses, and the same module built without `-pie` still runs.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:54 -04:00
Frédéric Desbiens 3e5a16c95b Allowed tx_timer_change to be called from tx_application_define (#730)
Fixes #224

`_txe_timer_change` returned `TX_CALLER_ERROR` for any call at or above
`TX_INITIALIZE_IN_PROGRESS`, making `tx_timer_change` the only timer service
that could not be called from `tx_application_define` -- while still being
allowed from an ISR.

The check has no technical basis: `_tx_timer_change` only writes the expiration
fields of a timer that is not on an active list, with interrupts disabled.
Removed it from both the `common` and `common_smp` copies of
`txe_timer_change.c`, with the now-unused includes and the `TX_CALLER_ERROR`
line in the header comment. Relaxing an error check is backward compatible.

`testcontrol.c` in both suites now calls `tx_timer_change` at initialization, so
the timer simple test covers it: `ERROR #30` on `dev`, green with the fix.
116/116 SMP, 103/103 non-SMP.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:50 -04:00
Frédéric Desbiens 9b2979e6b0 Replaced the GNU-only dsb/isb 0xF operands with the UAL sy form (#729)
Fixes #551

`_tx_thread_system_return_inline()` in the Cortex-M `tx_port.h` headers spells
its barriers `dsb 0xF` and `isb 0xF`. A bare hexadecimal operand is a GNU
assembler extension, and IAR rejects it with `operand syntax error`, so the
header cannot be included at all. The block is guarded for GCC, armclang and IAR
together, so every IAR user of an affected port hits it -- four independent
reports on Cortex-M33 and M7 with EWARM 9.50 and 9.70.

Both operands become `sy`, the Arm UAL name for exactly what `0xF` encodes. The
generated instruction is unchanged. Applied to the two `ports_arch` masters and
all 32 copies under `ports`, covering M0, M23, M3, M33, M4, M52, M55, M7 and M85
across ac5, ac6, gnu, iar and keil, plus the `scripts/check_ports.sh` probes that
matched the old spelling.

`check_ports.sh` passes, the copy scripts still reproduce every generated port
byte for byte, and `arm-none-eabi-gcc -O2` compiles a caller for every patched
header, emitting `dsb sy` and `isb sy`. Three headers that need toolchain
intrinsics GCC does not ship fail identically on `dev`.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:45 -04:00
Frédéric Desbiens d15f28ab9f Masked the SMP remap core maps to silence a false -O2 array bounds error (#728)
Fixes #469

`_tx_thread_smp_remap_solution_find` indexes `_tx_thread_smp_schedule_list` with
the lowest set bit of the core maps it is given. Every caller already masks
those maps with `TX_THREAD_SMP_CORE_MASK`, but the compiler cannot see it, so
when the function is inlined into `_tx_thread_system_suspend` at `-O2` GCC
assumes a bit number as high as 31 and reports an out-of-bounds subscript.
`-Werror` turns that into a build failure.

Masked the three incoming maps at the top of the function, in both the inline
version in `common_smp/inc/tx_thread.h` and the twin in
`tx_thread_smp_utilities.c`. The masks are semantic no-ops, so scheduling is
unchanged, but the range is now visible to the optimizer. The first core queue
entry is also initialized, because the narrowed range lets GCC consider an empty
queue and warn about that instead.

Reproduced on the Cortex-A9 SMP port with arm-none-eabi-gcc 13.2.1, and clean
afterwards across -O2, -O3 and -Os and 2, 4 and 8 core configurations. SMP suite
116/116.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:40 -04:00
Frédéric Desbiens 1d4a4aeec8 Guarded _tx_thread_stack_analyze against inverted stack pointers (#727)
Fixes #460

`TX_ULONG_POINTER_DIF` casts the pointer difference to `ULONG`, so when
`tx_thread_stack_highest_ptr` sits below `tx_thread_stack_start` the midpoint
wraps to a huge value, the probe lands outside the stack, and the search never
converges: the caller hangs or faults.

`_tx_thread_stack_analyze` now requires the highest pointer to be strictly above
the start of the stack, and bounds the final scan by it. #464 covered the
`TX_THREAD_STACK_CHECK` path; this covers direct callers too, as
@billlamiework suggested on the issue.

Inconsistent pointers still mean a real overflow or a corrupted control block,
which remains the application's problem. What changes is that ThreadX reports it
through the stack error handler instead of hanging.

New cases for an inverted and an equal pointer pair crash the suite without the
fix and pass with it.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:35 -04:00
Frédéric Desbiens 5d235a534c Fixed the garbage _tx_initialize_unused_memory in the GNU Cortex-A ports (#726)
Fixes #435

`LDR x1, =__top_of_ram` already loads the top of RAM, so the `LDR x1, [x1]`
that followed read whatever sat at that address and left
`_tx_initialize_unused_memory` holding garbage.

Dropped that instruction from the 13 non-SMP GNU Cortex-A ports, the ARMv8-A
source they are generated from, and the two Cortex-A35 module examples. The SMP
GNU ports already had the correct form, and the Arm Compiler ports are
unaffected -- their symbol really does need the dereference.

`scripts/check_ports.sh` passes and the `gnu` CI job build-verifies the AArch64
ports. Not run on hardware.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-15 16:24:09 -04:00
Frédéric Desbiens cd6a2d9034 Put the module manager test's thread on the kernel's created list, so the stand-in kernel matches the one the manager reads (#739)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
The host test built a TX_THREAD, gave it an ID and a module instance, and
left it off the kernel's created list. A created thread lives on that list,
so the thread the test created was not one the kernel would recognise.

The created list head and count are now defined for each object type, the
thread the test creates goes onto the thread list the way thread create puts
it there, and a successful delete takes it off again the way thread delete
does. Only the thread list is populated; the others stay empty because
nothing here creates an object of those types.

No expectation changed.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-14 17:09:00 -04:00
Frédéric Desbiens 83a3e62c28 Added a Module Manager host test harness and released a module thread's kernel stack only when it has one (#738)
* Released a module thread's kernel stack only when it has one, so deleting a thread without a manager-allocated stack no longer asks for that memory back

The thread delete dispatcher decided whether to release a kernel stack from
the calling module's property flags. A user-mode module can be given the
address of a thread that carries no kernel stack the manager allocated, and
deleting it passed that thread's null stack pointer to
_txm_module_manager_object_deallocate().

The pointer is now tested instead of the module's properties. Both thread
create paths clear the whole control block before filling it in, so a thread
with no manager-allocated kernel stack holds TX_NULL in the field, which
makes the test exact rather than an inference from how the module was
loaded.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Added a Module Manager host test harness and covered the user-mode thread kernel stack lifetime

The Module Manager is not in the ThreadX library the test tree links, and its
control blocks come from a module port rather than a base port, so nothing in
the suite could reach it. The thread_transition directory already describes
this technique as the one "the module manager tests in this tree use", but no
such tests existed.

This adds the directory that comment refers to: a port shim that replaces the
three interrupt primitives with host equivalents that count, and a CMake
target that compiles the manager sources under test directly against one
module port's headers.

The dispatch layer is one header of static functions and an unoptimised build
emits all of them, so a test that includes it to reach one dispatcher would
pull in references to every service the manager can dispatch. The header
guards each dispatcher with its own TXM_<SERVICE>_CALL_NOT_USED macro, so the
build reads that list out of the header and defines every guard except the
ones the test needs. Reading it rather than writing it down means a service
added later is excluded without anyone having to remember.

The first test asserts the invariant a user-mode module thread has to hold:
one logical thread costs the object pool two allocations, the control block
the module asks for and the kernel stack the manager takes on its behalf, and
deleting the thread must return the pool's available bytes and the module's
allocation-list count to exactly what they were. It asserts an equality
rather than a bound, because a bound would pass while one of the two was left
behind on every cycle, and then runs enough cycles that a leak of one stack
per cycle exhausts the pool several times over.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-14 16:08:48 -04:00
Frédéric Desbiens dde43b8ab2 Replaced the stale system stack switch pseudo-code in the ARMv7-A ports with comments that describe what the code actually does (#735)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
The context save, vectored context save and system return routines in the
ARMv7-A ports carried pseudo-code comments claiming that they saved the
thread stack pointer and then switched to _tx_thread_system_stack_ptr.
Neither of those things happens, and none of these ports references that
variable outside an unused IMPORT in their example builds.

On ARMv7-A each processor mode has its own banked stack pointer. The IRQ
handler branches to _tx_thread_context_save while still in IRQ mode, so the
core's banked IRQ stack already serves as the system stack, and the thread
stack pointer is stored in the control block by _tx_thread_context_restore,
and only when the interrupt results in preemption. The scheduler runs on the
banked SVC mode stack that the startup code sets up. There is nothing for a
software stack switch to do.

The comments were therefore misleading rather than merely redundant, and had
led at least one user to try to restore the code they described. They are now
replaced by a description of the actual mechanism.

The AArch64 SMP ports keep their comments unchanged, because ARMv8-A does not
bank a stack pointer per processor mode and those ports do reload
_tx_thread_system_stack_ptr[core] explicitly.

This is a comment-only change. Every changed line is a comment, and all
twenty-five GNU variants still assemble cleanly for their target core.

The fourteen files under ports/cortex_a{5,7,8,9,12,15,17} were regenerated
from ports_arch/ARMv7-A/threadx/common/src/tx_thread_system_return.S with
ports_arch/ARMv7-A/update.sh. The ARMv7-A SMP ports have no generator, so
those files were edited directly.

Fixes #734

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-11 13:27:32 -04:00
Frédéric Desbiens c0aa4dbe29 Initialized the suspend status before the non-interruptable suspend in tx_thread_sleep, so a sleep no longer returns a stale error left over from an earlier timed-out suspension (#725)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
When TX_NOT_INTERRUPTABLE is defined, _tx_thread_sleep did not set
tx_thread_suspend_status to TX_SUCCESS before calling
_tx_thread_system_ni_suspend. The interruptable path always did.

Nothing else writes that field on behalf of a sleeping thread:
_tx_thread_timeout resumes a TX_SLEEP thread directly and there is no
suspend cleanup routine for sleep. The value therefore survived from
whatever suspension the thread performed last, and tx_thread_sleep
returned it. A tx_semaphore_get that timed out with TX_NO_INSTANCE
would be followed by a tx_thread_sleep that also returned
TX_NO_INSTANCE after sleeping correctly.

The same change is applied to the SMP copy, which is identical.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-10 12:06:54 -04:00
Frédéric Desbiens 70741198e9 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>
2026-09-10 12:04:40 -04:00
Frédéric Desbiens 9ee198a2d6 Made the stack analyze binary search require several consecutive fill words, so unwritten holes in a used stack no longer cause the highest stack pointer to be under-reported (#720)
The binary search in _tx_thread_stack_analyze() accepted a probe location as
unused as soon as a single word still held TX_STACK_FILL. A word inside an
otherwise used region that simply was never written - the padding of a
partially initialized local array, for example - therefore made the search
move away from the real boundary and report far less stack usage than the
thread had actually consumed, which in turn kept the stack guard from firing.

The probe now walks down from the candidate location and requires
TX_THREAD_STACK_ANALYZE_FILL_WORDS consecutive fill words before it treats
the location as unused, stopping early at the lowest location already known
to hold the fill pattern. The new macro defaults to eight words and can be
overridden in tx_port.h; setting it to one restores the previous behavior.
Holes shorter than the configured run no longer mislead the search, and the
result is unchanged for stacks that contain no such holes.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-10 11:02:42 -04:00
Frédéric Desbiens d56f96f37f Refreshed the reason gcc_check leaves RISC-V out (#722)
cortex_m / Cortex M0 build (push) Canceled after 0s
cortex_m / Cortex M3 build (push) Canceled after 0s
cortex_m / Cortex M4 build (push) Canceled after 0s
cortex_m / Cortex M7 build (push) Canceled after 0s
gcc_check / gnu (push) Canceled after 0s
r52_fvp / r52 (push) Canceled after 0s
regression_test / tx (push) Canceled after 0s
regression_test / smp (push) Canceled after 0s
regression_test / freertos (push) Canceled after 0s
regression_test / riscv (push) Canceled after 0s
regression_test / deploy (push) Canceled after 0s
regression_template / run_tests (push) Canceled after 0s
regression_template / deploy_code_coverage (push) Canceled after 0s
The header explained the exclusion by saying RISC-V "is not regressing" and
that adding it would widen the toolchain download. The second half is still
true; the first read as though nothing in CI exercised the family at all,
which stopped being the case with #717.

RISC-V is now the best-covered of the four excluded families rather than the
least: regression_test.yml builds both ports and runs 955 tests on them under
QEMU -- 475 on RV32 and 480 on RV64, across five build configurations each --
which is more than a compile-and-link check could establish. That is a
stronger argument for leaving it out of this workflow than the original, so
the sentence now makes it.

The download figure is kept and quantified: the two bare-metal toolchains this
workflow would have to fetch are about 500 MB apiece.

Comment only; no behaviour change. scripts/check_gcc.sh names the same four
families but states the exclusion without giving a reason for it, so it needs
no matching edit.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-10 09:31:58 -04:00
Frédéric Desbiens 6c84e61d19 Gave install_riscv.sh the network hardening install.sh already had, and shared it between them (#721)
install.sh grew a retry loop, per-command timeouts and a deliberately
non-gating apt-get update after this runner pool cost several whole runs: a
mirror going silent for two hours, and a Hash Sum mismatch from a third-party
repository the project does not even use turning builds red. Those lessons were
local to that one file.

install_riscv.sh had none of them, and #717 puts it on every pull request's
critical path. Under set -e its bare apt-get update was a single point of
failure for the whole suite -- the precise case install.sh downgrades to a
warning on purpose -- and its two wget calls, each fetching about 500 MB, had
no retry and no timeout.

Rather than copy the helpers and let them drift again, they move to
tx_ci_common.sh and both scripts source it, following the arrangement
scripts/tx_windows_common.ps1 already uses on the Windows side. install.sh
keeps its behaviour exactly: same APT_OPTIONS, same 120-second TIMEOUT, same
three-attempt retry, and the comments explaining each of them travel with the
code they explain.

Two things are new:

  - TIMEOUT_LONG, 180 seconds, for a single large download. Sized against the
    39 seconds each tarball took on 10 Sep 2026 and deliberately not larger:
    the install step is capped at ten minutes, and a per-attempt timeout able
    to swallow that cap would leave the retry loop no turn to take, which is
    the failure mode the apt comment already records.

  - fetch(), which verifies a SHA-256 before anything is unpacked. Both digests
    were taken from the releases API and then checked against the bytes the CDN
    actually serves. This is not an independent trust root -- expected value and
    file come from the same host -- but it pins the bytes, so a deleted and
    re-pushed tag or a replaced asset stops the build instead of being picked up
    silently.

Also verifies qemu-system-riscv32 alongside riscv64. run.sh selects one per
architecture, so both are worth failing on here rather than at the first test.

Verified locally: retry returns 0 on success and 1 after three attempts;
fetch accepts a correct digest and, on a wrong one, fails and removes the
partial file; the source line resolves from the repository root, from an
absolute path and through a symlink; and both recorded digests match the
bytes served for the pinned tag.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-10 09:28:44 -04:00
Akif EjazandFrédéric Desbiens 18abe103af Enabled the RISC-V regression suite in CI for QEMU targets (#717)
* Enable the RISC-V regression suite in CI

The riscv: job in regression_test.yml has been present but commented out
since the RISC-V CI infrastructure landed (scripts/install_riscv.sh,
scripts/build_tx_riscv.sh, scripts/test_tx_riscv.sh, and the CMake tree
under test/tx/cmake/riscv/). It has been gated on a note that read
're-enable when RISC-V CI is ready', with no other blocker recorded.

The suite is ready. A local run of scripts/build_tx_riscv.sh followed by
scripts/test_tx_riscv.sh against upstream/dev builds every RISC-V target
and passes every registered test across all ten build configurations:

  RV32 (five configs): 5 * 95 = 475 tests, 0 failures, 0 timeouts.
  RV64 (five configs): 5 * 96 = 480 tests, 0 failures, 0 timeouts.

The extra RV64 test is threadx_riscv_new_thread_fpu_state_test, which
lives beside test/tx/cmake/riscv/regression/CMakeLists.txt and is added
only when THREADX_ARCH is risc-v64 -- the RV32 stack builder still
leaves the mstatus slot of the frame unwritten. Every test runs on
qemu-system-riscv32 / qemu-system-riscv64 with -machine virt, and each
configuration completes in roughly thirteen to fifteen seconds.

The job is wired the same way the ThreadX, SMP and FreeRTOS suites are:
it calls .github/workflows/regression_template.yml with the RISC-V
install/build/test scripts and the RISC-V cmake_path, sets
result_affix: RISC-V so its check name and artifacts are named, and
carries skip_deploy: true because coverage publishing stays on the
Linux suites for now. skip_coverage: true is kept because the CMake
configurations under test/tx/cmake/riscv match the Linux suite's minus
the coverage instrumentation, so gcovr has nothing to read -- the same
reason the FreeRTOS lane sets it. The regression_test.yml triggers were
not touched, so the RISC-V suite now runs on push and pull_request to
master and dev alongside the other three suites.

* Said why the RISC-V suite collects no coverage, instead of implying a missing build configuration

The comment added with the job read "No coverage build configuration for the
RISC-V suite yet". Both halves of what followed are true -- the configurations
under test/tx/cmake/riscv are the Linux suite's minus the coverage one, and
gcovr has nothing to read -- but "yet" points the next reader at a fix that
would not work.

Coverage here is not one missing default_build_coverage entry. These tests are
bare-metal images run under QEMU with -bios none, and
test/tx/cmake/riscv/bsp/syscalls.c has _write to the UART, _exit through the
sifive_test device, and stubs for _close, _fstat, _isatty, _lseek, _read and
_sbrk -- but no _open. gcov emits a .gcda by opening a path, so instrumenting
these builds produces nothing regardless of how they are configured. Nor is
the template's TX_COVERAGE=OFF what holds coverage off: test/tx/cmake/riscv is
its own top-level project and never declares that option, so the value is
inert there.

Getting a figure out of this suite needs a transport off the target -- gcov's
dump routines over the UART the BSP already drives, or semihosting. Worth
stating plainly in a project that asks for 100% coverage, rather than leaving
a reader to discover it after adding a configuration that cannot help.

Comment only; the job's inputs are unchanged.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
2026-09-10 09:08:11 -04:00
Frédéric Desbiens 42f1d2bcf0 Pointed the SMP coverage comment at the pull request that made its measurements (#719)
The paragraph explaining why the SMP floor sits at 99 rather than 100 ended
with a bare reference that resolves to nothing a reader of this repository can
open. It named a source outside the tree in place of one inside it.

The real source is #677: it wrote the four tests that closed 53 of the 64
lines, took the measurements the paragraph quotes -- the 180,003 windows with
zero handovers, and the three remaining lines in tx_thread_smp_utilities.c --
and raised the floor from 98 to 99. Citing it gives the next reader somewhere
to go.

This is the same correction #718 made across five other files; this line was
missed because it sits in regression_test.yml rather than the template.

Comment only; no behaviour change.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-10 09:06:13 -04:00
Frédéric Desbiens 39277cf026 Stated the compiler and coverage requirements directly, and said who the pinned toolchain default serves (#718)
* Stated the compiler and coverage requirements instead of citing a file no contributor can open

Seven comments across five files cited a maintainer-local document as the
source for two project requirements: that GCC 14 on Linux is the default
compiler, and that the coverage target is 100%. That document is not part of
this repository and is not published anywhere, so the citation gave a reader
nothing to follow -- it named a source they cannot open, in place of simply
stating the requirement.

Both requirements are real and both stay. Only the pointer goes: each comment
now states the requirement on its own terms, which is what the surrounding
prose was already doing everywhere else.

  cmake/cortex_r52.cmake                     the pinned reference toolchain
  scripts/check_gcc.sh                       why the script exists
  .github/workflows/gcc_check.yml            why the workflow exists, and the
                                             GCC_VERSION pin
  .github/workflows/r52_fvp.yml              the GCC_VERSION pin
  .github/workflows/regression_template.yml  the coverage floor, twice

Comments only; no behaviour changes. Two paragraphs are rewrapped where the
shorter text left a ragged line. Verified that scripts/check_gcc.sh still
parses and prints its help from the header range it slices, and that
cmake/cortex_r52.cmake still configures the Cortex-R52 build.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Said who the pinned toolchain default in the CMake toolchain files serves

Both cmake/cortex_r52.cmake and cmake/cortex_m52.cmake default
ARM_TOOLCHAIN_PATH to a toolchains directory under the user's home, guarded by
an EXISTS check. Nothing said whether CI relies on that, and the natural
reading is that it does.

It does not. The three workflows that install a toolchain unpack it into the
workspace and cache it there, and r52_fvp.yml puts that directory on PATH
before configuring; scripts/check_gcc.sh passes -DARM_TOOLCHAIN_PATH at each of
its three CMake call sites. On a runner the guarded directory is absent, the
EXISTS check falls through, and the compiler comes from PATH. The default only
ever fires on a developer machine, where it is what makes a no-flag build work.

Both comments now say that, so the default is not mistaken for a CI dependency
and not removed as dead code. cortex_r52.cmake carries the explanation and
cortex_m52.cmake refers to it, matching the cross-reference already there.

The r52 comment also claimed absolute paths mean "the build does not depend on
PATH ordering", which is only true where the pinned directory exists -- in CI
the build depends on PATH and nothing else. Qualified accordingly.

Comments only; no behaviour changes. Verified that both toolchain files still
configure, and that the fall-through is real: with HOME pointed at a directory
holding no toolchains, cortex_r52.cmake configures against the arm-none-eabi-gcc
found on PATH.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-10 07:55:11 -04:00
Frédéric Desbiens 5e9d046b12 Compiled the module manager C sources with GCC, as clang already did (#716)
#689 added the module manager stage to check_clang.sh alone. The GCC half was
never written, so the module manager C stayed unbuilt by the project's declared
default compiler: 28 files of portable module manager under common_modules,
plus the three to nine per-port files under
ports_module/<core>/gnu/module_manager/src, across nine Arm module ports.

The stage is deliberately check_clang.sh's, port for port and header for
header, because a port covered by one check and not the other implies a parity
the checks list does not have. The same two details the ports dictate carry
over: an SMP port's control blocks come from common_smp rather than common, and
the TrustZone ports need -mcmse for their cmse_nonsecure_entry functions to be
honoured rather than ignored. The clang-only waiver does not: GCC implements
the optimize attribute that tx_thread_secure_stack.c carries, so nothing needs
suppressing for that file.

One divergence is forced by the toolchain. txm_module_manager_absolute_load.c
carries a #pragma message steering callers to the extended entry point, and the
C stages treat any compiler output as a failure. check_clang.sh silences it with
-Wno-#pragma-messages; GCC has no equivalent, and neither -Wno-pragmas nor any
other -W option suppresses the note -- verified with 14.3.rel1. The note is
therefore filtered out of the stage's output instead, together with the source
quote GCC prints beneath it. The filter stops at the next line that begins a
diagnostic of its own, so an error immediately following a waived note is still
reported; that case is what the injected-defect run below checks.

The workflow needed no trigger change: #689 added common_modules/** to both
path lists in advance, for the stage that had yet to arrive. Its header comment
is brought in line with what the workflow now runs.

Verified with the toolchain CI pins, arm-gnu-toolchain 14.3.rel1, both triples.
The full script passes and every port compiles every file, matching the counts
check_clang.sh reports for the same nine ports:

  cortex_a35 31/31   cortex_a35_smp 31/31   cortex_a7 34/34
  cortex_m0+ 33/33   cortex_m23 37/37       cortex_m3 33/33
  cortex_m33 37/37   cortex_m4 33/33        cortex_m7 33/33

Verified that the stage fails as intended by injecting defects into a throwaway
worktree: one in common_modules on the line straight after the waived pragma
note, reported under all nine ports, and one in a single port's own source,
reported only under that port.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
2026-09-10 07:25:37 -04:00
Frédéric Desbiens 7f6e496233 Added the missing VFP enable field to the Cortex-R5/AC5 thread control block, which VFP builds were writing over the FileX pointer (#715)
The Cortex-R5/AC5 port defines TX_THREAD_EXTENSION_2 as empty while its
assembly reads and writes the per-thread VFP enable flag at [thread, #144].
With TX_ENABLE_VFP_SUPPORT that offset lands on tx_thread_filex_ptr, so
tx_thread_vfp_enable corrupted the FileX pointer and lazy save/restore
tested an unrelated value.

Defined TX_THREAD_EXTENSION_2 as ULONG tx_thread_vfp_enable, matching the
Armv7-A ports and the Cortex-R4/R5 GNU and AC6 ports fixed earlier, and
added tx_port_offset_check.c so a future layout change breaks the build
instead of silently retargeting the accesses. The offset was measured at
144 for this port and asserted.

Fixes #382

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-09 17:17:37 -04:00
tardigradeandFrédéric Desbiens a8c84593dd Added ARMv7-A SMP Linux build scripts(A5,A7,A9) (#674)
* ports: add Linux build scripts for Cortex-A5 SMP

* ports: fix Cortex-A5 SMP sample linking on Linux

* ports: add Linux build scripts for Cortex-A7 and A9 SMP

* ports: fix Cortex-A7 SMP assembly source path

* wip: add ATFE selection to Armv7 SMP scripts

* ports: complete Armv7 SMP Linux toolchain support

* ports: add local startup for Armv7 SMP samples

* ports: add license headers to Armv7 SMP scripts

* Dropped the preprocessing flag the file extension now carries

Two lines named assembly sources that #672 renamed. That change moved
twenty-nine files under gnu trees from .s to .S, because GAS runs the C
preprocessor on .S and not on .s: in a .s file every # line is a comment, so a
#define is never substituted and an #if/#else pair emits both arms. Four files
were silently doing the wrong thing as a result, including
ports_smp/cortex_a7_smp/gnu/src/tx_thread_smp_unprotect.s, which ignored all
four of its own feature macros.

These scripts had worked around the same defect with -x assembler-with-cpp
rather than hitting it, which was correct when they were written. With the
rename the flag is redundant and the lowercase names no longer resolve, so
cortex_a5_smp/build_threadx.sh failed with "cc1: fatal error:
tx_initialize_low_level.s: No such file or directory". The other seven
references to those two files across these three scripts already named them
with a capital S.

Verified with the pinned Arm GNU 14.3.rel1 rather than the 13.2 that a distro
package supplies: build_threadx.sh and build_threadx_sample.sh both succeed for
a5, a7 and a9, all three link a sample_threadx.out, and scripts/check_gcc.sh
passes end to end with its example stage reading 45 of 45, up from 42.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
2026-09-09 16:30:23 -04:00
Frédéric Desbiens 383dd311cd Removed the vector table offset register and system stack pointer setup from the Cortex-M low-level initialization (#714)
_tx_initialize_low_level wrote VTOR and derived _tx_thread_system_stack_ptr from
the reset vector on every Cortex-M port. Programming VTOR is the job of the
low-level startup code that runs before the kernel is entered, and doing it again
inside ThreadX silently overrode a vector table that the application, a
bootloader or a firmware update had already installed. It also forced every
application to export a vector table symbol that the kernel itself never needed.

_tx_thread_system_stack_ptr is never read by any Cortex-M port, so the value
copied out of the reset vector served no purpose.

Both blocks are removed from all Cortex-M ports, together with the now-unused
symbol declarations. The GreenHills files keep the NVIC base address load that
the removed block used to leave in r0 for the SysTick setup that follows.

Applications that relied on ThreadX programming VTOR must now set it in their
startup code. The bundled examples link their vector table at address zero and
run on the reset default.

Fixes #370

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-09 16:28:59 -04:00
850a172bac Added lazy FPU stacking and QEMU functional tests for RV64 GNU port (#549)
* add lazy FPU stacking to context save/restore

Save mstatus/sstatus to stack slot 29 and skip floating-point register
save/restore when FS is Off (bits 14:13). This avoids unnecessary FP
context work for threads that do not use the FPU.

- context_save: check FS in nested and first-level interrupt paths
- context_restore: gate FP restore on nested, no-preempt, and preempt paths
- use sstatus when TX_RISCV_SMODE is defined, otherwise mstatus

* add QEMU virt CMake build and automated test runner
Wire the QEMU virt demo into the CMake build system and add a
Python/GDB functional test runner, mirroring the risc-v32/gnu port.
- Add qemu_virt/CMakeLists.txt to build kernel.elf and register the
  check-functional-riscv64 target (requires Python3; skipped if absent)
- Link kernel.elf with --whole-archive so all ThreadX symbols resolve
- Pin _start at 0x80000000 via .text.boot in entry.s and
  KEEP(*(.text.boot)) in link.lds
- Extend demo_threadx.c with fpu_test_val and shorten thread_0 sleep
  for GDB-driven FPU, timer, and preemption checks
- Add test/azrtos_test_tx_gnu_riscv64_qemu.py; verified passing on
  QEMU virt (FPU, timer interrupt, preemption)

* Clean up RV64 PR scope and remove QEMU test integration leftovers

Revert accidental RV64 qemu_virt test/CMake integration changes and keep this branch
focused on lazy FPU context handling only. Also remove unintended TX_RISCV_SMODE-based
mstatus/sstatus save path and align comments/logic to mstatus-only behavior.

* Initialize mstatus.FS in RV64 stack build so new threads start with clean FP state

Slot 29 was left uninitialized while context restore reads it as an FP-live
hint; garbage FS bits could make a new thread inherit the previous thread's
floating-point registers.

* Add RV64 regression test for the FP state of a newly created thread

The test dirties every floating point register, then creates a thread and
checks that the stack builder wrote the mstatus slot and that the new
thread starts with all floating point registers zeroed. It is registered
for RV64 only, since the RV32 stack builder still leaves the slot unwritten.

* Completed the RISC-V64 lazy FPU so the restore side matches the save side

The lazy FPU save in this branch skips the floating-point stores when
mstatus.FS is Off, and records the mstatus it judged that on in frame slot
29. Merged onto current dev, only the save side had that treatment: both
restore paths and the scheduler's interrupt-frame path still reloaded the
FP registers unconditionally, from slots the save had deliberately left
alone. A thread that never touched the FP unit would have had whatever the
frame happened to contain loaded into its registers, and FS driven to
Dirty on the way out.

The guard is added at the three places that consume an interrupt frame:
both paths in _tx_thread_context_restore, and _tx_thread_schedule_loop.
Each reads slot 29 and skips the FP block when FS was Off, which is the
same shape the risc-v32 port already uses.

The solicited path is deliberately left alone. _tx_thread_system_return
saves the callee-saved FP registers unconditionally, so restoring them
unconditionally is consistent; making that pair lazy as well is a separate
change, and risc-v32 is the model for it.

Verified with QEMU on all five configurations:

  risc-v64   96 of 96 passing, five configurations, nothing unlinkable
  risc-v32   95 of 95 passing, five configurations, unchanged
  functional check-functional-riscv64 passes every check

The ninety-sixth test is the one this branch adds. It is load bearing:
seeding stack build with FS = Off instead of Initial makes it fail, and
restoring the seed makes it pass, so it guards the behaviour the rest of
this branch is about rather than passing regardless.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: r <r@r>
Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
2026-09-09 16:25:28 -04:00
Frédéric Desbiensandr 16297d7a2a Added a workflow that runs the Cortex-R52 images on the Armv8-R AEM FVP (#688)
* Added a workflow that runs the Cortex-R52 images on the Armv8-R AEM FVP

Nothing in CI executed a single instruction of any ThreadX port. gcc_check
compiles and links -- its own header says it "executes nothing" -- and its
CMake stage covers one R52 configuration, the base FVP example. A change that
assembles cleanly, links cleanly and then hangs on the first context switch
passed every required check.

This workflow builds both supported configurations and runs them on the
Armv8-R AEM FVP, asserting each image's self-reported result. The feature
configuration matters as much as the default one: VFP with the hard float ABI,
FIQ, IRQ nesting and FIQ nesting gate whole assembly blocks that the default
configuration never assembles, and it builds eight images where the default
builds five.

Arm distributes the model free of charge but behind a click-through licence
with no stable unauthenticated URL -- the developer.arm.com permalink forms
all 404 for it -- so the download location is a repository variable rather
than a literal. When it is unset the build lanes still run and still gate the
pull request; only execution is skipped, and it says so in the log and in the
job summary rather than passing quietly.

The image list is read from the generated ninja graph rather than kept in the
workflow. The images are EXCLUDE_FROM_ALL, so a bare build reports "no work to
do", and reading the graph means a target added to CMakeLists.txt cannot
escape the check by nobody remembering to list it here.

The module manager port and the S32Z280 targets are not on dev yet. Their
lanes belong in this workflow when they land, not in a second one.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

* Kept the FVP cache honest and stopped ctest passing on an empty suite

Two corrections to the new workflow.

The FVP cache key named the URL and hashed the workflow file. hashFiles()
reads files, so it cannot hash a repository variable, and pointing it at
this workflow keyed the cache on the file's own contents instead. That is
wrong in both directions: repointing FVP_AEMV8R_URL at a different model
kept serving the old one from cache, which is exactly what the comment
claimed the key prevented, and editing anything in this file forced a
needless refetch of a large archive. A small step hashes the URL and the
key uses that, so it now tracks what it names.

ctest reported success when it found nothing to run. Its default is to
pass an empty suite, and the example registers its tests inside both
if(FVP found) and if(Python3_FOUND). Either one going unmet on a runner
would have left this stage green having executed no image at all, which
is the hole the workflow exists to close. --no-tests=error makes an empty
suite a failure.

Verified with the pinned toolchain, Arm GNU 14.3.rel1: both configurations
configure, enumerate and build, and the image lists are the ones claimed.

  default: 5 images  boot_check demo_m2 demo_m3 demo_mpu demo_threadx
  feature: 8 images  the above plus demo_fiq demo_m5 demo_nesting

Also checked that the target enumeration survives the real ninja graph:
the generated graph offers twenty .elf paths, and the filter reduces them
to those eight, dropping the cmake_object_order_depends_target aliases and
the CMakeFiles copies.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: r <r@r>
2026-09-09 14:39:54 -04:00
Frédéric Desbiensandr 8be4c45cce Compiled the module manager C sources, which no check had ever built (#689)
* Compiled the module manager C sources, which no check had ever built

#672 corrected this script's assembly glob and brought the module ports into
the count, but only their assembly. Their C stayed outside every check: 27
files of portable module manager under common_modules, plus the per-port code
under ports_module/<core>/gnu/module_manager/src. By this script's own standard
-- a port simply absent from the count reads as covered -- 293 files across
nine Arm module ports were compiled by nothing, with either compiler.

Each module port ships its own tx_port.h and txm_module_port.h carrying the
control-block extensions the dispatch code needs, so a port is compiled against
its own headers rather than the base port's. Two details the ports themselves
dictate:

  An SMP port's control blocks come from common_smp. Pairing cortex_a35_smp
  with the single-core headers hid _tx_thread_smp_protect and
  _tx_thread_smp_unprotect behind implicit declarations and lost
  tx_thread_smp_core_executing from TX_THREAD, so fourteen files reported
  errors for a port that builds correctly.

  The TrustZone ports carry cmse_nonsecure_entry, which needs -mcmse to be
  honoured rather than ignored. tx_thread_secure_stack.c also carries GCC's
  optimize attribute, which clang does not implement; that divergence is
  suppressed by name, for a file GCC builds cleanly.

All nine ports compile: 293 of 293. Verified that the stage fails as intended
by injecting a defect into a throwaway worktree -- a defect in common_modules
is reported under every port, one in a port's own source only under that port.

Assisted-by: Claude Code (Opus 5)

* Ran the module manager stage against the sources that reach it

Three corrections to the new stage, all found by running it against dev
rather than against the tree it was written on.

The workflows did not trigger on common_modules. Both check_clang.yml and
check_gcc.yml list ports_module but not common_modules, and the portable
module manager under it is the larger half of what the stage compiles --
28 of the 31 to 37 files each port builds, plus two include directories.
A change there left the stage unrun, which is the same "absent from the
count reads as covered" that the stage exists to close. Both lists gain
common_modules, and they stay identical to each other as the comment in
each asks.

A deliberate deprecation notice read as a build failure. Since this PR
was opened, txm_module_manager_absolute_load.c gained a #pragma message
steering callers to the extended entry point. The stage treats any
compiler output as a failure, so that one notice failed every port: nine
failures on a tree where nothing is wrong. Pragma messages are now
waived for the stage, because a notice to callers is not a defect in the
file that carries it.

That failure also printed nothing. Both C stages report by grepping the
output for "error:", so a diagnostic that is not an error produced a bare
FAIL line with no reason under it, and the only way to learn the reason
was to reproduce the compile by hand. Both stages now fall back to
showing what the compiler actually said.

The TrustZone attribute waiver is narrowed to the one file that needs it.
tx_thread_secure_stack.c carries GCC's optimize attribute, which clang
does not implement; it is the only file among the 300-odd this stage
compiles that does. Waiving the warning for the whole port would have
swallowed a stray unknown attribute anywhere else in it.

Verified with the same toolchain CI uses, ATfE 22.1.0: the full script
passes, and every port compiles every file.

  cortex_a35 31/31   cortex_a35_smp 31/31   cortex_a7 34/34
  cortex_m0+ 33/33   cortex_m23 37/37       cortex_m3 33/33
  cortex_m33 37/37   cortex_m4 33/33        cortex_m7 33/33

The counts are each one higher than this PR first reported, because
txm_module_manager_absolute_load_extended.c has landed since. The stage
picked it up with no edit, which is what globbing the directories was for.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: r <r@r>
2026-09-09 14:39:30 -04:00
Frédéric Desbiens 6ce8d5cc76 Marked every published ThreadX include directory as SYSTEM so applications no longer get warnings from ThreadX headers (#713)
* Marked every published ThreadX include directory as SYSTEM so applications no longer get warnings from ThreadX headers

Commit 8c3c08f added the SYSTEM keyword to the target_include_directories
call in common/CMakeLists.txt, but the same call in every port, in the
SMP common directory's consumers, in the POSIX and FreeRTOS compatibility
layers, and for the generated tx_user.h directory was left unchanged.
A consumer building with a strict warning set therefore still saw
diagnostics coming from tx_port.h and from the compatibility layer
headers, which is exactly what the original change set out to avoid.

Added the SYSTEM keyword to all of those calls so CMake emits -isystem
rather than -I for every directory holding a ThreadX public header. The
ARMv7-M, ARMv8-M, ARMv7-A and ARMv8-A architecture sources under
ports_arch were updated alongside the ports they generate, keeping the
two in step.

Directories that are PRIVATE to an example or test build were left as
they are, since nothing is published from them.

Fixes #290

Assisted-by: Copilot (Opus 5) <noreply@github.com>

* Stopped apt-get update being a gate it was never meant to be

apt-get update fails if any configured repository serves a bad index,
including ones this project never reads. The GitHub runner image carries
Google's and Microsoft's apt repositories, and a Hash Sum mismatch from
Google's, their CDN caught mid-publish with the index and the Release
file eight hours apart, failed all three attempts and turned a run red
over a browser nobody was installing.

Made a failed update warn and carry on, leaving apt-get install as the
gate. Nothing is weakened by that: the install still exits on a package
it cannot find, so an unreachable archive still stops the script, one
step later and naming the package it could not get, which is a better
diagnostic than a hash mismatch in a repository nobody asked for.

Disabling third-party sources before updating would keep the update
strict, but this script also runs on a contributor's own machine, and
rewriting someone's apt configuration to suit CI would be worse than
tolerating a stale index for an archive we do not read.

Assisted-by: Copilot (Opus 5) <noreply@github.com>
2026-09-09 14:27:02 -04:00
4d90a0c21c Stopped the win32 and win64 ports from enabling performance metrics and event trace (#676)
* win32: do not always enable trace or performance metrics in tx_port.h

if these are required they can be enabled in tx_user.h

* win64: do not always enable performance metrics in tx_port.h

if required they can be enabled in tx_user.h

* Removed the disabled blocks rather than commenting them out

The win32 and win64 ports were the only two that turned performance
metrics on for the application, and win32 the only one that turned event
trace on. Leaving those to tx_user.h is right: the symbols extend the
control blocks, so a port that sets them behind the application's back
changes structures the application also sees.

The blocks were disabled with #if 0 rather than deleted. That is the form
MISRA C:2012 Directive 4.4 is about -- sections of code should not be
commented out -- and it leaves two copies of a list that now has no
reader. They are removed, and a short note in their place says where the
symbols belong and why the port does not set them.

No behaviour change beyond what this pull request already made. Checked
that the preprocessor nesting in both headers is still balanced.

Worth recording for whoever looks next: with this in, no port defines
either symbol. The linux port carries the same list commented out, which
reads at a glance like a third case but is not one.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Co-authored-by: r <r@r>
Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
2026-09-09 13:39:13 -04:00
Akif EjazandFrédéric Desbiens 40db27e843 riscv32: spec compliance and regression test fix (#691)
* riscv32: spec compliance and regression test fix

Signed-off-by: Akif Ejaz <akifejaz40@gmail.com>

* Derived the RISC-V32 frame sizes from the port contract in one place

tx_port.h published TX_RISCV_TRAP_FRAME_SIZE for the GNU BSP assembly, but
nothing in the port consumed it. Six .S files each rebuilt the same numbers
from their own #if, so the interrupt frame size was written out in seven
places and the solicited frame size in three.

That is the shape that produced the RISC-V64 fault fixed in #708, where the
port moved to a padded frame and one copy of the constant did not. The
sources now include tx_port.h and take both sizes from it, and no literal
frame size remains in the port. TX_RISCV_SOL_FRAME_SIZE joins the contract,
since the solicited frame was never published at all.

The emitted code is unchanged: 400 and 176 bytes for ILP32D, 128 for
soft-float, confirmed by disassembly before and after.

Two further corrections:

_tx_initialize_low_level carried .global immediately followed by .weak, so
the symbol stayed weak and the .global did nothing. Weak is what the port
wants, because the example and regression BSPs both provide their own
definition, so the stray .global is removed rather than the .weak. Verified
with nm that the symbol is still W.

The QEMU runner seeded fpu_verified from skip_fpu, so a soft-float run
satisfied the FPU gate whether or not the script ever reported the skip. It
now starts false and is set only when the skip marker is present, so a run
that dies before reaching that point fails instead of passing.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>

---------

Signed-off-by: Akif Ejaz <akifejaz40@gmail.com>
Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
2026-09-09 11:40:05 -04:00