GHSA-39rx-hq8c-4px8: Module Manager object lookup validates one byte of a module’s search name
GHSA ID |
|
|---|---|
CVE ID |
CVE‑2026‑94024 |
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
The Module Manager’s object lookup validated one byte of a memory-protected module’s search name and then walked as much of it as the module declared, in privileged mode, with the kernel’s preemption count raised. Both lookup dispatchers are affected: the recommended txm_module_object_pointer_get_extended is handed a length and does not use it to bound the buffer, and the deprecated txm_module_object_pointer_get supplies the largest value a UINT can hold, so it removes the bound rather than lacking one.
A module can make the walk unbounded without knowing anything about the memory it reaches, and where the walk reaches memory a privileged read cannot take, the resulting fault leaves the kernel unable to preempt for the rest of the system’s life.
Vulnerable functions: _txm_module_manager_txm_module_object_pointer_get_dispatch, _txm_module_manager_txm_module_object_pointer_get_extended_dispatch (both in common_modules/module_manager/inc/txm_module_manager_dispatch.h) and _txm_module_manager_object_name_compare (common_modules/module_manager/src/txm_module_manager_util.c).
Details
TXM_MODULE_MANAGER_PARAM_CHECK_DEREFERENCE_STRING validates one byte. That is sufficient for the eight object create services, which store the caller’s name pointer in the control block and never read further. It is not sufficient for the two lookup dispatchers, which hand the pointer to code that walks the string.
_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 terminating character a length excludes. _txm_module_manager_txm_module_object_pointer_get_extended_dispatch receives N in an extra parameter array, validates that two-word array correctly, and then passes N to the manager without binding the name buffer to it. _txm_module_manager_object_pointer_get, the deprecated wrapper, calls the same implementation with TXM_MODULE_MANAGER_UTIL_MAX_VALUE_OF_TYPE_UNSIGNED(UINT).
The comparison returns TX_FALSE at the first differing character, which makes the overrun look self-limiting: the module would seem to need to know the bytes past its region in order to match them. It does not. A create service stores the name pointer a module supplies 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 terminating byte in memory the module neither owns nor can see — or, through the deprecated service, until UINT_MAX characters have been compared.
Even without that, the first byte past the region is read unconditionally whenever a name buffer ends at a region boundary and its first character matches the first character of any object’s name, both of which the module chooses.
_txm_module_manager_object_pointer_get_extended raises _tx_thread_preempt_disable before it walks the created lists and lowers it only on its normal return path. The memory fault handlers terminate the requesting thread and notify the application; none of them touches that count. A fault taken inside the comparison therefore leaves it permanently raised, after which _tx_thread_system_preempt_check does nothing and _tx_thread_system_resume updates the execute pointer without returning to the system: a thread that becomes ready at a higher priority than the running one no longer preempts it. Cooperative switching continues; preemptive scheduling does not, and there is no path back short of a reset.
Impact
A memory-protected module causes privileged reads of memory it may not reach, beginning at the byte after one of its own regions and continuing for a distance neither the module nor the manager bounds.
The reliable impact is availability: where the walk reaches memory a privileged read cannot take, the requesting thread is terminated and the kernel is left unable to preempt for the remaining life of the system. The request can be repeated at will.
Confidentiality impact is limited rather than absent. The lookup’s return value does not distinguish "matched a prefix and then differed" from "did not match", so a module cannot recover adjacent bytes one at a time. What it gets is one equality bit per request about a whole-string hypothesis it chooses, of which the only cheap and reliable one is whether the byte immediately after its own region is zero. No integrity impact is claimed: the only write on this path is the object pointer into the module’s own range-checked result slot.
Affected configurations
ThreadX Modules with memory protection enabled, on every release from 6.0.1 through 6.5.1.202602a, verified by reading each of the 26 release tags that carry a Module Manager. Version 6.0 ships no Modules component.
A module loaded without TXM_MODULE_MEMORY_PROTECTION is unaffected, because the checks are inside the dispatchers' memory-protection block. The services must not be compiled out with TXM_MODULE_OBJECT_POINTER_GET_EXTENDED_CALL_NOT_USED or TXM_MODULE_OBJECT_POINTER_GET_CALL_NOT_USED, which is the default.
The finding is live on all 31 memory-protected module port and toolchain combinations for a name in the module’s read-only image, because the portable region check honours the extent everywhere. For a name in the module’s writable data or in shared memory registered to it, it is live on the 24 combinations whose data check acts on the size it is given, and on the three Cortex-A7 combinations once GHSA-fvp6-37q5-c648 is applied, that check having discarded the size before it.
Block pools and byte pools are refused to a memory-protected module by the lookup itself, so the six other object types are the ones this is exercised through.
Severity
CVSS v4.0 AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:H/SC:N/SI:N/SA:N — 6.9, medium.
AC:L holds without qualification: the module creates an object with a name pointer of its choosing, computes an address at the end of a region whose bounds it already knows, and calls the lookup. Nothing is guessed and no other component is involved.
VA:H rests on the walk being bounded by nothing the module supplied or proved reachable, which the aliasing above establishes without any knowledge of the memory past the region, and is reinforced by the preemption count a fault leaves raised. VC:L rests on the one-bit-per-request equality oracle described under Impact; it is neither VC:N, since something real does come back and the module chooses what it asks about, nor anywhere near VC:H.
Recorded sensitivities: VC:N gives 6.8 and VC:H gives 6.9, so the score is insensitive to the confidentiality argument. VA:L — for a reviewer who rejects the aliasing composition — gives 4.8. AC:H gives 5.8. All stay medium.
Patches
Fixed in ThreadX 6.5.2.202603. TXM_MODULE_MANAGER_PARAM_CHECK_DEREFERENCE_STRING_RANGE is added beside the one-byte check the create services keep using. It validates name_length + 1 bytes over the same set of regions, refuses a length whose range cannot be expressed rather than letting the addition wrap, and keeps the null clause the one-byte check has.
The extended dispatcher validates the extra parameter array first, since the length comes out of it, and then the name over its declared range. Every refusal in that dispatcher returns TXM_MODULE_INVALID_MEMORY, so the reordering is not observable. The deprecated dispatcher refuses a memory-protected module outright, because no complete range can be proven from a pointer alone; the request number and the ABI are unchanged, and modules without memory protection are unaffected. Such a module should use txm_module_object_pointer_get_extended, which the module library has already flagged at compile time.
_txm_module_manager_object_name_compare is unchanged. Reading N+1 bytes for a declared length of N is the documented contract; the defect was that nothing checked it had been met.
A regression test drives both dispatchers and measures both extents — the one the dispatcher validates and the one the comparison reads — asserting that the second never exceeds the first, across every declared length, every amount of room, and each of the three regions a search name may live in. Against the unfixed headers it fails 408 of 1130 expectations. Coverage of the two dispatchers is 100% of lines, branch outcomes and calls.