Skip to content

Deleting a user-mode module thread leaks its privileged kernel stack

Moderate
fdesbiens published GHSA-9hhf-6357-hpgq Oct 2, 2026

Package

ThreadX (Eclipse ThreadX)

Affected versions

>= 6.0.1, <= 6.5.1.202602a

Patched versions

6.5.2.202603

Description

Summary

One logical thread of a user-mode ThreadX module costs the Module Manager's shared object pool two allocations, not one. The module asks for a TX_THREAD control block through txm_module_object_allocate; _txm_module_manager_thread_create then takes a second allocation of TXM_MODULE_KERNEL_STACK_SIZE bytes for the privileged kernel stack that thread's system calls run on, and records it in the control block. Both come from _txm_module_manager_object_pool and both are linked onto the module's allocation list.

The Module Manager's thread-delete dispatcher released only the first. The kernel stack stayed allocated until the whole module was stopped, so a module that creates, terminates and deletes threads in a loop — every state transition valid, every service returning TX_SUCCESS — removed a fixed block from a finite pool shared by the manager and by every other loaded module, on every cycle, for as long as it stayed loaded. Once the pool is exhausted, no memory-protected module can create any kernel object and no user-mode module thread can be created anywhere in the system.

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

Details

A module loaded with TXM_MODULE_USER_MODE runs its own code unprivileged, and the privileged side of each of its system calls needs a stack of its own. _txm_module_manager_thread_create allocates that stack per thread, out of the object pool, and stores the address in the new thread's tx_thread_module_kernel_stack_start. The module never requests it and cannot see it; it is the manager's allocation, made on the module's behalf.

On a successful tx_thread_delete the dispatcher called _txe_thread_delete and then released param_0, the control block, and nothing else. Nothing else in the system reclaims the kernel stack:

  • _tx_thread_delete invalidates the thread ID and unlinks the thread from the created list, and does nothing about module memory.
  • TX_THREAD_DELETE_PORT_COMPLETION, the one hook that could, defaults to empty and is overridden only by the Linux, Win32 and Win64 host ports. No module port overrides it.
  • The only sweep is in _txm_module_manager_stop, which releases every allocation still on the module's list — but only when the entire module is stopped.

So the block remained linked on txm_module_instance_object_list_head and counted in txm_module_instance_object_list_count, unavailable to any caller, until the module was torn down. This is not allocator caching: the byte pool considers the block in use, and a replacement thread gets a fresh allocation.

The per-cycle cost is a compile-time constant of the port — 512, 768 or 1024 bytes — plus the manager's allocation header and the byte pool's own per-block overhead. On a small pool that is enough to matter quickly: on the 12,280-byte pool the regression test uses, the measured cost is 792 bytes per cycle and fifteen cycles exhaust it.

The creation path is not at fault. _txm_module_manager_thread_create has no failure return after the kernel-stack allocation succeeds, so no error path leaks the stack at creation time. The defect is entirely in the deletion path.

Impact

A module with no privileges beyond the ability to manage its own threads can exhaust a finite privileged resource that the whole system shares, while its own accounting looks correct throughout — every service returns success, and it holds no live thread and no live allocation of its own.

Once the pool is exhausted the loss is total for that resource rather than gradual. All eight object-create dispatchers refuse a memory-protected module's request, and any application logic that needs to create a kernel object in order to respond to the condition cannot itself run. Depending on the integrator's error handling this disables services, prevents recovery tasks from starting, wedges module restart logic, or fails the system's availability outright.

Because every cycle uses valid ThreadX state transitions, neither the error-checking layer nor MPU or MMU isolation prevents it. An accidental dynamic-thread workload exhausts the pool by the same mechanism over a longer period.

Recovery requires stopping the offending module, which does reclaim the leaked blocks and does not itself need the exhausted pool. The condition therefore persists after the module stops attacking, until an administrative act by another component clears it.

Affected configurations

ThreadX Modules with a Module Manager object pool, and a module loaded with TXM_MODULE_USER_MODE.

Memory protection requires user mode: both load paths refuse a module that asks for TXM_MODULE_MEMORY_PROTECTION without TXM_MODULE_USER_MODE. Every memory-protected module is therefore a user-mode module, and every one of its threads holds a kernel stack. A user-mode module without memory protection is affected in the same way.

All 31 memory-protected module port combinations are affected. Every one defines TXM_MODULE_KERNEL_STACK_SIZE — 768 bytes on 25 of them, 512 on five and 1024 on one — so the leaked block exists everywhere and only its size varies.

Present since v6.0.1_rel, the release that introduced user mode, the per-thread kernel stack and the dispatch table together. The thread-delete dispatcher was byte-identical in that release and in every release since, up to and including v6.5.1.202602a_rel. v6.0_rel predates user mode and is unaffected.

Severity

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

The actor is a module running on the device, using the ordinary thread-management system calls it is entitled to make: correctly ordered create, terminate and delete calls in a loop, with no race, no allocator shaping beyond repetition, no malformed argument and no configuration beyond a normal supported user-mode Modules setup.

Nothing is disclosed and nothing is corrupted. The only observable is the iteration at which allocation starts failing, which a module can already measure through txm_module_object_allocate. The leaked allocation stays correctly linked with an intact header and an accurate count, and the module-stop sweep releases it properly.

VA:H because the pool is finite and shared, every cycle consumes a fixed block, the module can run arbitrarily many cycles, and the loss is a total refusal of the resource rather than a degradation that persists after the attack completes.

Subsequent-system metrics are N: the impact stays inside one ThreadX instance — one kernel, one address space, one object pool, one Module Manager. A second module is a component of the vulnerable system rather than a subsequent system, because the memory-protection boundary between modules partitions memory, not security authority, and a denial reaching another module reaches it through a resource VA already records. Consequences beyond the RTOS instance depend on what a particular product puts in a second module and belong in the Environmental metrics.

Patches

Fixed in ThreadX 6.5.2.202603.

On a successful delete the dispatcher releases the kernel stack through _txm_module_manager_object_deallocate before releasing the control block. Two conditions gate the release: the calling module carries TXM_MODULE_USER_MODE, and tx_thread_module_kernel_stack_start is non-null. The property flag mirrors the allocation side, so the two ends of the thread's life agree about when there is a kernel stack. The null test makes the release exact rather than an inference from how the module was loaded: a user-mode module can be given the address of a thread whose kernel stack the manager never allocated, and deleting such a thread would otherwise ask for memory back that was never handed out. Both thread create paths clear the entire control block before filling it in, so a thread with no manager-allocated kernel stack holds TX_NULL in that field.

Reading the field after the delete is sound: _tx_thread_delete leaves the control block intact and no module port defines a completion hook that would clear it. Releasing the control block last means the pointer is read while the control block is still the module's. Both releases are attempted whatever the other returns, and the first failure is what the module is told — the kernel thread is already deleted when either is attempted, so neither allocation can still be in use, and skipping one because the other was refused would strand an allocation. Releasing through _txm_module_manager_object_deallocate keeps the existing ownership check, so a module cannot release another module's kernel stack this way, and that function unlinks the allocation and decrements the count before releasing the memory, so the module-stop sweep cannot then double free it.

No other Module Manager delete dispatcher has an equivalent defect. _txm_module_manager_object_allocate is called from exactly two places in the manager — the txm_module_object_allocate dispatcher, which is the module's own request, and this kernel-stack site — so no other object type has a manager-owned satellite allocation. The module's own code and data areas come from a different pool, _txm_module_manager_byte_pool, and are released at unload.

A host test asserts the invariant this defect broke: deleting the thread must return the pool's available bytes and the module's allocation-list count to exactly what they were. The assertion is an equality rather than a bound, because a bound would pass while one of the two allocations was left behind on every cycle. It then runs 200 create-and-delete cycles against a pool that a leak of one kernel stack per cycle exhausts well inside that count. Against the unfixed dispatcher the test fails in eight places and exhausts the pool at cycle 22. It needed a host harness for the Module Manager, which the test tree did not have — the manager is not in the ThreadX library the suite links, and its control blocks come from a module port rather than a base port. That harness ships with the fix and is reusable by any later test of the manager.

The txm_module_manager_object_pool_create reference gains the sizing fact it was missing. The manual described the object pool as memory the modules allocate their objects from, and said nowhere an integrator sizing a pool would look that the manager also draws on it for a per-thread kernel stack, so a pool sized for control blocks alone is undersized by TXM_MODULE_KERNEL_STACK_SIZE per module thread.

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) during a review of the Module Manager's object lifetime paths. 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.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v4 base metrics

Exploitability Metrics
Attack Vector Local
Attack Complexity Low
Attack Requirements None
Privileges Required Low
User interaction None
Vulnerable System Impact Metrics
Confidentiality None
Integrity None
Availability High
Subsequent System Impact Metrics
Confidentiality None
Integrity None
Availability None

CVSS v4 base metrics

Exploitability Metrics
Attack Vector: This metric reflects the context by which vulnerability exploitation is possible. This metric value (and consequently the resulting severity) will be larger the more remote (logically, and physically) an attacker can be in order to exploit the vulnerable system. The assumption is that the number of potential attackers for a vulnerability that could be exploited from across a network is larger than the number of potential attackers that could exploit a vulnerability requiring physical access to a device, and therefore warrants a greater severity.
Attack Complexity: This metric captures measurable actions that must be taken by the attacker to actively evade or circumvent existing built-in security-enhancing conditions in order to obtain a working exploit. These are conditions whose primary purpose is to increase security and/or increase exploit engineering complexity. A vulnerability exploitable without a target-specific variable has a lower complexity than a vulnerability that would require non-trivial customization. This metric is meant to capture security mechanisms utilized by the vulnerable system.
Attack Requirements: This metric captures the prerequisite deployment and execution conditions or variables of the vulnerable system that enable the attack. These differ from security-enhancing techniques/technologies (ref Attack Complexity) as the primary purpose of these conditions is not to explicitly mitigate attacks, but rather, emerge naturally as a consequence of the deployment and execution of the vulnerable system.
Privileges Required: This metric describes the level of privileges an attacker must possess prior to successfully exploiting the vulnerability. The method by which the attacker obtains privileged credentials prior to the attack (e.g., free trial accounts), is outside the scope of this metric. Generally, self-service provisioned accounts do not constitute a privilege requirement if the attacker can grant themselves privileges as part of the attack.
User interaction: This metric captures the requirement for a human user, other than the attacker, to participate in the successful compromise of the vulnerable system. This metric determines whether the vulnerability can be exploited solely at the will of the attacker, or whether a separate user (or user-initiated process) must participate in some manner.
Vulnerable System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the VULNERABLE SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the VULNERABLE SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the VULNERABLE SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
Subsequent System Impact Metrics
Confidentiality: This metric measures the impact to the confidentiality of the information managed by the SUBSEQUENT SYSTEM due to a successfully exploited vulnerability. Confidentiality refers to limiting information access and disclosure to only authorized users, as well as preventing access by, or disclosure to, unauthorized ones.
Integrity: This metric measures the impact to integrity of a successfully exploited vulnerability. Integrity refers to the trustworthiness and veracity of information. Integrity of the SUBSEQUENT SYSTEM is impacted when an attacker makes unauthorized modification of system data. Integrity is also impacted when a system user can repudiate critical actions taken in the context of the system (e.g. due to insufficient logging).
Availability: This metric measures the impact to the availability of the SUBSEQUENT SYSTEM resulting from a successfully exploited vulnerability. While the Confidentiality and Integrity impact metrics apply to the loss of confidentiality or integrity of data (e.g., information, files) used by the system, this metric refers to the loss of availability of the impacted system itself, such as a networked service (e.g., web, database, email). Since availability refers to the accessibility of information resources, attacks that consume network bandwidth, processor cycles, or disk space all impact the availability of a system.
CVSS:4.0/AV:L/AC:L/AT:N/PR:L/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

CVE ID

CVE-2026-95108

Weaknesses

Uncontrolled Resource Consumption

The product does not properly control the allocation and maintenance of a limited resource. Learn more on MITRE.

Missing Release of Resource after Effective Lifetime

The product does not release a resource after its effective lifetime has ended, i.e., after the resource is no longer needed. Learn more on MITRE.

Credits