GHSA-rrjj-jwcw-hvf8: Module Manager accepts unauthenticated kernel object pointers, allowing module-to-kernel privilege escalation
Affected packages
| Package | Vulnerable versions | Patched versions |
|---|---|---|
ThreadX |
>= 6.0.1, ⇐ 6.5.1.202602a |
6.5.2.202603 |
Description
Summary
An unprivileged, memory-protected ThreadX module can have the kernel read and write memory at addresses of its choosing, in privileged mode, and can use that to clear the MPU enable bit and remove its own isolation boundary.
The Module Manager decided whether a privileged service could dereference an object address a module named by asking only whether that address fell outside the module. The manager’s object pool is outside every module, so the 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. The reported chain uses that to reach a privileged memset across an attacker-chosen range.
Details
_txm_module_manager_param_check_object_for_use() rejected a null pointer and an object overlapping the calling module’s own code or data, and accepted everything else. It established neither that the address was the start of an object rather than an address inside one, nor which type of object it was, nor whether the object was live. The TXM_MODULE_MANAGER_PARAM_CHECK_OBJECT_FOR_USE macro exposed that policy to all 58 object-using dispatchers, which passed a control block size and so could not have established a type even had the check tried to.
The object ID at the front of a control block is not an authenticator. A module can place the value of an ID inside an object it legitimately owns, which is what lets a shifted address pass an ID test; and distinct object types of equal size exist, so on a 32-bit target a byte pool, a mutex and a timer are indistinguishable by size and by nothing but their ID even at an address known to be an object start.
Earlier creation-side hardening checks the pointer passed to a create service. It does not constrain later object-use calls, and the reported chain begins with entirely legitimate allocations and creates, forging addresses only for the calls that follow.
The reported chain drove four dispatchers — tx_timer_create, tx_thread_create, tx_thread_time_slice_change and tx_thread_reset — and observed that others might serve equally well. That is correct: the gap is the object-use policy shared by all 58 object-using dispatchers, not the four the chain happened to use.
The functions at the centre of it are _txm_module_manager_param_check_object_for_use, _txm_module_manager_tx_thread_time_slice_change_dispatch, _txm_module_manager_tx_thread_reset_dispatch and _txm_module_manager_thread_reset.
Impact
An unprivileged module crosses the MPU boundary the Modules feature exists to enforce. The information-get dispatchers copy fields out of a purported object into module-supplied buffers, so a forged control block over a chosen privileged address returns the words at that address to the module; the reset and time-slice-change paths write attacker-chosen values to attacker-chosen addresses. With the MPU enable bit cleared the module has direct access to kernel and device memory, and a malformed forged object can instead hang or crash the instance.
Affected configurations
ThreadX Modules builds with memory protection. The defect is portable rather than port-specific: the check and the dispatchers live in common_modules, and every module port reaches them through the same dispatcher.
Every tagged release from 6.0.1 through 6.5.1.202602a accepts an object address on the strength of its location alone. Version 6.0 predates the Modules component. The wording of the check changed over that span without the policy changing: the earliest releases accept an address outside the module or a null one; later ones add a third accepting term rather than a restricting one, because the terms are disjoined; and from 6.4.3.202503 the check moves into a function that drops the null term and keeps the location-only policy. Both dispatchers the reported chain uses are present from 6.0.1.
Severity
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N — 8.5, High.
VC:H reflects a genuine arbitrary privileged read through the information-get dispatchers rather than an oracle. AT:N reflects that the gap is the authentication policy itself, present by default in every affected configuration, not the one demonstration that exercised it. AC:L reflects that control block layouts are fixed at compile time and the chain needs no race, no probabilistic condition, no secret and no mitigation bypass. SC:N/SI:N/SA:N reflect that the module and the kernel it escapes are the same ThreadX instance; consequences in a containing product belong in an environmental score.
Patches
Fixed in ThreadX 6.5.2.202603. Kernel objects a module names are authenticated before any privileged service dereferences them. Authentication establishes that the address is the exact address of a live kernel object of the type the service expects, and it establishes it from the kernel’s created list for that type, which the create and delete services maintain. Membership of that list settles all three questions at once: the address is an object start rather than an address inside an object, the object is of this type, and it has not been deleted. Nothing a module can influence is used to establish a type, and an address a module named is not read at all until a kernel record says there is an object there.
That list is also what makes an object the application created and shared with a module authenticable, 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. What authentication deliberately does not decide is whether the calling module is authorised to operate on an authentic object belonging to someone else; that is a separate question about ownership.
All 58 object-using dispatchers now pass the object type rather than a control block size. 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 and no 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 retained for dispatchers outside the repository and hardened to refuse object pool interiors, which is the part of the attack it can see without being given a type.
Two further changes close paths authentication alone would leave open. _txm_module_manager_thread_reset() 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 once it has run to completion, and reset reads the shell entry function out of that instance, so a null one was followed in privileged mode. _txm_module_manager_object_deallocate() clears the control block ID as it gives memory back, so memory a module wrote an ID into stops presenting that ID once the manager no longer owns it, rather than returning to the pool for the next allocation placed there to present. Authentication itself consults the created list and not the ID, so what this protects is the size-based check retained for dispatchers outside the repository.
126 host expectations 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 handed out again, objects the application owns, memory that merely carries a plausible ID, objects another module allocated, and the bounds on both scans, plus the reset sink. Reverting the object checks to the permissive policy fails 86 of them; removing the reset guard makes the test die with SIGSEGV inside the reset path; removing the ID clearing in deallocation fails 1.
Workarounds
None that preserve the feature. The path is reachable only from a loaded module, 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
This defect was reported privately to the project before the current vulnerability handling process was in place, and the contact details for the original reporters have not survived. The project has been unable to reach them. If you recognise this as your report, please contact the Eclipse ThreadX security team and we will credit you.
Fixed by Frédéric Desbiens (@fdesbiens). The analysis, fix and regression tests were produced with AI assistance under human review, in accordance with the Eclipse Foundation’s Generative AI Usage Guidelines.