RC RANDOM CHAOS

Sandi Metz's 2016 essay is a bug-class taxonomy

The 2016 'wrong abstraction' rule read as a memory-corruption bug class: use-after-free, type confusion, double-free, from DirtyCOW to Dirty Pipe and DirtyCred.

· 7 min read
Sandi Metz's 2016 essay is a bug-class taxonomy

Sandi Metz published “The Wrong Abstraction” in January 2016. The argument was about maintenance cost: duplication is cheaper than the wrong abstraction. Pull one shared representation out of two callers that only looked similar, and every later change fights the abstraction instead of the requirement. In memory-unsafe code that same sentence describes a bug-class taxonomy. The wrong abstraction over object lifetime is a use-after-free. The wrong abstraction over object type is a type confusion. Redundant ownership of one allocation is a double-free. Each one is an exploit primitive, and each has a CVE record running from 2016 to the present.

Lifetime first. An allocation has one correct owner and one correct free point. Abstract that ownership badly, share a raw pointer across two structures that each believe they control the free, and the allocator returns the chunk while a live reference still points at it. That is the use-after-free. The freed chunk drops onto a free list. In modern glibc that list is the tcache, a per-thread singly linked LIFO with no integrity check before version 2.29. An attacker reclaims the chunk by requesting an allocation in the same size class and fills it with controlled bytes. The stale reference now reads attacker data in place of the original object. If that object held a C++ vtable pointer, the next virtual dispatch transfers control to an address the attacker chose. The primitive is a controlled call. The exploit is the heap grooming that makes the reclaim deterministic.

Type confusion is the same fault in the type system. One abstraction, two concrete types: a union, a base class reinterpreted as a derived class, a variant tag the code forgets to check. The program reads the object through the wrong layout. A field that holds a scalar in type A holds a pointer in type B. Read it as B and the result is an information leak. Write it as B and the result is an arbitrary write. V8 type confusions follow this exactly. TurboFan compiles on an assumed object shape, a path the compiler never guarded violates the assumption, and a double lands where a tagged pointer was expected. The output is an arbitrary read/write inside the heap. Microsoft has tracked type confusion as a recurring root cause across its scripting-engine and browser CVEs for a decade. The class never closes because the abstraction that conflates two types is a design decision, not a coding slip.

Double-free is redundant ownership made concrete. Two structures both believe they own the chunk, both call free, and the chunk lands on the freelist twice. The allocator then hands the same address to two distinct allocation requests. Two live objects now overlap one backing store, and writing one rewrites the other’s length or pointer fields. glibc 2.29 added a key sentinel to the tcache to catch the immediate double-free; attackers sidestep it by cycling the chunk through a different size class before the second free.

The 2016 anchor is DirtyCOW. Copy-on-write is a duplication abstraction by design. The kernel maps the same physical page into two processes read-only and promises to duplicate it the instant either side writes. CVE-2016-5195, CVSS 7.8. The bug was a race between the fault handler creating the private copy and a competing write through /proc/self/mem. A madvise(MADV_DONTNEED) call discarded the private copy after it was made and before the write resolved, so the write landed on the original read-only mapping. The duplication was not atomic. A non-privileged user wrote to root-owned files. It shipped in Android rooting chains and ran as one of the most reliable Linux privilege escalations of the decade. MITRE T1068, exploitation for privilege escalation.

DirtyCOW was patched in October 2016. The abstraction flaw was not. CVE-2022-0847, Dirty Pipe, CVSS 7.8, is the same family six years on. A pipe buffer carried a stale PIPE_BUF_FLAG_CAN_MERGE flag that reuse never cleared. The flag asserted the buffer could merge into the page cache. The page cache held a read-only file’s contents. An unprivileged write into the pipe merged into the cached page of a file the caller could only read. The same conflation drives it: a cached copy with ambiguous ownership, a write that reaches the wrong owner. No race this time, which made it more reliable than DirtyCOW.

The modern tooling sits in kernel heap exploitation. DirtyCred, presented by Zhenpeng Lin at Black Hat 2022, generalised the pattern. Free a credential structure, reclaim its slot with a second cred under attacker control, and swap an unprivileged identity for a privileged one without corrupting a single pointer. Cross-cache attacks defeat slab isolation by exhausting one cache until the allocator pulls fresh pages that overlap another cache’s objects. msg_msg, pipe_buffer, and sk_buff are the dependable grooming objects. CONFIG_SLAB_FREELIST_RANDOM and freelist pointer hardening raise the cost of the reclaim. They do not delete the class.

Userland tracks the same line. CVE-2016-7855 was a Flash use-after-free, exploited in the wild by APT28 - Fancy Bear - in targeted campaigns that year. Object lifetime was wrong, the dangling reference was reclaimed, control transferred. glibc answered the freelist abuse in steps: the tcache double-free key in 2.29, safe-linking in 2.32 that mangles the forward pointer by XORing it with the chunk address shifted right twelve bits. Each mitigation forces an extra heap leak before the primitive works. None removes the underlying condition, where a freed chunk and a live reference coexist.

Memory corruption produces almost nothing in telemetry at the moment it fires. No Sysmon event marks a chunk reclaimed from the tcache. No Windows Security event records a vtable pointer overwrite. No EDR alert category covers a type confusion inside a JIT. The corruption lives inside one process address space, below the syscall boundary every behavioural sensor watches. Heap grooming, the spray that arranges the address space before the trigger, allocates and frees objects through the program’s own legitimate APIs, so it generates the same allocation profile as normal load. Defenders never see the primitive. They see what the primitive is used to do.

For Linux kernel privilege escalation the observable is the credential transition. A process whose effective UID reaches zero with no setuid binary in its lineage, no su, no sudo, no pkexec. auditd captures the execve and the cred change when the rules are loaded. eBPF sensors, Falco and the EDR agents built on the same instrumentation, flag the privilege jump and the anomalous parent-child chain. The kernel ring buffer sometimes carries an oops or a slab corruption warning when the exploit is imperfect. That is the noisy case. A clean exploit changes credentials and leaves the corruption path with no log.

For userland the observable is the second stage. T1055, process injection. T1203, exploitation for client execution. The document handler or renderer spawns a child it has never spawned, writes into an RX page, or opens an outbound connection its process model never produced. Sysmon Event ID 1 on the anomalous child, Event ID 10 on the cross-process access, Event ID 3 on the egress. Detection always lands downstream of the bug. The interval between the corruption and the behaviour is the gap, and on a clean in-memory chain that interval can cover the whole engagement.

The industry response targets the class rather than each instance. Meta, Google, and Microsoft have moved new systems code to Rust, where the ownership the C heap left implicit becomes a compile-time constraint. CISA’s 2023 memory-safety guidance named the same root causes - use-after-free, type confusion, buffer overflow - across critical software. The bug classes that map onto Metz’s wrong abstraction are the exact set the migration removes.

The patch boundary is narrow and the residual exposure is wide. DirtyCOW closed in the October 2016 kernel updates. Dirty Pipe closed in 5.16.11, 5.15.25, and 5.10.102. Flash is dead. The taxonomy is intact. Reuse one representation for two lifetimes and the use-after-free returns. Cover two types with one abstraction and no checked tag and the type confusion returns. Hold a duplicated copy with no single owner and the write reaches the wrong page. Allocator hardening and slab randomisation raise the price of exploitation; they do not change the bug class. Metz described a maintenance cost in 2016. In systems code the same wrong abstraction is a write primitive, and it has not stopped shipping.

Share

Keep Reading

Stay in the loop

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