GHSA-36vf-7cx6-9f9j: Module delete dispatchers do not enforce kernel-object ownership before irreversible deletion
GHSA ID |
|
|---|---|
CVE ID |
CVE‑2026‑94023 |
Severity |
Medium |
CWE |
|
Published |
2026‑10‑02 |
Last updated |
2026‑10‑02 |
Affected packages
| Package | Vulnerable versions | Patched versions |
|---|---|---|
ThreadX |
>= 6.0.1, ⇐ 6.5.1.202602a |
6.5.2.202603 |
Description
Summary
A ThreadX 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.
Details
Two defects on the same path.
The ownership decision is reached after the irreversible act. txm_module_manager_object_deallocate, which all eight _txm_module_manager_tx*delete_dispatch functions call unconditionally after a successful _txe*_delete, has required the memory to belong to the calling module since v6.0.1_rel. A module deleting anything else could therefore only ever end in TX_PTR_ERROR. The ownership requirement is already in the delete path, expressed as the only way that path can complete successfully — it is enforced one step too late. By the time the error is produced the object has been unlinked from the created list for its type and its created count decremented, threads suspended on it have been resumed with TX_DELETED, an owned mutex’s ownership list and priority inheritance have been unwound in the owning thread, an active timer has been deactivated, and a pool the application was allocating from has ceased to exist. An error returned at that point describes a cleanup failure and restores nothing.
A privileged read in front of a caller-named address. _txm_module_manager_object_deallocate reached the manager’s private header by computing ((TXM_MODULE_ALLOCATED_OBJECT ) object_ptr) - 1 and read it before establishing that it *was a header — no pool bounds check of any kind. Because every delete dispatcher calls that function directly, 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 are unrelated memory read in privileged mode. They are also acted on: an address whose preceding words happen to name the calling module is unlinked from that module’s allocation list — a write through pointers read out of non-pool memory — and handed to _txe_byte_release. Where the preceding words are unmapped or fault-protected, the read itself faults in privileged mode.
The remediation below removes that read rather than bounding it. Searching the module’s own allocation list answers the ownership question without dereferencing the caller’s address at all, so a header that begins inside the object pool and ends past it, and an end-of-allocation computation that wraps, both stop being reachable.
The supported way for a module to learn a foreign object’s address is the manager’s own lookup service, and it applies no ownership filter to threads, timers, queues, event flags groups, semaphores or mutexes. One lookup call and one delete call are the whole sequence: no race, no ID forgery, no address guess. Block pools and byte pools differ — txm_module_object_pointer_get_extended refuses those two types outright for a memory-protected module, with TXM_MODULE_INVALID — so for those a module needs a pointer passed to it by another component, or a stale pointer to object-pool memory it once owned.
Memory protection does not help. The privileged Module Manager performs the delete on the module’s behalf, and for a module without memory protection the dispatcher’s parameter checks are skipped entirely.
GHSA-57mf-w87f-rq7m refuses to release the memory of an object that is still created. It does not reduce this advisory: by the time the unauthorised-delete path reaches the deallocation the object has been deleted, so the liveness refusal does not fire. The two compose in the deallocator — this advisory establishes that the address names one of the calling module’s allocations, and that one refuses to release it while a live object is in it.
Impact
A low-privilege module destroys kernel objects belonging to the resident application or to other modules, through the privileged dispatcher, using only services it is entitled to call. Synchronisation between components is broken, waiters are woken with TX_DELETED under a protocol that does not expect it, timers stop, pools disappear and a mutex owner’s effective priority is silently altered. None of it is recoverable: the object is gone before any check can refuse the request.
The blind read in front of a non-pool address adds a privileged fault on an address the module named, and a write through pointers read out of memory that is not the manager’s.
Confidentiality is not affected. Nothing read in front of the object reaches the module: the only value derived from that read is the deallocator’s return code, the address cannot be chosen because txe*_delete requires a genuine control block ID there, and the value compared against — the module’s own instance pointer — is unknown to the module and each probe costs the destruction of a real object.
Affected configurations
Any ThreadX Modules build with a Module Manager object pool. The defect is portable rather than port-specific: the delete dispatchers and the deallocation live in common_modules, and every module port reaches them through the same dispatcher.
Every release from 6.0.1 through 6.5.1.202602a, verified by reading each tag rather than by inference. v6.0_rel predates the Modules component and is not affected. The use check became stricter over that range, but every form of it accepts an object belonging to the application or to another module, which is what the delete path needs to be given, and the lookup service applies no ownership filter to the six non-pool types in any tag.
No race, ISR context or disabled error checking is required.
Severity
CVSS v4.0 base score 6.9 (Medium) CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N
AC:L was checked per object type rather than assumed, and holds because the lookup service returns unowned threads, timers, queues, event flags groups, semaphores and mutexes by name with no ownership filter — six of the eight types, including the synchronisation objects a shared design is most likely to have given the module the name of.
VC:N is retained after scrutiny, for the reason given under Impact: the attacker can aim the privileged read at nothing, and learns at most one bit about an unknown constant at the price of destroying a real kernel object. Recorded sensitivities: a reviewer who counts that one bit as a limited disclosure gets VC:L and 7.0, which crosses into high; one who holds that a foreign object’s address is not reasonably obtainable gets AC:H and 5.8, still medium. VI:H and VA:H do not depend on either reading.
Patches
Fixed in ThreadX 6.5.2.202603. The eight delete dispatchers now ask the ownership question before the delete rather than after it. A memory-protected module may delete a kernel 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, deletion being the inverse of creation. Anything else returns TXM_MODULE_INVALID_MEMORY; the object stays created, nothing waiting on it is resumed, and no created count moves, because the kernel is never asked. That status is what every other pre-service parameter rejection in the dispatch table returns, so no new value enters the module ABI, and it deliberately does not distinguish "not yours" from "not a usable address", so a module cannot use a delete request as an ownership oracle.
The check sits inside the existing memory-protection test with the other parameter checks. An unprotected module skips it as it skips all of them: such a module creates objects in its own memory through no check at all, and refusing to let it delete them would break it for no security gain.
_txm_module_manager_object_deallocate no longer reads a header to find out whether there is one. It finds the allocation by searching the module’s own allocation list, comparing the caller’s address and never dereferencing it, so an address that names none of this module’s allocations is refused without a privileged read of anything in front of it. The search also establishes what the header read could not: that the address is the exact start of an allocation rather than somewhere inside one. Being in the deallocation itself rather than at a call site, it covers all eight delete dispatchers, the deallocation request a module can make directly, and a direct application call. It runs with interrupts disabled and is bounded by the count the manager keeps beside the list, so damaged links cannot make it run on.
A type and lifecycle field in the private allocation header was considered and rejected, because such a field records only what the manager was told. _txm_module_manager_stop needs no exemption, verified rather than assumed: it deletes a module’s objects through the internal services rather than any dispatcher, so no caller-facing check is on that path.
Regression coverage drives all eight object types through allocation, ownership checks from both the owning module and another, and deallocation, with the pool boundaries, interior addresses, null and stale pointers, and memory that has changed hands. Restoring the previous deallocation fails four expectations and then segmentation faults on the null-pointer case; neutralising the ownership check fails 74. The new functions and the changed deallocation reach 100% line and branch coverage, all 99 host tests pass in each of the five build configurations under GCC 14, and the changed files compile warning-neutrally at -Werror for every GNU 32-bit module port.
Not covered by tests, stated so nothing more is read into them: the eight dispatcher call sites, because nothing in the tree compiles the kernel dispatch translation unit — they are covered by cross-compilation and review — and the ordering property, that the decision precedes txe*_delete, which is established by reading the eight functions.
Workarounds
None that preserve the feature. Every path here is reachable only from a module the Module Manager has loaded, so a system that loads only modules it builds itself is not exposed — but that is precisely the assumption module memory protection exists so that you do not have to make.
Credits
Found and fixed by Frédéric Desbiens (@fdesbiens). The finding came from a local AI-agent source review configured and run by the maintainer, then validated against the source. The remediation, its regression test and the documentation change were produced with AI assistance under human review, in accordance with the Eclipse Foundation’s Generative AI Usage Guidelines.