September 2026 · ZDI-26-647 / CVE-2026-59346 (CVSSv3 9.3 vendor / 7.5 ZDI) · Fixed in VMware Workstation/Fusion 26H1u1 (VMSA-2026-0007)
Broadcom shipped a fix for a VMXNET3 TSO overflow last year, CVE-2025-41236. The patch adds four bounds checks to the segmentation function. All four bound individual descriptor fields against a fixed ceiling, and none of them go anywhere near the multiply that actually decides how large the allocation is, so the same function still wraps a 32-bit product and still writes guest-controlled bytes off the end of a host heap buffer. I was working through emulated device models at the time, auditing the paths where a guest hands structured input to the host, which is how I came to be reading the VMXNET3 TSO segmentation code in the first place. The four bounds checks sitting in it are the CVE-2025-41236 patch, and it only takes reading what they actually compare to see that none of them touches the value the allocation is computed from.
The short version: set MSS to 1 and a payload length big enough to push the segment count into the high hundreds of thousands, keep per_seg_size under the 9,216 ceiling so every check passes, and the product seg_count * per_seg_size runs past 232. Mem_Malloc gets the truncated low 32 bits, the segmentation loop writes the untruncated amount, and roughly 4.3 GB of packet headers go into a 6 KB buffer. Tuned carefully it stops being a crash and becomes a silent write over adjacent glibc chunk metadata with content I choose.
VMXNET3 is VMware’s paravirtualised network adapter and the default NIC for modern guests. When a guest sends a TCP Segmentation Offload packet, vmware-vmx on the host splits it into MSS-sized segments: it reads header length, MSS and payload length out of the guest TX ring, computes a segment count, allocates one buffer to hold every segment, then copies packet headers into each slot. Everything it reads comes from descriptors the guest wrote. A guest administrator can craft raw TX descriptors directly, skipping whatever sanity the in-guest driver would have applied, and ring the doorbell register at BAR0+0x600 to kick off host-side processing. That was the entry point for CVE-2025-41236 and it is the entry point here.
The 2025 patch
The four checks run after vmware-vmx reads the descriptor fields and before it allocates. In pseudocode:
if (MSS > 9216) goto reject;
if (per_seg_size > 9216) goto reject;
if (MSS + per_seg_size > 9216) goto reject;
Fail any of them and the function logs NetPkt: TxOffload: expectations not satisfied and returns NULL. In the disassembly, with offsets from Workstation 25.0.1 build 25219725 which will move in other builds:
; Check 1: MSS > 9216?
cmp $0x2400, %r12d ; r12d = MSS
seta %r8b
; Check 2: per_seg_size > 9216?
cmp $0x2400, %r13d ; r13d = per_seg_size
ja reject
; Check 3: deferred MSS check
test %r8b, %r8b
jne reject
; Check 4: MSS + per_seg_size > 9216?
lea (%r12,%r13,1), %edx
cmp $0x2400, %edx
ja reject
Every one of those compares a single field, or the sum of two fields, against 9,216. The allocation size is neither of those things. It is seg_count multiplied by per_seg_size, and seg_count never gets looked at anywhere in the patch.
Bounding two multiplicands separately tells you very little about their product, which is the whole problem. seg_count is computed as ceil(payload / MSS), so driving MSS down drives seg_count up without touching any value the patch bounds. Keep per_seg_size below 9,216 and all four checks pass while the product runs past 32 bits. The 2025 fix closed the specific trigger in the original report and left the arithmetic exactly where it was.
Getting the multiply to wrap
Past the checks, the function rounds per_seg_size up to an 8-byte boundary and multiplies:
add $0x11, %edx ; header_total + 17
and $0xfffffff8, %edx ; 8-byte align -> per_seg_size
mov 0x24(%rsp), %edi ; edi = seg_count
imul %edx, %edi ; edi = seg_count * per_seg_size (32-BIT)
call Mem_Malloc ; allocates the truncated value
The imul is operating on %edi and %edx, so the top 32 bits of the result are gone before Mem_Malloc ever sees them. The segmentation loop afterwards has no idea the allocation was truncated. It iterates seg_count times and writes per_seg_size bytes of packet headers per iteration, because that is what it was told to do.
Some concrete numbers: MSS of 1, and a payload length chosen to give a seg_count of 688,297. After alignment per_seg_size comes out at 6,240, which is under the cap, so all four checks pass. 688,297 * 6,240 is 4,294,973,280, and in 32 bits that is 5,984. Mem_Malloc allocates about 6 KB. The loop then writes 6,222 bytes per segment (Ethernet, IPv6, extension headers, TCP) across all 688,297 iterations, which comes to something like 4.3 GB into a 6 KB buffer.
The bytes come from the guest’s own packet headers, fully specified in the TX descriptor chain, laid out per segment like this:
Bytes 0-9: internal per-segment header (10 bytes)
Bytes 10-23: Ethernet header (14 bytes)
Bytes 24-63: IPv6 header (40 bytes)
Bytes 64-6207: 3x IPv6 Destination Options (3 * 2,048 = 6,144 bytes)
Bytes 6208-6231: TCP header (24 bytes)
Total per segment: 6,222 bytes written, per_seg_size: 6,240
Landing on chunk metadata
The overflow length is tunable, because the wrap residue is a function of the input parameters and I can solve for whichever residue I want. The PoC does that solve up front: given a target residue it searches for an MSS and payload length whose product, taken mod 232, lands on the value I asked for, while keeping per_seg_size under the 9,216 ceiling so all four checks still pass. Change the target residue and the overflow length changes with it. Nothing else about the packet has to move.
The default parameters pick the residue that maximises the overflow, which runs the writes through adjacent chunks into unmapped memory and kills vmware-vmx with SIGSEGV. That gets you a host crash and very little else. The useful version is far smaller.
Pick parameters where (seg_count * per_seg_size) mod 2^32 lands roughly 256 bytes below per_seg_size. A residue of 5,984 makes Mem_Malloc(5984) hand back a glibc chunk of 6,000 bytes (0x1770), with the buffer starting at chunk_addr + 16 and 5,984 bytes of usable space in it. The first segment’s SgCopy write puts 6,222 bytes into that, so it runs 238 bytes past the end, and what sits 238 bytes past the end is the next chunk’s header:
Buffer + 5984: N+1 chunk prev_size (8 bytes)
Buffer + 5992: N+1 chunk size (8 bytes)
Both of those offsets fall inside the IPv6 Destination Options padding region, and Destination Options padding is entirely guest-specified, so I get to write arbitrary 8-byte values into prev_size and size.
That corruption is silent, which is the property worth having. A size value with PREV_INUSE set and a plausibly aligned prev_size will pass glibc’s integrity checks, so free() processes the chunk and nothing aborts. The corrupted header then sits there until the adjacent chunk is freed or reallocated and the allocator consumes it, and from that point the usual glibc primitives are available: unsafe unlink for an arbitrary write, tcache poisoning to get an allocation at an address I choose, FSOP through a corrupted FILE structure in the vmware-vmx process.
Content and offset are independent of each other. The Destination Options padding decides which bytes land on the metadata fields, the modular arithmetic decides how far past the buffer the write goes, and I can pick them separately.
Impact
With untuned parameters this is a host crash. vmware-vmx takes a SIGSEGV once the writes reach unmapped memory, and vmware.log puts the fault on the vcpu-0 thread in the VMXNET3 TX path:
PANIC: Unexpected signal: 11.
PANIC: Loop on signal 6.
SymBacktrace: realloc -> glibc internal -> abort
Thread: vcpu-0 (VMXNET3 TX processing)
A crash proof of concept is at github.com/0xCyberstan/CVE-2026-59346-POC. It builds as a guest kernel module, and loading it is the whole trigger:
cd poc/
make # produces vmxnet3_tso_trigger_v8.ko
insmod vmxnet3_tso_trigger_v8.ko # host vmware-vmx dies, guest powers off
The module writes TX ring descriptors directly rather than going through the in-guest driver’s TSO logic. Entry 0 is the SOP descriptor, carrying a valid DMA address, the full header length and MSS=1, entries 1 to N carry the payload, and then it rings the doorbell at BAR0+0x600. It ships with the maximum-overflow parameters, and I have not published the residue that produces the silent metadata corruption described above.
With tuned parameters it is deterministic, silent corruption of glibc heap metadata, with content and offsets I control, inside the host process. That is a guest-to-host code execution primitive.
It needs administrative privileges in the guest and a VMXNET3 adapter, which is the default on modern VMware guests. Workstation, Fusion, Player and ESXi all share the same TSO segmentation code in vmware-vmx and are all affected. ESXi runs the VMX process inside a sandbox, so there is less to reach afterwards there, though the overflow itself happens the same way.
The fix
The fix
The truncation is the whole bug, and there are two reasonable ways to close it: do the multiply in 64-bit arithmetic, or check the product against a sane allocation ceiling before calling Mem_Malloc. Field-level bounds checks are worth keeping as input sanity. They just cannot do this particular job, because the dangerous value is a product and the checks only ever see the factors.
I guessed Broadcom would bound the product, since that is the smaller change to a function that already has four checks sitting in front of the allocation. I was wrong. They widened the multiply, in the same function, now at PIE+0x55f630.
25219725: imul %edx, %edi ; 32-bit product, truncates
25688693: imul %rax, %rdi ; 64-bit product, does not
It is more thorough than that one instruction suggests. Both factors are zero-extended as they are loaded, the round-up to a multiple of eight moved from a 32-bit add and mask to 64-bit ones, and the stack slots carrying the sizes down to the copy loop grew to 64 bits, so the widening follows the value instead of stopping at the multiply. The allocation is now the real product, and you can no longer make the host allocate a small buffer and write a large one. A descriptor declaring a nonzero MSS and a zero payload length is rejected up front now as well, which closes the degenerate seg_count case next door.
What they did not add is a ceiling. seg_count is still unbounded, so the size the guest asks for is still the size the host tries to allocate: my parameters now compute 4,294,973,280 exactly, which is either a 4.3 GB allocation the loop fills to the last byte or a NULL from malloc and a panic. Correct, but duller than I would expect. Worth checking if you run Workstation: 26.0.0, build 25388281, still has the 32-bit multiply.
Fixed in VMware Workstation and Fusion 26H1u1, released 3 September 2026 (VMSA-2026-0007).
Timeline
2026-04-26 – Reported to Trend Micro ZDI
2026-08-26 – ZDI notifies vendor (Broadcom)
2026-09-03 – Broadcom ships 26H1u1 (VMSA-2026-0007)
2026-09-09 – ZDI advisory published (ZDI-26-647)
