GHSA-rrg6-24r9-wqpm: Module queue front-send validates a word count as a byte count
GHSA ID |
|
|---|---|
CVE ID |
CVE‑2026‑95111 |
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
A memory-protected ThreadX module’s queue request reaches the Module Manager’s dispatch layer, which decides how much of the module’s buffer the privileged queue copy may touch and hands that extent to the module port’s range check. A queue’s tx_queue_message_size is a count of ULONG`s and the copy moves that many words, so the extent is `message_size * sizeof(ULONG) bytes.
The front-send dispatcher passed the word count itself. tx_queue_send and tx_queue_receive, either side of it in the same header, have always multiplied.
A source buffer ending message_size bytes into memory the module may read was therefore accepted, and _txe_queue_front_send then read message_size * sizeof(ULONG) bytes from it in privileged mode — 3 * message_size bytes past the end of what the module is allowed to read. The words are written into the queue’s message area, so a module that can receive from that queue reads them back.
Details
TX_QUEUE.tx_queue_message_size is a count of ULONG words. TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ takes a count of bytes. _txm_module_manager_tx_queue_front_send_dispatch passed the first where the second was required:
queue_ptr = (TX_QUEUE *) param_0;
if (!TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, param_1, queue_ptr -> tx_queue_message_size))
return(TXM_MODULE_INVALID_MEMORY);
tx_queue_message_size is used as an extent in exactly three places in txm_module_manager_dispatch.h — the send, front-send and receive dispatchers — and front-send was the only one of the three missing the multiplication.
sizeof(ULONG) is 4 on every module port in the tree, including the two AArch64 ports, whose tx_port.h defines ULONG as unsigned int. The under-check is a factor of four everywhere and there is no port on which the two units coincide.
The execution path requires nothing but the module’s own ordinary capabilities:
1\. The module creates a queue with message size N words, N ⇐ TX_QUEUE_MESSAGE_MAX_SIZE.
2\. It computes a source address S leaving exactly N bytes between S and the end of one of the regions it may read — its writable data, a shared region registered to it through txm_module_manager_external_memory_enable, or its own read-only image.
3\. tx_queue_front_send(queue, S, TX_NO_WAIT) passes the object check, and the buffer check is satisfied because N bytes really do remain.
4\. _tx_queue_front_send copies N ULONG`s from `S inside a TX_DISABLE window. Bytes S + N through S + 4N - 1 lie past the end of the module’s region.
5\. The module calls tx_queue_receive on the same queue and reads them.
Impact
A memory-protected module obtains the contents of up to 3 * TX_QUEUE_MESSAGE_MAX_SIZE bytes immediately following any region it is permitted to read: 48 bytes at the ThreadX default and 96 where an integrator has raised the limit to 32. On the common layout, where a module’s data region is one allocation in the memory area the application handed the Module Manager, those bytes belong to the next allocation in that area — another module’s data, or the manager’s object pool and therefore kernel control blocks. Where the following bytes belong to the same module, the impact reduces to an intra-module over-read and discloses nothing.
What bounds the disclosure is that the buffer cannot be aimed. It has to pass the same check to be accepted at all, so the read window is anchored to the end of a region the module can already read, and the module controls only how far past that boundary it reaches — from nothing up to 3 * message_size bytes. There is no address of the module’s choosing that the window can be made to cover.
Where the adjacent window is not readable in privileged mode, the privileged read faults instead, inside a kernel critical section with the queue mid-update. The module port’s memory fault handler terminates the requesting module’s thread and notifies the application; the system continues.
A module loaded without TXM_MODULE_MEMORY_PROTECTION is unaffected: the check is inside the dispatcher’s memory-protection block, and such a module reaches the kernel service with no range check either before or after this fix.
Affected configurations
ThreadX Modules with memory protection enabled and TXM_QUEUE_FRONT_SEND_CALL_NOT_USED not defined, which is the default.
The dispatcher passes the word count in every tag from v6.0.1_rel onwards, and the portable range macros have acted on the size they are given for that whole range, so the defect has been present since the first release that shipped the Modules component. v6.0_rel has no Module Manager.
Reachability is not uniform across module ports, because the ports differ in what they do with the extent they are handed. Of the 31 module port and toolchain combinations in the tree, 24 act on it directly — Cortex-M0+, M23, M3, M33, M4 and M7 across their toolchains, Cortex-R4 and RXv2 — and on those the wrong unit was consumed as a byte count and the over-read followed. The three Cortex-A7 combinations defined their data range check to take the pointer alone, so no extent reached them at all through a module’s data; that is the subject of GHSA-fvp6-37q5-c648, and its fix is what brings those ports into line and makes this defect reachable on them as it is elsewhere.
Severity
CVSS:4.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.
AC:L holds without qualification: the module creates its own queue and chooses the message size, computes the source address from the end of a region whose bounds it is linked against, and front-sends and receives. Nothing has to be guessed, no identifier has to be forged, and no other component has to be involved.
VC:L rather than VC:H. The disclosure is real content rather than an oracle — the bytes themselves come back through the queue, repeatably — but the module cannot choose the address, and the amount is bounded by 3 * TX_QUEUE_MESSAGE_MAX_SIZE bytes at a fixed offset from a boundary the module already sits against. That is CVSS v4.0’s VC:L in all three of its clauses at once: some restricted information is obtained, the attacker does not control what, and the amount is limited. Read instead as a serious direct disclosure, VC:H gives 6.9 — still medium.
VI:N: the only privileged write is into the queue’s own storage and accounting, and a module that can front-send to a queue is already entitled to put what it likes in it.
VA:L rather than VA:H, on two properties of the affected ports. The Cortex-M module ports enable the MPU with MPU_CTRL = 5, which is ENABLE together with PRIVDEFENA, so a privileged access falls back to the default memory map where no MPU region matches — a window a few tens of bytes past a region carved out of the device’s RAM resolves and returns data rather than faulting. That was measured on an MPU-enabled Cortex-M3 target: the privileged read past the module’s data end returned data, the service reported success, and bytes from beyond the boundary were recovered from the module’s own queue message buffer. Where it does fault, _txm_module_manager_memory_fault_handler terminates the requesting thread and the system continues, so repeating the attack cannot deny service outright.
Patches
Fixed in ThreadX 6.5.2.202603, with the multiplication the other two queue services already do:
if (!TXM_MODULE_MANAGER_PARAM_CHECK_BUFFER_READ(module_instance, param_1, sizeof(ULONG)*queue_ptr -> tx_queue_message_size))
The expression is now byte-for-byte the send dispatcher’s, so the three queue buffer services validate identically. The product cannot overflow: both operands are unsigned and _txe_queue_create refuses a message size above TX_QUEUE_MESSAGE_MAX_SIZE.
Regression coverage is the first test in the tree to drive a Module Manager dispatcher, and it measures the validated extent rather than sampling either side of it: for a given message size it anchors the buffer at the end of one of the module’s regions, walks it backwards a byte at a time, and reports the smallest amount of room the dispatcher accepts. That number is the extent, and it must equal message_size * sizeof(ULONG) for all three services, in the module’s data, in a shared region registered to it, and — for the two services that read the buffer — in the module’s read-only image. A receive destination in the read-only image is refused at every extent. The suite also covers the boundary and one-byte-short cases, an extent that wraps the address space, a queue aimed into the module or at nothing, a refusal that leaves the whole TX_QUEUE image byte-identical without reaching the service, the null-source pointer that the dispatch layer accepts and the kernel service refuses with TX_PTR_ERROR, and an unprotected module whose requests must remain unaffected.
Compiled against the unfixed dispatch header the test fails with 22 expectations, every one of them naming front-send, and send and receive pass identically in both runs. The three dispatchers under test reach 100% line, branch-outcome and call coverage. The change is warning-neutral at -Wall -Wextra on all seven GNU 32-bit module ports.
Modules that do not use tx_queue_front_send, and integrators who define TXM_QUEUE_FRONT_SEND_CALL_NOT_USED, are not exposed. There is no configuration workaround for a module that needs the service.
Workarounds
Defining TXM_QUEUE_FRONT_SEND_CALL_NOT_USED removes the affected dispatcher, at the cost of tx_queue_front_send from modules. Otherwise none that preserve the feature: the path is reachable only from a loaded module, which is the case module memory protection exists to handle.
Credits
Found and fixed by Frédéric Desbiens (@fdesbiens) during a maintainer review of the Module Manager’s dispatch layer. 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.
Independently discovered and reported by SounLabs (@SounLabs), whose demonstration on an MPU-enabled Cortex-M3 target established that the privileged read past the module’s data end returns data rather than faulting, and that the bytes reach the module.