GHSA-63qc-jcr6-qw6r: Module object allocation size overflow corrupts the manager object pool

GHSA ID

GHSA‑63qc‑jcr6‑qw6r

CVE ID

CVE‑2026‑95107

Severity

High

CWE

CWE-190, CWE-680, CWE-787

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 chooses object_size when it calls txm_module_object_allocate. The Module Manager adds its own 16-byte private header to that size and asked the object pool for the result without checking the addition. A size near the top of a ULONG wrapped, the pool served a block far smaller than the header, and the manager then wrote the header past the end of it and into the neighbouring byte-pool block.

Details

_txm_module_manager_object_allocate passed (ULONG) (object_size + sizeof(TXM_MODULE_ALLOCATED_OBJECT)) to _txe_byte_allocate. The protected dispatcher, _txm_module_manager_txm_module_object_allocate_dispatch, validates only the output pointer for one pointer-sized write; the size passes through unconstrained.

On a 32-bit ULONG, object_size of 0xFFFFFFF8 wraps the request to 8 bytes. The pool accepts 8 bytes and returns them, and the manager then writes all four header fields — owning module instance, next, previous, and the caller’s size — 16 bytes into an 8-byte allocation. Every wrapped total from 1 to 15 overruns; after the alignment round-up in _tx_byte_allocate the overruns are 12, 8 and 4 bytes.

What those bytes land on is byte-pool metadata. A block header is a next pointer followed by an ALIGN_TYPE field holding either TX_BYTE_BLOCK_FREE or the owning pool pointer. In the 4-byte case the field that lands past the end is txm_module_object_size — a fully attacker-chosen ULONG — and it lands exactly on the following block’s next pointer, leaving that block’s free marker intact. The following block exists there whenever the free block exceeded the request by TX_BYTE_BLOCK_MIN and was split, which is the ordinary case.

_tx_byte_pool_search then treats an attacker-chosen value as a block boundary:

  • available_bytes becomes next_ptr - current_ptr, so an arbitrary value passes the size test, and the block is split by writing a new header at current_ptr + memory_size + sizeof(UCHAR *) + sizeof(ALIGN_TYPE) — which can fall outside the pool.

  • Otherwise the search reads *(next_ptr + sizeof(UCHAR *)) to test the neighbour’s free marker, and if that word equals TX_BYTE_BLOCK_FREE it merges, reading *next_ptr into pool metadata.

Both of those are privileged accesses at an address the module chose.

An overflow-checked helper, TXM_MODULE_MANAGER_UTIL_MATH_ADD_ULONG, arrived in the same 2020 commit that introduced this call and has been used on both module load paths ever since. This path never used it.

The residual sizes need no separate bound. _txe_byte_allocate refuses a zero size and any size larger than the pool, and the Module Manager calls it by its internal name rather than through the tx_byte_allocate macro, so TX_DISABLE_ERROR_CHECKING does not remap it away. The alignment round-up in _tx_byte_allocate, which would itself wrap a size within sizeof(ALIGN_TYPE) of ULONG_MAX down to zero, is therefore unreachable from this path.

Impact

A low-privilege module corrupts the shared Module Manager object pool and its own per-module allocation list. The consequences are privileged faults during later allocate and release, an attacker-chosen 32-bit value installed in the pool’s block list which leads the privileged allocator to write a block header outside the pool, and denial of service for every module and for the manager itself with no recovery short of reboot.

Affected configurations

Any ThreadX Modules build with a Module Manager object pool. The defect is portable, not port-specific: the call lives in common_modules and every module port reaches it through the same dispatcher. It requires TXM_MODULE_OBJECT_ALLOCATE_CALL, which is enabled unless explicitly compiled out, and a 32-bit ULONG, which every currently shipped module port has.

Introduced with the Modules component itself in commit 6f61053f (2020-08-07), and present in every tagged release from v6.0.1_rel through v6.5.1.202602a_rel. v6.0_rel predates Modules and is not affected.

No race, ISR context or disabled error checking is required.

Severity

CVSS v4.0 base score 7.0 (High) CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:L/VI:H/VA:H/SC:N/SI:N/SA:N

Exploit Maturity was not assessed, so EQ5 takes the v4.0 default of E:A.

Integrity and availability are unconditional. Confidentiality is scored low rather than high: before the corrupted next pointer can be followed for its contents the search must merge, and it merges only when the word at next_ptr + sizeof(UCHAR *) equals TX_BYTE_BLOCK_FREE, a constant the attacker cannot arrange at a target of their choosing. A probe therefore discloses one equality bit about a chosen address rather than its contents. Once a merge does occur the word at the target reaches pool metadata, where a later available_bytes comparison makes it recoverable by binary search over memory_size; that chain is why confidentiality is not VC:N either.

Patches

Fixed in ThreadX 6.5.2.202603.

The header-inclusive size is computed with TXM_MODULE_MANAGER_UTIL_MATH_ADD_ULONG into a local allocation_size, which is what _txe_byte_allocate is given. The check runs after the output pointer is set to TX_NULL and before the protection mutex is taken, so a refused request returns TXM_MODULE_MATH_OVERFLOW and leaves the object pool, the module’s allocated object list and its list count exactly as they were.

Regression coverage models the byte pool behind the manager, applying the two bounds _txe_byte_allocate applies and handing 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 expectations fail against the unfixed implementation: two report the 8 and 12 bytes written past the end of the block, the rest report pool and list state a refused request must not have touched. The cases cover the smallest overflowing size, sizes wrapping to 1 and to 8, ULONG_MAX, the largest non-overflowing size — which must reach the pool carrying its full untruncated total and be refused there — a zero-length object, every ThreadX control-block size with a release round trip, pool-size and no-memory refusals, a null destination, and both size classes with no object pool created. Coverage of the changed function is 100% line, branch and call.

The documented return values for txm_module_object_allocate gain TXM_MODULE_MATH_OVERFLOW, along with TX_SIZE_ERROR and TXM_MODULE_INVALID_MEMORY, which the service already produced and which were undocumented.

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