GHSA-5w57-xhwq-v7wq: Module block-pool create validates two of the four parameter words it reads

GHSA ID

GHSA‑5w57‑xhwq‑v7wq

CVE ID

CVE‑2026‑94202

Severity

Medium

CWE

CWE-125

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 block-pool create dispatcher proved that two ALIGN_TYPE words of the extra-parameter array a memory-protected module supplies lay inside the module’s data, and then used indices 0 through 3 of that array.

One of the two unproved words is read 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. So the out-of-bounds read is performed by the validation code itself, before the kernel service is reached, and the size the caller’s pool start is then validated against is a word the manager has not proved the module owns. The other unproved word is read when the service’s argument list is built.

Vulnerable function: _txm_module_manager_tx_block_pool_create_dispatch, in common_modules/module_manager/inc/txm_module_manager_dispatch.h.

Details

A kernel service with more parameters than the module-to-manager call carries receives the surplus in an array that the module library builds and the module passes by pointer. The dispatch layer proves that array lies inside the requesting module’s data before reading any of it, by handing the port’s data check the array’s address and its length as sizeof(ALIGN_TYPE[n]), with n written by hand for each dispatcher.

For block-pool create n was written as 2. The body of the same function then reads:

  • extra_parameters[1] and extra_parameters[2] inside TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_WRITE(module_instance, extra_parameters[1], extra_parameters[2]), which asks whether pool_start through pool_start + pool_size is inside the module — where pool_size is index 2, outside the two words that were proved;

  • extra_parameters[0] through extra_parameters[3] to build the call to _txe_block_pool_create, re-reading indices 1 and 2 and reading index 3 for the first time.

Two words past the proved extent are therefore read in privileged context, each of them twice. The read is a fixed eight bytes on every module port ThreadX supports, all of which are 32-bit; there is no comparison and no loop, so there is nothing the module can lengthen.

Both of the two siblings of this shape in the same file are correct and have been since the array check was introduced, which is what establishes what the expression should have been. _txm_module_manager_tx_queue_create_dispatch has the identical four-word shape — a caller buffer at index 1 sized by index 2 and a control-block size at index 3 — and proves sizeof(ALIGN_TYPE[4]). _txm_module_manager_tx_byte_pool_create_dispatch has the same triple shifted down one index and proves sizeof(ALIGN_TYPE[3]) for its three words.

A memory-protected module normally reaches this path through the module library’s _txe_block_pool_create, which declares ALIGN_TYPE extra_parameters[4], fills all four words and passes the address. A malicious module need not use that wrapper: the kernel-call dispatcher’s address is supplied to the module as part of the module ABI, so it can call it directly with a pointer to an array that has exactly two words of room left before the end of its data region or of a shared region registered to it. A module controls its own data layout, so arranging that requires nothing but pointer arithmetic.

_txe_block_pool_create compares the control-block size it is handed against sizeof(TX_BLOCK_POOL) and refuses a null pool_start with TX_PTR_ERROR before dereferencing it. It does not re-check that pool_start through pool_start + pool_size lies inside the module, so the dispatcher’s buffer check is the only bound on that range.

Impact

A low-privilege module makes the Module Manager perform two word-sized privileged reads immediately past an approved parameter array, one of them inside the check whose purpose is to prevent exactly that.

The reliable impact is availability, and it is bounded. On the affected ports the MPU is enabled with the default memory map as a background region, so a privileged read a few bytes past a region carved out of RAM usually returns data rather than faulting; where it does fault, the memory fault handler terminates the requesting thread and the system continues. Nothing on this path disables interrupts or raises the kernel’s preemption count, so the fault costs the requesting thread and not the system’s ability to schedule.

Confidentiality impact is limited but real, and stronger than a status oracle. The unproved word at index 2 becomes the right-hand side of a comparison whose left-hand side, the pool start, is chosen by the module, and the dispatcher’s refusal is a status this service cannot otherwise return, so a manager refusal is always distinguishable from a kernel answer. A module can therefore bisect for that word, recovering it exactly in one request per halving of its own region, for every value a legitimate pool size could hold. The unproved word at index 3 is weaker: it reaches the service, which compares it against sizeof(TX_BLOCK_POOL), so it yields one equality bit per request.

What keeps that limited is that the address cannot be chosen. The array must pass the two-word check to be accepted at all, so the read window is anchored to the eight bytes immediately following a region the module already owns and may already read.

No integrity impact is claimed. The pool the kernel formats is bounded by a range the buffer check approved, and a memory-protected module cannot write to the two unproved words, so it cannot make either change between the check that reads it and the use that reads it again.

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. In the tags through 6.1.5 the array is not range-checked at all; from 6.1.6 onwards it is checked over two of its four words. Version 6.0 ships no Modules component.

A module loaded without TXM_MODULE_MEMORY_PROTECTION is unaffected, because the checks are inside the dispatcher’s memory-protection block. The service must not be compiled out with TXM_BLOCK_POOL_CREATE_CALL_NOT_USED, which is the default.

Both checks on this path are data-only, so reachability is decided entirely by the port’s data check. The finding is live on 27 of the tree’s 31 memory-protected module port and toolchain combinations: the 24 whose data check acts on the size it is given, and the three Cortex-A7 combinations once GHSA-fvp6-37q5-c648 is applied, that check having discarded the size before it.

Severity

CVSS v4.0 AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:N/VA:L/SC:N/SI:N/SA:N — 4.8, medium.

AV:L/PR:L is module execution. AC:L/AT:N because the module chooses where its own array sits and the dispatcher’s answer is deterministic.

VA:L rather than H because availability follows the extent, and this extent is a compile-time constant in the dispatcher’s own source: eight bytes, with no comparison and no continuation condition. A fault taken here terminates the requesting thread, confirmed rather than assumed, the path taking no interrupt lock and raising no preemption hold.

VC:L because the disclosure is genuine recoverable content rather than an oracle, but at an address the module cannot choose and in an amount of two words. VI:N for the reason given under Impact.

Recorded sensitivities: at VA:L the score is insensitive to the confidentiality argument, VC:N also giving 4.8, and only VC:H moves it, to 6.9. VA:H gives 6.9. Treating the boundary placement as a race, AT:P, gives 2.0 and low.

Patches

Fixed in ThreadX 6.5.2.202603. The array extent in _txm_module_manager_tx_block_pool_create_dispatch becomes sizeof(ALIGN_TYPE[4]), which is what its two siblings of the same shape already use. That is the whole change: the check already runs before every read of the array, so nothing is reordered, and the dispatcher ABI and the TXM_MODULE_INVALID_MEMORY status are unchanged. sizeof on an array type is a compile-time constant, so the extent costs nothing at runtime.

No conforming module is affected by the correction. The module library’s wrapper declares the array as a four-word local inside the module’s data region, so only a caller that bypasses the wrapper with a shorter array meets the new refusal.

A regression test drives all three create dispatchers that take a parameter array and measures the extent each one validates rather than sampling either side of it, alongside the highest index each dispatcher uses, so the invariant they are checked against is measured on both sides rather than written down. Against the unfixed dispatch header it fails 4 of 1117 expectations, reporting the validated extent as two words and the service reached carrying a word planted past the end of the region. All three create dispatchers are at 100% line coverage, the tests pass 99 of 99 in each of the five build configurations, and the changed header cross-compiles for all seven GNU 32-bit module ports with warning sets identical to the unfixed header’s.

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 analysis, fix and regression test were produced with AI assistance under human review, in accordance with the Eclipse Foundation’s Generative AI Usage Guidelines.