Skip to content

Merged changes ahead of the v6.5.2.202603 release - #783

Open
fdesbiens wants to merge 182 commits into
masterfrom
dev
Open

fdesbiens wants to merge 182 commits into
masterfrom
dev

Conversation

@fdesbiens

Copy link
Copy Markdown
Contributor

No description provided.

francdoc and others added 30 commits June 16, 2026 10:26
The RX GCC assembly port contains an unclosed C-style comment in tx_thread_context_save.S. Because .S files are passed through the C preprocessor before assembly, the nested comment can trigger GCC warnings.

Close the affected comment block without changing assembly instructions or runtime behavior.

Signed-off-by: FranCDoc <fchiesadoc@gmail.com>
Updated the SMP execution profile aggregate getters to copy each core's total into the matching output array element. Added a focused regression test for thread, ISR, and idle total getters.

Co-authored-by: Codex <codex@openai.com>
…#554)

* Fixed Cortex-A9 SMP time source

Updated Cortex-A9 SMP timestamp reads to use the global timer low counter instead of the decrementing private timer counter. Applied the change to GNU and AC5 ports.

* Fixed remaining Arm SMP time sources

Updated Cortex-A5, Cortex-A7, and Cortex-R8 SMP timestamp hooks so execution profiling uses incrementing time sources instead of the decrementing private timer count register.

Cortex-A5 and Cortex-R8 now read the global timer count low register, matching the Cortex-A9 fix. Cortex-A7 now reads the generic timer physical count register.

---------

Co-authored-by: Codex <codex@openai.com>
Follow up on the issue-541 execution profiling work. PR #553 fixed SMP execution profile total-time aggregation, and PR #554 fixed Armv7/R SMP timestamp hooks that used decrementing private timers.

This change addresses the remaining related gap in ARMv8-A SMP ports: their timestamp hooks returned zero, which left execution profiling and trace timestamps without a progressing time source. Replace those stubs with the architectural generic physical counter, CNTPCT_EL0, across the ARMv8-A SMP GNU, AC6, IAR, and GHS variants.

The local ARMv8 AArch64 system timer example already uses CNTPCT_EL0 for physical count reads, so this keeps the SMP timestamp hook aligned with the existing port examples while preserving the 32-bit ULONG return contract.

Co-authored-by: Codex <codex@openai.com>
* Add SysTick counter reset in Cortex-M ports

The SysTick counter value (SYST_CVR @0xE000E018) is indeterminate after
reset. Without clearing it prior to enabling the counter, the first tick
interval becomes unpredictable.

* Fixed missing comment and added generated-by header in Cortex-M0/AC6 port

Added the missing inline comment on the LDR r1, =0 instruction
(Build value for SysTick reset) and the required AI-generated
attribution comment under the copyright header.

---------

Co-authored-by: Frédéric Desbiens <frederic.desbiens@eclipse-foundation.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use explicit immediate syntax when clearing BASEPRI in GNU and AC6 Cortex-M scheduler assembly.

Co-authored-by: Codex <codex@openai.com>
Use explicit immediate syntax when testing the Thumb bit in AC6 Cortex-R4 stack build assembly.

Co-authored-by: Codex <codex@openai.com>
Added a compile-time #pragma message warning to the module library
source so that any module that includes or compiles this file receives
an explicit deprecation notice at build time.

Updated the internal documentation block in the manager-side
implementation to explain that this function must not be called directly
and that calling it on a live object causes a use-after-free.

The Module Manager dispatch layer already releases pool memory
automatically after a successful tx_*_delete() call. Module authors
should remove any explicit call to txm_module_object_deallocate().

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Added #pragma message compile-time warning to the module library
source and updated the DESCRIPTION blocks in both the library and
manager implementations.

Reason: this wrapper passes UINT_MAX as the name-buffer length to
the underlying extended search. The comparison loop can therefore
read past the end of a short name buffer, which is undefined
behaviour. Callers should use txm_module_object_pointer_get_extended()
and supply the actual buffer length.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Fixed missing VFP thread extension on Cortex-R4 and R5 ports

Building cortex_r4/gnu, cortex_r5/gnu or cortex_r5/ac6 with
TX_ENABLE_VFP_SUPPORT corrupts memory. The assembly in
tx_thread_schedule, tx_thread_system_return and tx_thread_context_restore
reads and writes a per-thread VFP enable flag as [thread, #144], but every
TX_THREAD_EXTENSION in these ports' tx_port.h was empty, so no such member
existed.

Offset 144 in the resulting TX_THREAD is tx_thread_filex_ptr, so the
damage runs both ways:

  - tx_thread_vfp_enable() stores 1 into tx_thread_filex_ptr, after which
    FileX dereferences 0x1;
  - conversely, once FileX sets that pointer the context switch reads it as
    "floating point enabled" and starts pushing about 132 bytes of
    floating-point context onto a thread stack never sized for it, then
    restores unrelated data into the D registers.

sizeof(TX_THREAD) is 180 on these ports, so 144 is a live member rather
than padding past the end of the structure.

What makes this dangerous is that it is silent. The defect was reproduced
at runtime by reverting the Cortex-R52 port -- which inherited the same
assembly and the same hard-coded 144 from the R5 port -- to this state and
running its floating-point demo on the Armv8-R AEM FVP. Every
floating-point value was still preserved exactly and no corruption was
reported across interrupts, because a non-zero filex_ptr reads as "enabled"
and the context switch dutifully saves and restores. The only visible
damage was tx_thread_filex_ptr reading 0x00000001. In other words the
feature you enabled appears to work, and what breaks is an unrelated
pointer that nothing notices until FileX is introduced. With the field
present the same demo reports the sentinel intact.

This appears to be drift rather than a deliberate limitation. All fourteen
Armv7-A port and toolchain combinations define the field and contain the
VFP code paths. The R-profile family is inconsistent in both directions:
these three have the code paths without the field, while cortex_r4/ac5,
cortex_r4/ac6, cortex_r4/iar, cortex_r5/ghs, cortex_r5/iar, cortex_r7/ghs
and cortex_r8_smp/ac5 define the field but contain no VFP code paths.

The fix follows the Armv7-A ports exactly: TX_THREAD_EXTENSION_2 carries
tx_thread_vfp_enable, and tx_thread_vfp_enable/disable are declared. The
field is defined unconditionally rather than under TX_ENABLE_VFP_SUPPORT
because the offset is hard-coded in assembly, so a conditional member
would shift every following field and be correct in only one
configuration. No assembly changes are needed: 144 is already correct once
the member exists.

Verified with GCC 14.3: on all three ports tx_thread_vfp_enable now lands
at exactly offset 144, matching the assembly, with tx_thread_filex_ptr
moved to 148. The library builds cleanly for cortex_r4/gnu and
cortex_r5/gnu both with and without TX_ENABLE_VFP_SUPPORT, and the VFP
save and restore instructions appear in the port assembly only when it is
requested. cortex_r5/ac6 is verified by offset and inspection only, as
that toolchain was not available here. Runtime verification was performed
on Armv8-R as described above rather than on R4/R5 silicon; upstream QEMU's
xlnx-zcu102 machine holds its Cortex-R5F cores in reset, so no R5 target
was available.

ABI note for release documentation: adding the member moves
tx_thread_filex_ptr and everything after it by four bytes and grows
TX_THREAD from 180 to 184 bytes. This is the layout the Armv7-A ports
already have, so the change aligns R-profile with A-profile rather than
introducing a new one, but kernel awareness and TraceX tooling that
hard-codes offsets will need rebuilding.

Follow-up worth considering separately: nothing in the toolchain ties the
literal 144 in the assembly to the C structure, which is what allowed this
to go unnoticed. A compile-time assertion per port turns any recurrence
into a build failure -- when the experiment above reverted the field, that
assertion failed the build before a corrupting binary could be produced.
ports/cortex_r52/gnu/src/tx_port_offset_check.c is a working reference.


* Added compile-time offset checks to the Cortex-R4 and R5 ports

Nothing in the toolchain connects the structure offsets hard-coded in the
port assembly to the C definition of TX_THREAD, which is what allowed the
missing VFP thread extension to go unnoticed. These files assert the
offsets that the context-switch path depends on, so a recurrence becomes a
build failure instead of memory corruption discovered later at runtime.

Three offsets are asserted: the VFP enable flag at 144, the thread stack
pointer at 8 and the run counter at 4. The stack pointer and run counter
precede every extension macro in TX_THREAD, so they are stable by
construction. Offset 144 was checked against the build options that could
plausibly move it -- stack checking, event trace, event logging,
performance info and TX_NOT_INTERRUPTABLE -- and is unchanged by all of
them, because the only conditional member nearby guards
tx_thread_filex_ptr, which follows the extension, and
TX_THREAD_USER_EXTENSION appears much later in the structure. The
assertions therefore cannot misfire on a legitimate configuration.

C99 has no _Static_assert, so a negative array dimension is used. Each
file compiles clean under -std=c99 -pedantic -Wall -Wextra and emits zero
bytes of code or data.

Verified that the checks actually catch regressions rather than merely
compiling: asserting a deliberately wrong offset is rejected on all three
ports, and removing the VFP field again fails the build outright with a
message naming the missing member.

These ports have no CMake build of their own, so the files take effect
only in builds that compile everything under src/. That is still
worthwhile given they cost nothing and emit nothing. Extending the same
technique to the remaining ports is deliberately left as separate work: it
requires reading each port's assembly to attribute every hard-coded
literal to the right structure, and copying assertions without that
analysis would risk asserting wrong offsets.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Added a ThreadX port for Arm Cortex-R52 with the GNU toolchain

Adds ports/cortex_r52/gnu together with a CMake build, an example build for the
freely available Armv8-R AEM FVP, and an automated test suite.

The port is EL2-aware by construction.  Cortex-R52 always implements EL2 and
resets into it, so the reset path configures EL2, installs both the EL2 and EL1
vector tables, and only then drops to EL1 to run the kernel.  Keeping that
structure from the start lets partitioning work reuse the boot path rather than
replace it; TX_R52_BOOT_AT_EL1 skips the EL2 stage where a vendor monitor has
already dropped privilege.

This is also the first R-profile port in the tree with a working CMake build.
cmake/cortex_a9.cmake exists but ports/cortex_a9/gnu/CMakeLists.txt is empty, so
no A- or R-profile port could be built this way before.

Contents:

  - Kernel port: 16 assembly sources plus tx_port.h, seeded from the
    Cortex-R5/GNU port.
  - Build: cmake/cortex_r52.cmake and the port CMakeLists.txt, with soft/hard
    float, VFP, FIQ and IRQ/FIQ nesting options.
  - Example BSP: EL2 to EL1 boot, GICv3, generic timer, PMSAv8-R MPU,
    semihosting and PL011 consoles.
  - Tests: six images registered with CTest, each judging itself and
    terminating the model.
  - tx_port_offset_check.c: compile-time assertions on the TX_THREAD offsets
    the assembly reaches by hard-coded displacement.  This is the same defect
    class fixed for Cortex-R4/R5 in #578; nothing in the toolchain ties those
    literals to the C structure.

Two facts were established by experiment rather than taken from documentation,
and are recorded in the code and the readme because both are traps:

  - PRBAR.AP bit order is reversed relative to th
    AArch64 macro set: the low bit is read-only and the high bit grants EL0
    access.  Programming four disjoint regions, one per encoding, and
    attempting a privileged write to each gave 0b
    and 0b10 allowed.  Using the AArch64 ordering
    as read-only and silently accept writes, because region coverage is still
    enforced: an unmapped address faults while a "read-only" region does not.
  - The generic timer PPI is INTID 30, recorded from ICC_IAR1 after enabling
    the whole PPI range 25-31, rather than assume

Model behaviour that a silicon port must revisit: the FVP leaves CNTFRQ at zero
with the system counter stopped, so the BSP programs both; CNTHCTL.PL1PCTEN,
CNTHCTL.PL1PCEN and ICC_HSRE must be set at EL2 or EL1 accesses trap; and
tx_thread_vfp_enable() sets only a per-thread software flag, so enabling the FPU
hardware (CPACR, FPEXC.EN) is the board support package's responsibility, not
the kernel's.  The FVP reports MIDR 0x410FD0F0, an architecture envelope model
rather than a Cortex-R52, so it validates architecture and not implementation.
Nothing in the port is gated on MIDR.

Changes outside ports/cortex_r52, three and all small: cmake/cortex_r52.cmake is
a new toolchain file; CMakeLists.txt gains enable_testing() at the top level,
without which add_test() in a subdirectory generat
the root never references, so ctest reports no tests from the build root; and
.gitignore covers build directories and __pycache__.

Verification: all six images build warning-free a
FVP_BaseR_AEMv8R in three configurations -- protection off, protection and
caches on, and both together with hard-float VFP.  Six build-option
combinations build clean and pass the tick-and-preemption demo at run time.

The tests assert behaviour rather than configuration, which is what caught the
problems above: the cooperative demo asserts the order of execution, because
counting alone cannot distinguish working context switches from one thread
running to completion, and the MPU test provokes real faults, because reading
SCTLR back only proves a bit was set.

The 96-test regression suite is host-side and pas
configurations on the Linux port.  It is not cross-run here: it validates
portable kernel logic, while the port-specific risk is the assembly, which the
FVP images exercise.  Structural coverage is therefore not claimed.

Deliberate deviations: passing a string literal to tx_thread_create reports a
discarded const qualifier from common/inc/tx_api.
CHAR *; demo_threadx.c keeps the shipped sample's int main() so the demo body
stays byte-identical to it; and the floating-point test compares exactly on
purpose, since a tolerance would mask a restored
wrong.

Not included: modules support, split-mode SMP and
support. The example targets the FVP only, and NXP S32Z280 bring-up is
separate work; the readme flags what to re-verify there.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The host-side module converter utilities allocated code_buffer inside the
loop over the ELF code sections and never released it, so every code
section in the input ELF leaked one buffer. The malloc() result was also
unchecked, so a failed allocation passed a null pointer on to
elf_object_read() and crashed the tool.

Release the buffer at the end of each iteration and report a clean failure
with exit code 5 when the allocation does not succeed. Verified with
AddressSanitizer on a two-code-section input: 128 bytes leaked in 2
allocations before, none after, with byte-identical output.

Fixes #571

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Hardened the module converter utilities against malformed input

While reviewing the code_buffer leak reported in issue 571, three further
pre-existing defects turned up in the same host-side utilities.

The four ELF area allocations in module_to_binary.c and module_to_c_array.c
were unchecked, and every elf_object_read() return value was discarded, so a
truncated or crafted ELF file was read into whatever the allocation and the
reads happened to leave behind. Check each allocation, distinguishing a NULL
return for an empty area from a genuine failure, and abandon the conversion
with exit code 5 on an allocation failure and exit code 6 on a read failure.

Validate the section string table index taken from the ELF header before it
is used to subscript the section header area. AddressSanitizer confirms that
an out-of-range index produced a heap buffer overflow in both tools.

Correct the address format specifiers in module_to_c_array.c and
module_binary_to_c_array.c, which passed an unsigned long to %08X, and close
the source file on the invalid format path of module_binary_to_c_array.c.
The unused current_total local is removed. All three utilities now build
warning free with gcc -std=c99 -Wall -Wextra, and the code they emit is
unchanged byte for byte on valid input.

Refresh the version banners of all three tools, on the console and in the
header written into the generated C arrays, to the 2024 Microsoft Corp and
2026 Eclipse ThreadX contributors copyrights and version v6.5.2.202603. The
banners still advertised v5.8 and v5.4 with a 2018 build date. The .exe
suffix is dropped from the tool names, since these tools build on Linux too.

Related to #571

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

* Added the missing licence header to module_binary_to_c_array.c

The file carried no copyright or licence header at all, unlike the two other
converter utilities in the same directory. Use the same MIT header they carry,
since the three tools share an origin.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
xQueueCreate() allocated the queue descriptor and its backing memory, then
created two ThreadX semaphores, and returned NULL on either semaphore failure
without releasing anything. Since no handle reached the caller, vQueueDelete()
could not be used to recover, so both allocations were lost. A failure on the
second semaphore additionally abandoned the read semaphore it had already
created, leaving a live ThreadX control block inside freed memory.

Release the backing memory and the descriptor on both paths, and delete the
read semaphore before returning when the write semaphore cannot be created.
This is the teardown order vQueueDelete() already uses, and it matches the
cleanup xTaskCreate() performs on its own error paths.

Verified with a fault injection harness that intercepts the ThreadX byte pool
and semaphore entry points to force tx_semaphore_create() to fail on a chosen
call. On a read semaphore failure the layer previously performed 2 allocations
and 0 releases, and on a write semaphore failure 2 allocations, 0 releases and
0 semaphore deletions. It now performs 2 releases in both cases and deletes
the read semaphore in the second, with the byte pool restored to its prior
state.

Fixes #570

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Fixed the resource leaks on the xQueueCreate error paths

xQueueCreate() allocated the queue descriptor and its backing memory, then
created two ThreadX semaphores, and returned NULL on either semaphore failure
without releasing anything. Since no handle reached the caller, vQueueDelete()
could not be used to recover, so both allocations were lost. A failure on the
second semaphore additionally abandoned the read semaphore it had already
created, leaving a live ThreadX control block inside freed memory.

Release the backing memory and the descriptor on both paths, and delete the
read semaphore before returning when the write semaphore cannot be created.
This is the teardown order vQueueDelete() already uses, and it matches the
cleanup xTaskCreate() performs on its own error paths.

Verified with a fault injection harness that intercepts the ThreadX byte pool
and semaphore entry points to force tx_semaphore_create() to fail on a chosen
call. On a read semaphore failure the layer previously performed 2 allocations
and 0 releases, and on a write semaphore failure 2 allocations, 0 releases and
0 semaphore deletions. It now performs 2 releases in both cases and deletes
the read semaphore in the second, with the byte pool restored to its prior
state.

Fixes #570

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

* Added a regression suite for the FreeRTOS compatibility layer

The compatibility layer had no tests in this repository, which is awkward for
its creation functions in particular. Each of them takes one or two byte pool
allocations for its bookkeeping and then creates ThreadX kernel objects, and
each returns NULL when a kernel object cannot be created. The caller is left
without a handle, so it cannot call the matching delete function, and anything
the layer failed to release is gone until the system restarts. A leaking
version and a correct version are indistinguishable from the outside, which is
how the leak in issue 570 went unnoticed.

Add a suite that counts what the layer takes and gives back. A test asks the
harness to fail a chosen kernel creation call, then checks the number of byte
pool allocations, releases, object creations and object deletions performed.
The ThreadX entry points are intercepted with the linker's --wrap so that
tx_freertos.c is compiled exactly as it ships, with no test hooks in it. Note
that tx_api.h maps the public API onto the error checking entry points, so the
_txe_ symbols are the ones wrapped. Coverage is the creation and teardown paths
of queues, tasks, semaphores, mutexes, event groups and timers, including a
regression test for the two paths fixed for issue 570.

The suite follows the layout of the existing ThreadX and SMP suites, is
registered with ctest, and runs in CI through the shared regression template.
It is built 32 bit because the Linux port defines ULONG as unsigned int on
x86_64 while the layer passes pointers through ULONG arguments, so a 64 bit
build truncates them. It is Linux only because --wrap has no MSVC equivalent,
and the CMake configuration says so rather than failing at link time.

Validated by building the suite against the layer as it stands before the
issue 570 fix, where the two expected checks fail with the leaked counts, and
against the fixed layer, where all three tests pass.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
xQueueCreateStatic() and xTaskCreateStatic() take their storage from the
caller, so neither leaks memory, but both create ThreadX objects and both
return NULL when a later step fails. The caller is left without a handle and
cannot call the matching delete function, so any object already created stays
registered in the kernel, pointing into a caller buffer that the application
is now free to reuse or discard.

Three paths were affected. xQueueCreateStatic() abandoned the read semaphore
when the write semaphore could not be created. xTaskCreateStatic() abandoned
the notification semaphore when the thread could not be created, and abandoned
both the semaphore and the thread when the thread could not be resumed.

Delete what was already created before returning on each of them. The resume
path terminates the thread before deleting it, since a thread created with
TX_DONT_START is suspended rather than terminated, which is the same order the
idle task uses when it reaps a deleted task.

Extend the regression suite to cover all three paths, and add thread resume to
the set of entry points the harness can force to fail. Each static failure case
now uses its own control block, so a future regression on one path cannot carry
damage into the next case and report misleading counts there.

Verified against the layer as it stands on dev, where the three new checks fail
with the objects left behind, and against the fixed layer, where the suite
passes.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
_tx_thread_system_return_inline() in the Cortex-M4 AC6 tx_port.h was followed
by a second, orphaned copy of its own body. The copy had no function header, so
it declared interrupt_save at file scope and then placed statements there,
which does not compile. It is also the older version of the body, without the
dsb and isb barriers, so it was left behind rather than intended: the barriers
were added by commit 33efad3 and the previous text was not removed.

Delete the orphaned copy. What remains is the same body every sibling port
carries: after this change ports/cortex_m4/ac6/inc/tx_port.h differs from
ports/cortex_m4/iar/inc/tx_port.h and ports/cortex_m7/ac6/inc/tx_port.h only in
the port name in the banner and the version string, as it should.

Verified by compiling the function in isolation, which fails on the file scope
statements before the change and is clean afterwards, and by a structural scan
of all 208 tx_port.h files in the repository confirming this was the only
occurrence.

Fixes #569

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The Cortex-M ports under ports/ are generated. scripts/copy_armv7_m.sh copies
one tx_port.h and the per tool sources to fifteen M3, M4 and M7 targets, and
scripts/copy_armv8_m.sh does the same for nine M33, M55 and M85 targets. The
ports_arch_check workflow runs both scripts and fails if the tree is not
reproducible, so those copies are meant never to be edited directly.

They were. Every Cortex-M fix since #523 was applied to the generated copies
and not to the source, so the source fell behind and the check went red:
running the three scripts on dev changes 35 files. The check triggers only on
pull requests targeting master, which is why nothing caught it while the fixes
were merged into dev.

Left alone, the next run of these scripts would have reverted three separate
pieces of work: the memory barriers and clobbers from #523, the correction of
the IAR assembly header to use the assembler's own comment syntax, and the move
of tx_initialize_low_level.S into example_build for the M33, M55 and M85 GNU
ports from #514.

Bring the sources up to what the ports carry today, and regenerate. Two
behavioural changes come with that, both deliberate. The barriers from #523
reach the ac5 and keil variants of M3, M4 and M7, which were outside the scope
of that fix and never received it. The barrier that follows restoring the
interrupt posture, which #523 gave only to the GNU ports because GNU was the
only toolchain that could be tested, now applies to every tool; the identical
asm statement already shipped in the AC6 and IAR ports, so this adds a pipeline
flush rather than any new compiler exposure.

Regenerating also drops a stray #endif at the end of the Cortex-M85 IAR
tx_port.h, added by #523, which left that header with one more #endif than #if
and unable to compile. Every other ARMv8-M port was balanced.

Verified that the scripts are idempotent afterwards, that ports_arch_check
would pass, that no port loses a barrier or a clobber, that every regenerated
header is preprocessor balanced, and that every Cortex-M port covered by the
two scripts now carries the entry barrier.

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

Three defects reached the repository through the port trees recently, and each
of them is mechanically detectable without a cross compiler. Add
scripts/check_ports.sh, which looks for exactly those three, and give CI and
the release process the same command a contributor can run locally.

The generated Cortex-M ports must be reproducible from ports_arch. Fixes were
applied to the generated copies instead of the source for eight months, and the
next run of the copy scripts would have reverted them.

Preprocessor directives must balance. A fix left the Cortex-M85 IAR tx_port.h
with one more #endif than #if, so that header could not compile.

No port header may carry a statement outside a function body. A fix left a
second, headerless copy of a function body in the Cortex-M4 AC6 tx_port.h,
which is issue 569. The check tracks brace depth while skipping preprocessor
lines, multi-line macro bodies and comments, and reports assignments,
dereferences and control statements that land at file scope. Headers under
example_build are excluded, since those trees vendor third party SDK code.

A fourth section reports, without failing the run, on port families that have
no copy script and so cannot be checked for reproducibility. It currently
observes that the Cortex-M0 ac5, ac6 and keil ports lack the barriers their gnu
and iar siblings have.

ports_arch_check now calls the script rather than inlining a copy and diff, so
CI and the command line check the same things by the same definition, and the
workflow now triggers on pull requests to dev as well as master. Triggering on
master alone is why the drift went unseen. prepare_release.sh runs the checks
before it branches or rewrites anything, and stops if they fail, with
SKIP_PORT_CHECKS=1 as the escape hatch.

Each check was verified by reintroducing the defect it exists to catch and
confirming that the script fails, then confirming it passes on a clean tree.

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

The ARMv7-A and ARMv8-A ports are generated by update.ps1, which needs
PowerShell, so the cortex-a job in ports_arch_check ran on a Windows image and
nobody could reproduce it locally on Linux. Add update.sh beside each
update.ps1, with the same cores, compilers, copy sets and patches, and move the
job to the same Linux image as everything else.

The bash scripts were checked against the PowerShell ones by comparing what
each reports as drifted. They agree exactly on the 63 files the Windows job
last reported, and differ on 12 more, which turn out to be a defect in
update.ps1 rather than in the port. Its two .cproject patterns are written as
'value=`"cortex-a7`"' with backticks that survive into the pattern, so that
replacement has never matched, while the neighbouring Cortex-A7.NoFPU pattern
has no backticks and always worked. The result is that the AC6 example builds
for the A5, A8, A9, A12, A15 and A17 cores name cortex-a7 as their CPU while
their FPU string is correct. The bash scripts do what the PowerShell ones
intended, so regenerating corrects those twelve files.

Restore ports_arch as the source for the rest. The implementation of
_tx_thread_smp_time_get from #555 was applied to the twenty four generated SMP
ports and never to ports_arch, which still held MOV x0, #0 with a FIXME
comment, so regenerating would have replaced a working generic timer read with
a stub. That implementation now lives in the source. The remaining differences
are cosmetic and resolve in favour of the source: a trailing blank line in 38
copies of tx_thread_schedule.S and comment spacing in one tx_port.h.

Note that the Cortex-A VFP fix is already present in ports_arch and was never
at risk, contrary to what the description of the port consistency checks change
said before this was measured.

Extend scripts/check_ports.sh to run the A profile generators too, and make it
fail when a generator fails or is missing rather than reporting a clean tree,
which would have been a false pass.

Pin every workflow to ubuntu-24.04. ubuntu-latest already resolves to that
image, so nothing changes today, but a future migration becomes a deliberate
commit rather than something that happens underneath the -m32 builds.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
… that way (#593)

The gnu ports are only ever built with GNU tooling, and GNU as accepts several
non-canonical forms that LLVM's assembler rejects. Nothing noticed, because
nothing built them with anything else. This matters beyond clang itself: Arm
Toolchain for Embedded is LLVM based and is the successor to Arm Compiler 6, so
these are the code paths ac6 users move onto.

Seven files needed changing, none of which alters the emitted code:

LDREX and STREX take no offset in A32 state; the #imm form is Thumb-2 only. GNU
as drops the redundant zero, LLVM rejects it. Removed from the Cortex-A5, A7 and
A9 SMP protect routines.

ARMv8-M Baseline has no flag-preserving MOV immediate, so GNU as already emits
MOVS. Writing MOVS in the two Cortex-M23 sources says what the assembler was
doing anyway. One of them sits in a branch only compiled for the single mode
secure configurations, which is why it had never surfaced.

The Cortex-M0 schedule routine wrote LDR r0, =#0x10000000 with a stray hash,
which its own sibling file already wrote correctly.

The Cortex-M0 system return routine selected the numbered subsection .text 32,
which makes LLVM place the constant pool beyond the range a Thumb-1 PC relative
load can reach. Plain .text fixes it and GNU accepts either form. The reason is
recorded in the file, since 32 files pair a numbered subsection with a literal
pool load and the rest only escape because Thumb-2 and A32 have far more range.

Add scripts/check_clang.sh, which assembles every Arm gnu port source and
compiles the common C sources for one core per architecture profile, and a
clang_check workflow that installs Arm Toolchain for Embedded and runs it. The
toolchain version is pinned and checksum verified, for the same reason the
runner image is pinned.

The port directory to target mapping in that script is explicit rather than
prefix matched. Prefix matching is what makes cortex_a5 also match cortex_a53
and cortex_a55, which are AArch64, and assembling those as ARM32 produces
hundreds of misleading errors; that mistake cost real time while measuring this,
so the reason is recorded next to the table.

Verified with Arm Toolchain for Embedded 22.1.0: 711 of 711 assembly sources
assemble and 185 of 185 C sources compile for all eight profiles, against 6
assembly failures before the change. arm-none-eabi-gcc still assembles all 315
ARM32 sources, so nothing regressed for GNU. The check was confirmed to fail
when any one of the fixes is reverted.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
…en on Linux (#594)

* Made the example builds work with LLVM, and fixed four that were broken on Linux

The gnu example builds are the natural LLVM path: Arm Toolchain for Embedded is
LLVM based, consumes GNU ld linker scripts, and needs none of the scatter files
or Arm DS projects the ac6 examples carry. So rather than port the ac6 examples,
teach the gnu scripts to drive either toolchain.

Each script now selects the toolchain from a TOOLCHAIN variable that defaults to
gnu, so every existing invocation behaves exactly as before. TOOLCHAIN=atfe
switches the compiler, adds the target triple, passes the entry symbol through
to the linker rather than to the driver, and links the toolchain's own
semihosting library in place of --specs=nosys.specs, which is a GCC spec file
mechanism with no LLVM equivalent. That last choice avoids adding a syscall stub
source to every example.

The scripts are parameterised rather than duplicated. Copies of build scripts
would drift the first time one side was edited, which is the failure this
repository has just spent several changes recovering from.

Four sample scripts could not run on a case-sensitive filesystem at all. They
compiled MP_PrivateTimer.s and V7.s while the files on disk are
MP_PrivateTimer.S and v7.s, and the Cortex-A5 and A9 scripts named MP_GIC.s
where the file is MP_GIC.S. The link lines named V7.o accordingly. Corrected to
match the files, which is why the Cortex-A5, A7, A8 and A9 examples now build on
Linux where before they could not.

Extend scripts/check_clang.sh to link the example builds as well as compile the
sources, since compiling proves the sources parse while only linking exercises
entry symbols, linker scripts and the C library together.

Eight examples are listed as not expected to link, each with its reason, so the
gaps stay visible rather than being silently skipped. Five of them fail with the
GNU toolchain too and are therefore not LLVM problems: the Cortex-M0 example's
crt0 references __text_load_start__, __text_start__ and __text_end__, which its
linker script never defines, while the Cortex-M4 script defines the equivalents;
the arm9, arm11, Cortex-R4 and Cortex-R5 examples need newlib multilib variants
that are not present in every GNU toolchain packaging. The Cortex-A12, A15 and
A17 examples fail only with LLVM, because their link line omits -nostartfiles so
the toolchain's own crt0 is linked and wants picolibc's __data_start,
__data_source, __data_size and __bss_size, which their linker script does not
define.

Verified by building every example with both toolchains. Seven link with both:
Cortex-A5, A7, A8, A9, M3, M4 and M7. The GNU results are unchanged where they
worked before, and now also succeed for the four scripts with the case bug.

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

* Resolved the compiler path before running the example builds

The example stage runs each build script from inside its own directory, so a
relative path passed with --clang stopped resolving there and every example
build failed instantly. Local runs passed an absolute path and did not show it;
the CI job passes a path relative to the workspace root, which did.

Resolve the compiler to an absolute path once, before any directory change, and
print it so the toolchain in use is visible in the log.

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

* Parameterised the archiver as well, and stopped the check hiding failures

The toolchain selection covered the compiler but not arm-none-eabi-ar, which the
scripts invoke 628 times to assemble the library archive. A machine with an LLVM
toolchain and no GNU one therefore could not build any example, which is exactly
the situation in CI: the runner has no Arm GNU toolchain installed. Local runs
had one, so this only appeared once the job ran.

Select the archiver alongside the compiler, taking llvm-ar from beside clang in
the toolchain. The four scripts that call arm-none-eabi-ld directly are left
alone: they are arm9, arm11, Cortex-R4 and Cortex-R5, all already listed as not
expected to link, and their invocations are specific to GNU ld in ways that
parameterising would not resolve.

The check reported "example build produced no image" and then filtered the log
for lines containing "error", which hid the actual cause, since a missing tool
reports "command not found" or "No such file or directory". It now prints the
tail of the log. That filtering cost two CI round trips to diagnose something
the first run already knew.

Verified by shadowing arm-none-eabi-gcc, arm-none-eabi-ar, arm-none-eabi-ld and
the aarch64 equivalents with stubs that fail loudly, then running the whole
check: all 711 sources assemble, all eight profiles compile and all seven
example builds link without any GNU tool being invoked. The GNU default path
still produces an identical image. The diagnostics were confirmed by pointing
the archiver at a name that does not exist and checking that the reason appears.

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

Two unrelated Cortex-M0 gaps, both left over from earlier work.

The memory barriers from #523 reached the Cortex-M0 gnu port but not its ac6 or
iar siblings, in either the inline system return in tx_port.h or the assembly
routine. Both take the same GNU or IAR code path, and iar already carried the
entry barrier, so the missing pieces were the entry pair for ac6 and the barrier
after restoring the interrupt posture for both. All three tools now match.

The Cortex-M0 example could not link with any toolchain. cortexm0_crt0.S
references 23 linker script symbols and the script defined only 12 of them, so
__text_start__, __text_end__, __text_load_start__, the rodata and fast section
symbols, and the ctors and dtors load addresses were all unresolved. The
Cortex-M4 script defines all 23, including a .fast section with no content whose
symbols exist so that the startup copy is a no-op, and its comment says as much.
The Cortex-M0 script is brought to that same shape.

That left the example failing under LLVM only, on instructions that ARMv6-M can
encode just one way. The file declared .code 16 but no syntax mode, so GNU as
used the legacy divided syntax in which a plain add or sub sets the flags
implicitly, while LLVM implements unified syntax only and rejected the
non-flag-setting spelling. Declaring .syntax unified and writing movs, adds and
subs makes both assemblers agree, and the encodings GNU produces are byte
identical before and after, verified by disassembling both objects.

The Cortex-M0 example now links with GNU at 22,520 bytes of text and with Arm
Toolchain for Embedded at 22,866, so it comes off the list of examples not
expected to link and scripts/check_clang.sh now links eight of eight.

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

Those three sample scripts compiled crt0.S and reset.S but named neither on the
link line, and omitted -nostartfiles, so the toolchain was also free to link its
own startup. Under GNU that produced a working image; under an LLVM toolchain
picolibc's crt0 was pulled in and wanted __data_start, __data_source,
__data_size, __bss_size, __stack, __tls_base and __arm32_tls_tcb_offset, none of
which the linker script defines.

Defining picolibc's contract in the shared linker script was tried first and
abandoned: it reaches into ABI constants that cannot be verified here, and it is
unnecessary, because the in-tree crt0.S is already a complete startup for this
script. It sets up the stack and zeroes BSS between __bss_start__ and
__bss_end__, and .data carries no AT() so there is nothing to copy.

So the three scripts now pass -nostartfiles and name crt0.o and reset.o, which
is what the four sibling A profile cores already do and what these scripts were
evidently compiling those files for.

Both the old and the new GNU images contain the in-tree startup, __vectors from
reset.S and _mainCRTStartup from crt0.S, so the startup was already being used
through implicit startup file resolution rather than an explicit operand. How
that resolution happened without the objects being named is not accounted for
here, which is itself the argument for naming them: the same implicit behaviour
does not hold across toolchains, and that is why the LLVM link failed.

Add a readme to each of the three example directories. Their
tx_initialize_low_level.S is the generic ARMv7-A skeleton, 300 lines and
identical across all three, with no interrupt controller programming, no timer
and no vector table installation, where the A5, A7, A8 and A9 examples have all
three and ship the matching Versatile Express support files. These three
therefore demonstrate that the port builds; they will not receive a timer tick.
Nothing in the tree said so, which invites the assumption that they are
equivalent.

Verified with both toolchains for all three cores. GNU links at 41,276 bytes of
text, down from 41,768 because the unused toolchain startup is no longer
included, and Arm Toolchain for Embedded links at 37,982 where it previously
could not link at all. The resulting image is structurally sound: entry at
_start, __vectors at address zero, and a BSS range in RAM. It has not been
executed. scripts/check_clang.sh now links eleven of eleven example builds.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The AArch64 examples could only be built through the Arm Development Studio
project files beside them. There was no script, so nothing in CI or on a
developer's machine could build them, and the sources they need were never
named anywhere: vectors.S, v8_aarch64.S and v8_utils.S were present but
unreferenced, which is why a hand written link failed on GetCPUID, GetAffinity,
InvalidateUDCaches and ZeroBlock. Those four are defined in v8_aarch64.S and
v8_utils.S, in the tree all along.

Add one pair of scripts, in ports_arch so every AArch64 port receives them.
Both derive the -mcpu value and the kernel source directory from the port
directory they sit in, so a single pair serves the twelve ThreadX ports and the
twelve SMP ports, the latter building against common_smp and picking up the C
sources those ports carry alongside the assembly. TOOLCHAIN=atfe selects Arm
Toolchain for Embedded in place of the GNU toolchain, as it does for the ARMv7
example scripts.

One symbol genuinely had no definition. startup.S calls
initialise_monitor_handles to open the standard file handles over a debugger
connection; the GNU toolchain provides it in libgloss through
--specs=rdimon.specs, while picolibc has no equivalent and neither does the
LLVM toolchain's semihosting library. semihost_stub.S supplies a weak no-op,
linked for that toolchain only, so a real definition always wins.

Extend scripts/check_clang.sh to cover ports_smp as well as ports, since the
example builds now exist there too.

Verified by building every AArch64 example with Arm Toolchain for Embedded:
twelve ThreadX ports at 314,744 to 315,256 bytes of text and twelve SMP ports
at 325,304 to 325,880. scripts/check_clang.sh now reports 35 of 35 example
builds linking with none failing, the twenty-four new ones plus the eleven that
already built. The images have not been executed: the sample
targets the Base platform peripheral addresses, which the available emulator
does not provide.

Cortex-A34, and the Cortex-A34 and A78 SMP ports, are left out. They are not in
the generator's core list, so they receive nothing from ports_arch and cannot be
checked for reproducibility either. Bringing them in rewrites 114 files in those
three directories, which deserves its own change.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The comment block describing what the script does was left half-rewritten when
the example link stage was added to it: one sentence ends mid-clause and the
word "profile" appears twice, once as the tail of a sentence that was meant to
be replaced.

Say the three stages plainly instead, and keep the point the truncated sentence
was making, that only the linking stage needs a target C library. This block is
what --help prints, so the damage was user visible.

Comment only; no behaviour change. Verified that --help still frames the
intended lines, since it selects them by number.

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

ports/cortex_a34, ports_smp/cortex_a34_smp and ports_smp/cortex_a78_smp were
absent from the ARMv8-A generator's core list, so ports_arch never reached them
and scripts/check_ports.sh could not detect that they had drifted. They had.

The two Cortex-A34 ports sat two releases behind the shared sources, at 6.1.10
against 6.3.0. The difference is not only banners: tx_initialize_low_level.S
lacked the SUB x1, x1, #15 that precedes the BIC when the system stack pointer
is recorded, so the value stored was the incoming SP rather than the first
16-byte boundary below it. The Arm Development Studio example was missing
GICv3_aliases.h, which every other port's GICv3_gicc.h includes to reach the
interrupt controller registers through the stringify indirection.

The Cortex-A78 SMP port had never had its debug launch configuration patched at
all. It still named the A35 platform and model, so Debug Cortex-A35,
Base_A35x4 and FVP_Base_Cortex-A35x4 would have started an A35 model for an A78
port.

Add cortex_a34 to the core list. Cortex-A78 cannot go in the same list, because
the generator walks cores against port sets and would then create a
ports/cortex_a78 that the tree has never had; give it a separate SMP-only list
consulted only for the tx_smp port set. Both changes are mirrored in update.ps1,
which stays equivalent to update.sh.

Verified by regenerating: exactly these three port directories change, 116 files
modified and 13 added, and no already-covered core moves, so the core list was
the only thing holding them back. scripts/check_ports.sh passes, which now means
these three are checked for reproducibility for the first time.
scripts/check_clang.sh reports 38 of 38 example builds linked, the three new
ports included.

readme_threadx.txt is not added here. Only the two Cortex-A35 template ports
carry it; the other 23 AArch64 ports do not, so its absence from Cortex-A34 is
the tree's norm rather than drift, and supplying it everywhere is separate work.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
…ped examples (#600)

Two gaps in check_clang.sh, both of which made the Cortex-R52 port look better
covered than it was.

C_CORES gains cortex_r52. That list is described as one core per architecture
profile, and Armv8-R AArch32 is a profile rather than a variant of Armv7-R: the
port is written by hand instead of generated from ports_arch, so cortex_r5 does
not stand in for its tx_port.h. The assembly was already covered, since #593
added the PORT_TARGET entry, but the common C sources had never been compiled
against that header.

The example stage skipped any directory lacking both driver scripts, and did so
in silence. A port absent from the count reads as covered, which is how the two
CMake based example builds under ports/cortex_r52 looked like part of the total
while never being built. The stage now reports what it passed over, in the two
distinct cases that exist. The comment above EXAMPLES_EXPECTED_TO_FAIL already
asks for gaps to stay visible; this makes the code agree with it.

Four Cortex-M ports turn out to carry build_threadx.sh with no
build_threadx_sample.sh, so they were skipped despite having a driver:
cortex_m23, cortex_m33, cortex_m55 and cortex_m85. Reported, not fixed. Whether
those want a sample script is a separate question.

The expected-to-fail test now runs before the Arm test rather than after. arm9
and arm11 are on that list but carry no PORT_TARGET entry, so testing for Arm
first dropped them from the report entirely, reintroducing the same silence for
two ports that were already being named.

Verified with Arm Toolchain for Embedded 22.1.0, the version CI pins: 711 of 711
assembly sources, 185 of 185 common C sources for each of the nine cores
including cortex_r52, and 38 of 38 example builds linking, so the built set is
unchanged by this commit. The remaining driverless ports report as cortex_r52,
cortex_a5_smp, cortex_a7_smp and cortex_a9_smp, the three A profile SMP ports
having no example scripts and the R52 examples being driven by CMake. --help
still frames the intended lines, since the additions sit below the range it
selects by number.

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

The Cortex-M23, M33, M55 and M85 examples each shipped a build_threadx.sh with
no build_threadx_sample.sh, so scripts/check_clang.sh skipped all four at its
linking stage: the whole Armv8-M family had no example-link coverage. Adding the
missing script surfaced five separate reasons why none of them could have linked.

ports/cortex_m23/gnu/src/tx_initialize_low_level.S referenced
Image$$ARM_LIB_STACK$$ZI$$Limit and __Vectors, which are Arm toolchain
scatter-load names that no GNU linker defines. This is a GNU port, so it now
uses __RAM_segment_used_end__ and _vectors, as the Cortex-M33 version already
does. The Cortex-A ports mention Image$$ZI$$Limit only inside a comment; this
was a live relocation.

The Cortex-M23 crt0 needed unified assembler syntax. The Armv7-M file it derives
from carries no .syntax directive, so GNU as reads it in the legacy divided
syntax, where a Thumb data-processing instruction sets the flags whether or not
the mnemonic says so, and "mov r2, #0" assembles to 2200, MOVS. LLVM implements
unified syntax only, where those mnemonics mean the non-flag-setting forms that
Armv8-M Baseline does not have. Disassembling the Cortex-M4 object confirms GNU
already emits 2200 movs, 1a52 subs and 3001 adds, so spelling them out changes
no encoding. The flags carry meaning: crt0_memory_copy branches on the result of
"subs r2, r2, r1" and again on "subs r2, #1".

Cortex-M23 has no SVC_Handler to name in its vector table. Its library is built
-DTX_SINGLE_MODE_NON_SECURE, and tx_thread_schedule.S defines that handler only
when neither TX_SINGLE_MODE_SECURE nor TX_SINGLE_MODE_NON_SECURE is set, so the
SVCall slot takes __tx_BadHandler. The table also uses the CMSIS handler names
throughout, because that is what the Armv8-M ports export: the Armv7-M table
references __tx_SVCallHandler and __tx_SysTickHandler, which resolve to nothing
here.

The other three needed a C library. tx_thread_secure_stack.c calls malloc and
free, which pulls the allocator in, and the sample scripts already defined
SYSCALL_LIB for exactly that without ever passing it to the linker. Their linker
scripts now also provide end and _end, which newlib's libnosys _sbrk wants, and
__heap_start and __heap_end, which picolibc wants instead.

Cortex-M55 and M85 build with the hard-float ABI now, in both the library and
the sample. Arm Toolchain for Embedded ships no soft-float MVE multilib and says
so plainly: "No library available for MVE with soft-float ABI." The library and
the sample have to agree, and -mfloat-abi=hard is what check_clang.sh already
uses for these two cores in PORT_TARGET.

Verified with GNU 13.2.1 and Arm Toolchain for Embedded 22.1.0. Every port links
under both: Cortex-M23 at 206,136 and 312,636 bytes, M33 at 305,856 and 320,696,
M55 at 306,228 and 320,664, M85 at 306,240 and 320,668. check_clang.sh now
reports 42 of 42 example builds linking, up from 38, and no longer lists any port
as carrying build_threadx.sh without build_threadx_sample.sh. The images are
link-verified only and have not been executed, as with the AArch64 examples.

Only .sh drivers are added. Cortex-M23 has a build_threadx.bat with no sample
counterpart and the other three have no .bat at all; adding untested Windows
scripts belongs in its own change.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
fdesbiens and others added 16 commits September 28, 2026 11:31
* 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)
…#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>
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>
_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>
)

_tx_thread_smp_unprotect takes the Linux mutex on entry and releases it twice when the
protection structure names this core, but only once when it does not, while the matching
_tx_thread_smp_protect took it once either way. _tx_thread_system_return clears the
protection outright, so an unprotect can find it no longer naming its core and return
having released one level fewer than were taken.

A thread that later reaches _tx_linux_mutex_release_all drains that level. The timer
interrupt thread has none on its tick path unless a thread was preempted on core 0, so a
level leaked there while core 0 is idle is never recovered: the nesting count never
reaches zero, the mutex is never handed back to Linux, and every other thread waits on
it for the life of the process while the tick keeps arriving.

The release of the level the matching protect took now happens whether or not the
protection still names this core. Protection bookkeeping and scheduling are unchanged,
and releasing beyond what a thread holds was already a no-op.

Two hung processes captured untraced, in different tests, show the timer thread owning
the mutex with a nesting count of one while parked in its tick wait, the scheduler
blocked in pthread_mutex_timedlock, and the clock advancing 99 ticks a second across a
ten second sample while nothing else changes. On a healthy run that path is taken 0
times in 26,546 unprotect calls. Forcing it on the timer thread leaks one level before
this change and balances after it.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The randomised phase of threadx_smp_random_resume_suspend_exclusion_pt_test
guards its preemption-threshold setup with tx_thread_priority > 40, but the
test creates its 1024 source threads with priority = i reset whenever it
reaches TX_MAX_PRIORITIES, which is 32 on the Linux port the regression suite
runs. The guard is therefore never true. The one thing that distinguishes this
test from its non-pt twin in that phase never executes, so the randomised phase
exercises no preemption threshold at all and the test duplicates its twin
there. The threshold the earlier rebalance section sets is unaffected.

Both the guard and the threshold gap are now derived from TX_MAX_PRIORITIES, so
the phase sets a threshold whatever the port's priority count is. On a
32-priority port that is priority > 16 with a threshold eight levels better,
which keeps the original shape of a threshold set on the worse half of the
priority range.

Verified under gdb that the block is now reached, at priority 26 with a
threshold of 18, and that thread_entry's preemption-threshold invariant is
entered for a randomised-phase thread; neither happened before the change.
40/40 passes, 20 runs each in disable_notify_callbacks_build and
stack_checking_build.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The Cortex-R5 assembly guarded its execution-profile hooks with only the legacy
TX_ENABLE_EXECUTION_CHANGE_NOTIFY symbol. The documented
TX_EXECUTION_PROFILE_ENABLE configuration initialized profiling without recording
thread or interrupt transitions.

I made all AC5, AC6, GNU, Green Hills, and IAR hooks accept both symbols. I also
extended the port consistency and GNU/LLVM feature checks to cover the current
configuration.

All 849 base assembly files and all 219 TX_EXECUTION_PROFILE_ENABLE files passed
with GCC 14.2.1 and clang 22.1.0. A CMake/Ninja Cortex-R5 profile build emitted
all seven expected hook relocations. Proprietary toolchains were not run.

Assisted-by: Codex (GPT-5) <noreply@openai.com>
#763)

Running git blame on a file header returns a tree-wide copyright, disclosure or
version-constant pass rather than the commit that last touched the code. There
have been 11 such passes in this repository since 2024, and every header line
in the tree now points at one of them.

Added .git-blame-ignore-revs listing those commits. GitHub applies the file to
its blame view automatically; locally it takes one git config command, which
the file documents in its own header.

Additive. No source file is touched, and every listed commit was verified to be
an ancestor of dev.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The ThreadX README sent readers to archived Markdown documentation.

I linked the overview, Modules, and SMP guides to published HTML pages. The general
documentation entry now follows the latest release.

All replacement URLs returned HTTP 200; git diff --check passed.

Assisted-by: Codex (gpt-6-astra) <noreply@openai.com>
Git reads trailers only from a block at the very end of a commit message. A
squash merge concatenates the branch's messages, so every trailer but the last
ends up mid-message, and an indented trailer is skipped as well. Both forms are
real attributions and both are invisible to the trailer parser, which is what
the project has been using to answer which agents touched a file.

The script reads whole commit bodies instead. It groups by value, or lists one
line per commit with --list, and takes a revision and a path the way git log
does.

On dev it reports 163 attributions where the trailer parser reports 122, so a
quarter of the record was unreachable. The difference is entirely squashed and
indented trailers, not new commits.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
common_smp/src/tx_misra.c performs four implicit conversions that its
monoprocessor twin makes explicit, and ignores a parameter the twin casts to
void. Built with the warning set common_smp uses, the file reports five
diagnostics that common/src/tx_misra.c does not:

  tx_misra.c:49  conversion to 'int' from 'UINT' may change the sign of the result
  tx_misra.c:93  conversion to 'ULONG' from 'int' may change the sign of the result
  tx_misra.c:151 the same
  tx_misra.c:221 the same
  tx_misra.c:624 unused parameter 'status'

The two files are otherwise the same shim, so this is drift rather than a
deliberate difference: common/src already carries (INT) on the memset value,
(ULONG) on the three pointer differences, and (VOID)status.

That the shim is the file reporting them is the reason to fix it. It exists to
route the kernel's pointer conversions through functions a MISRA analysis can
account for, and an implicit signed-to-unsigned conversion inside it is the
class of construct it was written to remove.

The test trees hide it. test/tx builds the kernel with -Werror, so the
monoprocessor copy could never have regressed this way; test/smp has -Werror
commented out, so the SMP copy warns and builds.

Applying the monoprocessor form to all five sites. Object code is byte
identical before and after, compared with --strip-debug at -m32 -std=c99 with
TX_MISRA_ENABLE defined, so this changes diagnostics and nothing else.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
* Updated ThreadX contribution guide for current workflows

The existing guide left contributors without current build, test, and
submission guidance.

The guide now covers the shared project process and ThreadX-specific C99,
port, simulator, CI, attribution, and release practices.

Documentation links resolve and git diff --check passed. Build tests were
not run for this documentation change.

Assisted-by: Codex (gpt-6-sol) <noreply@openai.com>

* Added visible spacing between contributor setup steps

The ECA and Git setup items had no visible gap in GitHub Markdown.

An indented line break now separates them while preserving list numbering.

GitHub Markdown rendered the items in one list with the visible gap, and
git diff --check passed.

Assisted-by: Codex (gpt-6-sol) <noreply@openai.com>
…so (#762)

* Normalized the AI disclosure line across every tracked file type

The repository-wide pass covered source files only, so build files, CMake
toolchain files, shell scripts and the GDB and manifest files kept the older
per-edit form of the disclosure comment, which names a product and a model
version. The Cortex-R52 module manager port then merged after that pass and
brought the old form back into the sources as well.

Replaced it with the fixed text in all of them, using the comment character
each file already uses.

Comment-only. 143 files, one line each. The repository now holds 601 files
carrying exactly one disclosure line, none carrying the old form, and none
carrying more than one.

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

* Added a check that keeps the AI disclosure comment in its one accepted form

Nothing enforced the disclosure convention, so the drift it exists to prevent
returned twice: once when a port merged after the normalisation pass carrying
the older per-edit form, and once because that pass had covered source files
only, leaving build files and scripts untouched for months.

Added scripts/check_ai_disclosure.sh, which rejects the superseded per-edit
form, a doubled comment marker, more than one disclosure line in a file, and
any spelling of the line that is not exact. It runs from repo_checks.yml, a
workflow with no path filter, because a source-path filter is what hid the
build files the first time.

The check passes on this branch. Each of its four rules was confirmed to fail
on a tree with that defect reintroduced, and to pass once it was removed.

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

* Normalised the disclosure lines that landed after this branch

Thirteen files reached dev after this branch was written, each carrying the
superseded per-edit form. txm_module_manager_dispatch.h reached it with six
stacked copies, naming the same product and the same model every time -- the
accumulation the fixed text exists to prevent.

Each of those files now carries one disclosure line in the accepted form. Where
the accepted line was already present, the superseded ones are deleted rather
than converted, so no file gains a second.

check_ai_disclosure.sh reported eighteen hits across thirteen files before the
pass and passes after it.

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

* Exempted Markdown from the near-miss disclosure check

The near-miss rule flags any line carrying the phrase "AI assistance" that is
not the accepted text, which is right for source but wrong for documentation.
The contribution guide has to quote the accepted line and say when it applies,
so the check reports two paragraphs of prose as drift and fails the build.

Markdown is now exempt from that rule alone. The three rules that matter for a
documentation file -- superseded form, doubled comment marker, duplicate line
-- still scan it, so a stale disclosure in a Markdown file is still caught.

The check passes against a tree carrying the rewritten contribution guide, and
still fails when a near miss is planted in a source file.

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

* Corrected the trailer command named in the disclosure check

The header comment points a reader at git's own trailer parser to find out
which agents have touched a file. That parser reads trailers only from a block
at the very end of a message, so a squash merge -- which concatenates a
branch's messages -- buries every trailer but the last one mid-message, and an
indented trailer is skipped outright. On dev it finds 122 attributions where
163 exist.

The comment now names count_assisted_by.sh, which reads whole bodies.

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

Object names are exposed as writable pointers throughout the kernel API, which
rejects string literals in C++ and lets a caller modify a name an object still
holds. Information services return those names through writable double
pointers.

Create services, control blocks, information services, the module manager and
trace registration now preserve const qualification, behind
TX_ENABLE_CONST_NAMES. The option defaults to off, so a build that says nothing
gets exactly the types it got before. It is opt-in rather than opt-out because
it changes the type of a public struct field: application code that copies a
name into a writable CHAR * stops compiling, which is a reasonable thing to ask
of a minor release and not of a patch one. Issue #780 tracks making it the
default in 6.6.

Two things the option reaches that its own call sites do not.
TX_CHAR_TO_UCHAR_POINTER_CONVERT has exactly two users, both of them reading an
object name in _tx_trace_object_register, and every form of that macro but the
MISRA one casts the qualifier away without saying so; the conversion is now
const in and const out, so nothing launders const to make the build pass. The
FreeRTOS adapter holds the name pcTaskGetName retrieves in a TX_NAME_CONST
pointer so that it tracks whichever declaration tx_thread_info_get has, and
keeps its writable return type through an explicit MISRA C:2012 Rule 11.8 cast,
because that signature is part of the FreeRTOS API.

Default build: all seven host configurations and all five SMP configurations
build with zero warnings and pass -- 113/113 on five host configurations,
100/100 on the two MISRA builds, 118/118 on SMP, 3/3 FreeRTOS. With
TX_ENABLE_CONST_NAMES set, the host default and both MISRA configurations, the
SMP trace configuration and the FreeRTOS adapter build with zero warnings and
pass.

Co-authored-by: Tilen Majerle <tilen@majerle.eu>
Assisted-by: Codex (gpt-6-astra) <noreply@openai.com>
Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
)

ThreadX still declares 6.5.1.202602 with hotfix 'a', which is the previous
release rather than the one being cut.

prepare_release.sh updated the five version constants in tx_api.h and the
version string in 211 port headers across ports, ports_smp, ports_arch and
ports_module. The hotfix letter is cleared, since the target has none. Nothing
else changed: every line in the port commit carries a version, and no file
outside an inc/tx_port.h was touched.

The port consistency checks passed before the branch was cut, which is what the
script gates on. Host regression 113/113 with zero warnings, and the AI
disclosure check passes.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
Two commits on dev are mechanical and repository-wide, and neither is in the
list. The disclosure normalisation is the second half of a pass whose first
half, #740, is already listed, so blame behaves differently either side of it.
The version pass is the same shape as the two release preparations above it.

Both are now listed with their file counts. The const object names change is
deliberately absent: it alters behaviour, and size is not the criterion.

All thirteen revisions in the file resolve, and git accepts it as an ignore
file.

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

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Test Results FreeRTOS

3 tests   3 ✅  0s ⏱️
1 suites  0 💤
1 files    0 ❌

Results for commit 729959f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Test Results RISC-V

1 158 tests   1 158 ✅  6m 3s ⏱️
   12 suites      0 💤
   12 files        0 ❌

Results for commit 729959f.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Test Results SMP

590 tests  +263   590 ✅ +264   5m 55s ⏱️ - 14m 8s
  5 suites +  2     0 💤 ±  0 
  5 files   +  2     0 ❌  -   1 

Results for commit 729959f. ± Comparison against base commit 44d7c95.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
common_smp.src 100% 81% ➖
Summary 100% (5455 / 5468) 81% (3052 / 3788) ➖

Minimum allowed line rate is 99%

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Test Results ThreadX

765 tests  +285   765 ✅ +285   3m 13s ⏱️ - 4m 1s
  7 suites +  2     0 💤 ±  0 
  7 files   +  2     0 ❌ ±  0 

Results for commit 729959f. ± Comparison against base commit 44d7c95.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code Coverage

Package Line Rate Branch Rate Health
common.src 99% 77% ➖
Summary 99% (4658 / 4687) 77% (2607 / 3374) ➖

Minimum allowed line rate is 99%

The port stage matched a version only where "Version" was followed immediately by
four dotted numbers. A port writing three numbers, or a stray letter before the
first, was passed over and kept its old release, and the pass reported success
either way.

The RISC-V32/IAR port is one of them. It reads "Version G6.5.0.202601", and the
letter in front of the number has kept it out of every pass since, so it has
advertised 6.5.0.202601 across two releases it was not part of. Its string now
reads the current release.

The pattern accepts three or four numbers and tolerates a leading letter. A check
follows it: any port header that names a release other than the one being
prepared is listed and the pass stops, so a port the substitution cannot reach
fails the release instead of shipping a version that misreports itself.

Verified: every ThreadX port header now advertises 6.5.2.202603. Against a FileX
tree, a port the substitution cannot reach makes the pass name the file and exit
non-zero, and correcting that string lets the pass complete.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
@fdesbiens fdesbiens changed the title Merging changes ahead of the v6.5.2.202603 release Merged changes ahead of the v6.5.2.202603 release Sep 29, 2026
AFOliveira and others added 3 commits September 30, 2026 10:49
ThreadX had no board support for Erbium, the OpenHW Foundation CORE-ET RISC-V platform
that Zephyr and NuttX support. Its hart implements only part of the F extension, needs a
4 KiB aligned mtvec and uses the original Shakti UART.

The example runs the standard demo on hart 0 in machine mode. It builds the library and
the demo for rv64imc_zicsr_zifencei with the soft-float lp64 ABI and links without libc
or libgcc, so the pinned riscv64-unknown-elf toolchain can build it. It programs the
machine timer, a polling UART console and the PLIC, writing the source priorities and
threshold that silicon hardwires because the simulator resets them to 0. No shared file
changes.

The demo built without warnings and ran on erbium_emu from et-platform 836a4ab, with all
eight threads reporting and five thread 0 wakeups in 100M cycles, which matches the 2
MHz tick. Separate test images took five PLIC UART interrupts through the ThreadX ISR
path and halted with mcause 2 on an illegal instruction. check_ai_disclosure.sh and
check_ports.sh passed.

Assisted-by: Claude Code (Opus 5.5) <noreply@anthropic.com>
Long polling UART writes can mask timer interrupts across several tick periods.
PLIC enable-word updates can overwrite a change made by an interrupt handler.
The board example had no simulator tests for either case.

Added two simulator images and a runner. One compares hardware timer progress with
ThreadX ticks after a long print. The other toggles one PLIC source in timer
context while a thread changes another. Both tests fail on the PR code and pass
with the corresponding local fixes.

GCC 15.2.0 and Ninja built both images. erbium_emu reproduced both failures;
control runs passed both tests. Port consistency checks passed. No silicon run.

Assisted-by: Codex (GPT-6-Sol) <noreply@openai.com>
Polling UART output held machine interrupts off for an entire string, which
could lose ThreadX ticks. PLIC enable-word changes could overwrite an update
made by an interrupt handler.

The UART now polls with interrupts enabled and protects only the final status
check and byte write. PLIC enable and disable changes are protected across
their read-modify-write. The README explains the new simulator tests.

Both simulator tests failed on the original code and passed with these fixes.
The 100-million-cycle demo run reported all eight threads and five thread 0
wakeups. AI disclosure and port consistency checks passed. No silicon run.

Assisted-by: Codex (GPT-6-Sol) <noreply@openai.com>
The install step is capped at ten minutes for every consumer. That suits a
consumer that only installs packages, which is all of them bar one: measured
install steps run 0.2 to 0.5 minutes, and NetX Duo's secure interoperability
job, which builds OpenSSL and wolfSSL from source, takes 2.1 to 2.8.

GUIX does not fit. Its regression suite is staged across the install, build and
test steps to keep each inside its limit, so the install step also builds a
configuration, and on a slow mirror the packages and the ThreadX checkout leave
too little of the ten minutes for it. Three runs on dev were lost that way.

The cap is now an input defaulting to ten, so a consumer that needs longer asks
for it and every other consumer keeps the timeout it has today.

Assisted-by: Claude Code (Opus 5) <noreply@anthropic.com>
The build step is capped at fifteen minutes for every consumer, on the same
shape of assumption the install step carried: that a consumer builds once.
Measured build steps are 0.5 minutes for LevelX, 1.1 for FileX and 6.4 for
USBX, so fifteen is right for them.

GUIX builds five configurations in that step, because its suite is staged
across the three steps, and reaches 11.8 minutes. That is 79 per cent of the
cap, and it grows with every regression test added -- six advisories landed
tests this week.

The cap is now an input defaulting to fifteen, matching install_timeout_minutes
above. No consumer changes unless it asks to.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants