RC RANDOM CHAOS

The 64KB segment where every overflow rewrote a free list pointer

Win16's Local Heap overflow defines the metadata corruption class that still drives modern browser and kernel exploitation in 2026.

· 6 min read

Win16 ran on a segmented memory model. 16-bit selectors, near and far pointers, no virtual memory protection, no per-process address space. Every application shared a single linear address space mediated by the Global Heap and a per-module Local Heap. The Local Heap lived inside a single 64KB data segment. The Global Heap was managed through GlobalAlloc and a movable-handle abstraction. There was no NX bit. There was no ASLR. There was no stack canary. There was no SMEP. Every allocation was a candidate for adjacent corruption, and every corruption was a candidate for control flow hijack.

The primitive worth studying is the Local Heap overflow. LocalAlloc returned a handle. LocalLock dereferenced it to a near pointer inside the data segment. The heap manager maintained a doubly linked free list inline with the allocations - header, size, prev, next, then user data. The header sat immediately before the returned pointer. The free list pointers sat immediately after the user data for free chunks, or were absent for allocated chunks adjacent to free ones. A write past the end of an allocated buffer landed in the next chunk’s header. That header was metadata the allocator trusted.

This is the same shape as every unlink-style heap exploit that followed. dlmalloc unlink. Windows lookaside list corruption. glibc tcache poisoning. The Win16 Local Heap was the first widely deployed allocator where a linear out-of-bounds write into adjacent metadata produced an arbitrary-write primitive on the next LocalFree or LocalRealloc call. The mechanism is a write-what-where derived from attacker control of the prev and next fields. The allocator, executing LocalFree, walks the linked list, unlinks the chunk by writing prev->next and next->prev. If prev and next are attacker controlled, the unlink is the arbitrary write. The technique was not named in 1992. It was named ten years later when Solar Designer described it against System V malloc. The bug class predates the literature.

Why this matters now. A modern red teamer chaining a use-after-free in a renderer to a sandbox escape is operating on the same abstract primitive. Allocator metadata is in-band. The allocator trusts that metadata. Linear or temporal corruption of that metadata produces a controlled write during a subsequent allocator state transition. The differences between 1992 and 2026 are mitigation depth, not bug class. Win16 had no mitigations. Modern Windows has Low Fragmentation Heap, Segment Heap, heap entry encoding with a per-heap random key XORed into the header, guard pages, and CFG. Each was added in response to a specific exploitation technique. Each is a delta on the Win16 baseline.

Read the Win16 baseline first and the modern mitigations make sense as a sequence. Heap entry encoding exists because direct header overwrites worked unmodified through Windows XP SP1. SafeUnlink in Windows XP SP2 added checks that next->prev == chunk and prev->next == chunk before performing the unlink write. That check killed the naive technique. The response was the lookaside list overwrite, which bypassed SafeUnlink because lookaside operations did not validate. Microsoft hardened lookaside in Vista. The response was the Low Fragmentation Heap subsegment manipulation. The arms race is visible only if the starting position is visible. The starting position is Win16.

The Global Heap added a second primitive worth understanding. GlobalAlloc with GMEM_MOVEABLE returned a handle, not a pointer. The handle indexed into a master table the kernel maintained. GlobalLock incremented a lock count and returned the current linear address. The block could move when unlocked. The handle table itself was a fixed structure with predictable layout. Corruption of the handle table entry redirected every subsequent GlobalLock on that handle to an attacker chosen address. This is a type confusion in everything but name. The handle is a typed reference. The corruption changes what the type resolves to. Modern equivalents are V8 Map corruption, .NET MethodTable overwrites, and Objective-C isa pointer manipulation. The mechanism is the same. A trusted indirection is poisoned and every subsequent dereference yields attacker controlled data treated as a legitimate object.

Win16 had no separation between code and data segments at the hardware level for many real-mode configurations. Standard mode and 386 enhanced mode used protected mode segment descriptors, but the per-application data segment was both writable and reachable from code segment execution paths through far calls and far jumps. An attacker who corrupted a function pointer stored in the Local Heap - and many Windows 3.1 applications stored window procedure pointers, callback pointers, and dispatch tables inside Local Heap allocations - redirected execution into attacker controlled data without violating any segment policy. This is the original write-what-where to code execution pipeline. Modern equivalents require defeating DEP through ROP or JIT spraying. The Win16 version required defeating nothing.

What this produces for a 2026 red teamer is a clean mental model for triage. When a fuzzer produces a crash in a modern target, the question is which class of allocator state the corruption sits in. Header. Inline metadata. Free list pointer. Bucket index. Subsegment header. The Win16 taxonomy maps forward. Each modern allocator structure is a hardened version of a structure that existed in 1992 with no protection. The exploitation path is the same shape - corrupt metadata, trigger an allocator state transition, harvest the resulting primitive - with additional steps inserted to defeat each mitigation layer added since.

What this produces for an EDR engineer is different. There is no Win16 telemetry to recover. The lesson is what telemetry would have caught the original bug class if it had existed. Heap header validation failures. Free list integrity check failures. Allocator state machine violations. These are signals Windows now emits through ETW Microsoft-Windows-Kernel-Memory and through HeapEnableTerminationOnCorruption when the heap manager detects corruption. The events fire on RtlReportCriticalFailure with code STATUS_HEAP_CORRUPTION. Sysmon does not collect these by default. Defender for Endpoint surfaces them under specific exploitation categories. The signal exists. The collection often does not. Mapping a modern heap corruption alert back to the Win16 primitive that defines the bug class is the analyst skill that produces correct triage in under sixty seconds.

The attack chain implication is concrete. When initial access is browser exploitation - MITRE T1189 drive-by compromise followed by T1203 exploitation for client execution - the renderer side of the chain almost always pivots through a heap-shaped primitive. V8 ArrayBuffer backing store corruption. Blink GC heap UAF. PartitionAlloc bucket manipulation. Each one resolves to a Win16-era operation performed against a modern allocator. The attacker is not inventing primitives. The attacker is selecting which 1992 primitive maps cleanest onto the 2026 allocator’s metadata model and chaining the mitigation bypasses required to reach it.

The residual reality is that allocator hardening reduces the success rate of corruption-to-control conversion. It does not eliminate the underlying class. Memory unsafe languages produce allocator metadata corruption. The mitigations move the bar. Rust, Swift, and managed runtimes move the class itself. The industry shift toward memory safe languages - Microsoft’s stated direction for new Windows components, Google’s Rust adoption in Chrome and Android, the CISA push for memory safety roadmaps - is the only intervention that ends the lineage. Until then, every new CVE in a C or C++ heap allocator is a descendant of the Win16 Local Heap overflow. The bug has a thirty-four-year pedigree. The mitigation layer determines how much work the exploit takes, not whether the exploit is possible.

Win16 is not running in production. The bug class it defined is running everywhere C and C++ are running. The primitive is the lesson. Hardening is the historical record of attempts to contain it. Memory safety is the only exit.

Share

Keep Reading

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.