| Age | Commit message (Collapse) | Author |
|
Bumps the actions-minor-and-patch group with 1 update: [EnricoMi/publish-unit-test-result-action](https://github.com/enricomi/publish-unit-test-result-action).
Updates `EnricoMi/publish-unit-test-result-action` from 2.24.0 to 2.25.0
- [Release notes](https://github.com/enricomi/publish-unit-test-result-action/releases)
- [Commits](https://github.com/enricomi/publish-unit-test-result-action/compare/d0a4676d0e0b938bc201470d88276b7c74c712b3...7622c5bf3d112a72ab4fa3951c2aa5723d8ab92d)
---
updated-dependencies:
- dependency-name: EnricoMi/publish-unit-test-result-action
dependency-version: 2.25.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: actions-minor-and-patch
...
Signed-off-by: dependabot[bot] <[email protected]>
|
|
The FVP module manager example and the S32Z280 kernel and module examples are each
behind a CMake option that defaults to OFF, and no CI lane turned any of them on. The
module isolation tests were therefore never linked, and nothing built the S32Z280
images.
Both r52_fvp configurations now enable all four example options. The FVP module
images join the CTest suite whenever the model is available; the S32Z280 images are
built and never run, since CTest registers only FVP images. The header no longer
describes these examples as pending.
On the Armv8-R AEM FVP 11.32 with GCC 14.3, the default configuration builds 14
images and passes 8/8, and the feature configuration builds 20 and passes 11/11,
with zero warnings. CI still builds without executing until FVP_AEMV8R_URL is set.
Assisted-by: Claude Code (Opus 5.5) <[email protected]>
|
|
Fixes #805
r52_fvp only ran the FVP when a repository variable pointed it at the model, and none
was set, so every run built the images and executed nothing. The workflow also
expected an unpacked model, but Arm ships the FVP as an installer script.
The workflow now downloads the 11.32_19 archive from Arm's public permalink, checks
it against a pinned SHA-256, and runs Arm's installer with its EULA accepted. The
cache is keyed on the checksum. A missing model now fails the job instead of
skipping execution. CONTRIBUTING.md no longer describes the repository variable.
The workflow's install, locate, and build-and-run steps, run locally as written,
download and verify the archive, install 11.32+19, and pass 6/6 default and 9/9
feature on the FVP. The installed binary is identical to the one used for local
validation of the port.
Assisted-by: Claude Code (Opus 5.5) <[email protected]>
|
|
Comments, CMake messages and demo banners in the Cortex-R52 port and its module
example referred to development milestones by internal labels. The labels mean
nothing to a reader of the port, and three FVP demo images were named after them.
Replaced each label with a description of what the code or image does, and renamed
demo_m2, demo_m3 and demo_m5 to demo_coop, demo_tick and demo_vfp, with their run
targets to match. The banners the demos print change accordingly; the FVP test
runner matches only result lines, so no test depends on them.
Default, feature and module configurations pass 6/6, 9/9 and 8/8 on the Armv8-R
AEM FVP, as on dev, with zero warnings; the S32Z280 images build cleanly. Not run on
S32Z280 hardware.
Assisted-by: Claude Code (Opus 5.5) <[email protected]>
|
|
The header and function banner comments in the Cortex-R52 port and its module
port do not close on the right border. 990 of 4,228 banner lines sat one to three
columns off, and in the module port that was 42 per cent of them. Measured
against the ports the convention came from, Cortex-A7 and Cortex-M33 are at zero
and Cortex-R5 at 2 per cent, so this is the R52 tree rather than the house style.
Lines whose text already fitted are re-padded. Where the text itself ran past the
border the trailing words move down to the next line, which pushes the paragraph
along until every line fits, so no wording is shortened and no sentence is recut
by hand. The bar widths were already correct in every file and are untouched.
One line is left over and is a real exception:
_txm_module_manager_thread_stack_build is long enough that its name and the
release column cannot share a line at this width.
Verified: the banner text is word-for-word identical before and after in all 45
files, the port builds at 203 targets with zero warnings, and all eleven changed
assembly files assemble.
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
The artifact and Pages workflows pin actions behind their current patch releases.
Updated upload-artifact to v7.0.2, download-artifact to v8.0.2 and deploy-pages
to v5.0.1, retaining immutable SHA pins. Component sources are unchanged.
The ThreadX, SMP, FreeRTOS and RISC-V regression jobs and repository checks
passed. Pages deployment was not exercised by the pull request.
Assisted-by: Copilot (gpt-6.1-sol) <[email protected]>
|
|
Four comments in the Cortex-R52 module port explained what the Cortex-M ports do
instead. A reader working on this port cannot act on any of it: the behaviour
described belongs to a different architecture, and the surrounding comments
already say what this port does and why.
The remaining text is unchanged. The abort vector still explains the three things
it owes the fault handler and what omitting the first costs, the fault handler
still explains why its two statements are ordered as they are, and the preamble
still says that every GNU module port writes the entry-point words the same way.
Comment-only. The port builds clean, 203 targets, zero warnings.
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
Fixes #744
MISRA stack checking converted the stack address through 32-bit ULONG.
Preserved ALIGN_TYPE width in both stack-alignment conversions. Added a
native-width behavioral regression that fails on the original code.
The regression fails on the original code and passes with the fix. tx, smp, riscv and
freertos suites are green in CI, along with the Cortex-M/A/R52, gnu and atfe builds.
Assisted-by: Codex (gpt-6.1-sol) <[email protected]>
|
|
Gave the per-test ctest timeout headroom over the slowest test
|
|
Stopped the coverage deploy running after a cancelled test job
|
|
The ceiling was 1000 seconds and GUIX's guix_bidi_text_draw_32bpp needs most of it.
Measured across six passing runs in October 2026 it took between 831 and 957 seconds,
and it timed out four times in the same period. Nothing about the test changed between
the passes and the timeouts; runner load decided it, because the suite builds and tests
five configurations at once. Each timeout also costs twice the ceiling, since
until-pass:2 runs a failing test again, and the failures were indistinguishable from a
real hang in the logs.
Raised the default to 1500 and made it overridable through CTEST_TIMEOUT, matching how
CTEST_PARALLEL_LEVEL and CTEST_REPEAT_FAIL are already handled. A per-test TIMEOUT
property on the one slow test was considered and rejected because the ceiling is shared
by every suite this script serves and the next slow test should not need a second fix.
bash -n parses. The default and the override both resolve as intended. A test that
times out twice now occupies 50 minutes of the 60 minute job timeout, so a genuine hang
still fails the test rather than the step. This repository's own suites call the
template by relative path and so ran the new ceiling: tx, smp, riscv and freertos all
green. Components referencing the template at @master keep the old ceiling until this
reaches master.
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
deploy_code_coverage gated on !failure() && !cancelled(). Both report the status of
the workflow, not of the job named in needs, so a run_tests that is cancelled leaves
both false and the deploy proceeds. On the GUIX run of 5 October 2026 run_tests was
never assigned a runner, sat fifteen minutes and was cancelled; the deploy ran anyway
and failed with "No artifacts named github-pages were found for this workflow run".
A run that executed no test at all reported itself red for a missing artifact.
Gated on needs.run_tests.result == 'success' instead, so the deploy runs only when the
job that uploads the artifact actually succeeded. A caller passing skip_test gains the
same protection: run_tests is skipped, and the deploy no longer attempts a download
that cannot succeed.
yaml.safe_load parses the file. Against the four run_tests outcomes seen on that GUIX
run, the new condition deploys on success and skips on failure, cancelled and skipped;
the old one deployed on three of the four. This repository's own suites call the
template by relative path and are green on tx, smp, riscv and freertos -- but not the
changed line itself: deploy_code_coverage is push-only, so a pull request skips it
before the new condition decides anything. Components referencing the template at
@master are unaffected until this reaches master.
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
Merged changes ahead of the v6.5.2.202603 release
|
|
The RISC-V job installs qemu-system-misc, ninja-build and cmake under a
two-minute per-attempt budget. With recommends enabled that install fetches 67
packages and 87 MB of archives, 388 MB unpacked, because qemu-system-misc
recommends gstreamer, pulseaudio, v4l and a set of codec libraries. Twenty-six
of the 67 are that media stack, and the job runs qemu-system-riscv64 headless.
On a slow mirror the download does not finish inside the budget, and all three
attempts time out.
The install now passes --no-install-recommends. Raising the budget was the
alternative and is not available: TIMEOUT_LONG is already sized so that three
attempts fit inside the ten-minute install step, as its own comment records.
Observed on the run for #783, where all three attempts timed out while a second
RISC-V run at the same minute succeeded, which is the mirror-speed dependence
this removes.
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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 <[email protected]>
Assisted-by: Codex (gpt-6-astra) <[email protected]>
Assisted-by: Claude Code (Opus 5) <[email protected]>
|
|
* 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) <[email protected]>
* 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) <[email protected]>
* 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) <[email protected]>
* 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) <[email protected]>
* 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) <[email protected]>
|
|
* 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) <[email protected]>
* 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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
_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) <[email protected]>
|
|
_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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
* 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)
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
_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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
* 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) <[email protected]>
* 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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|
|
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) <[email protected]>
|