Reading past the edge: a heap out-of-bounds in Apple's HFS+ volume probe (CVE-2026-112456)
A twenty-five-year-old line of Apple filesystem code, a disk that gets probed as root the instant it is attached, and 64 bytes of memory read from the wrong side of a boundary. The vulnerability, how it was found and proved, and the one bounds check that was missing.
This is the first write-up from Zero-Day Research, an initiative I run inside Lean's Product Security team to find and responsibly disclose previously unknown vulnerabilities in widely used software. The finding below is in Apple's code: a vulnerability I reported, which Apple confirmed, assigned CVE-2026-112456, and fixed across iOS, iPadOS and macOS.
It is the kind of flaw that is easy to walk straight past. A disk does not have to be opened to be dangerous. The moment you attach one to a Mac, whether a USB stick or a disk image a stranger sent you, the operating system reaches out on its own to work out what it is. On that automatic identification path, running as root, sits a small helper that has shipped essentially untouched since 2001. Hand it the right crafted bytes and it reads 64 bytes of memory from the wrong side of a boundary: out of the buffer it was given, and into whatever happens to live next to it. No double-click, no prompt, no warning. Before the details, two short detours, because they are the whole point.
01What a CVE actually is
A CVE, short for Common Vulnerabilities and Exposures, is just a public, unique name for a single security flaw. The catalog is run by MITRE, a US non-profit. When a vulnerability is confirmed, it is assigned an identifier like CVE-2026-112456: the year, then a serial number, and from then on everyone, the vendor, defenders, scanners, auditors, can point at the exact same problem by the exact same name.
That sounds mundane, and it is, but it is also what makes coordinated defense possible at all. Without a shared identifier, the same flaw would be described a dozen different ways and no two teams could be sure they were talking about the same thing. A CVE turns "there is a problem in there somewhere" into something precise, trackable and citable. It is the unit of account for software security.
02Why zero-day research matters most in the AI era
Here is the part that is easy to miss. Until the moment a flaw is reported, nobody who could fix it knows it exists. In practical terms that is a zero-day: a live, unpatched weakness. Whoever finds it first decides what happens next. A researcher who reports it gets it fixed for everyone. Someone who keeps it quiet has a tool they can use, or sell, against targets for as long as it stays secret. The entire discipline of security research exists to make sure the first group wins more often than the second.
That race has never been tighter than it is now, and the reason is artificial intelligence. Three things shifted at once. Models now write and ship an enormous share of the world's new code, and they reintroduce the same memory-safety and logic flaws humans have always made, only faster and at far greater volume. Autonomous agents increasingly parse untrusted input on their own, files, archives, disk images, web responses, which means a low-level parsing flaw like the one in this post is now reachable without a human ever being in the loop. And the cost of turning a public patch into a working exploit has collapsed: an attacker can point a model at a fix, recover the vulnerability it closes, and weaponise it in hours instead of weeks. The gap between disclosure and mass exploitation is shrinking toward zero.
So finding and reporting flaws first, before that machinery is aimed at them, matters more now than it ever has. The same AI that accelerates attackers also sharpens research: used carefully, it reads code, flags suspicious patterns, and triages candidates far faster than a person alone. The teams that treat vulnerability research as a first-class, AI-assisted discipline are the ones who stay ahead of the teams that treat it as an afterthought.
None of this is abstract. A handful of findings have already shaped how the whole internet runs:
- Heartbleed (
CVE-2014-0160) was a read bug in OpenSSL that let anyone pull secrets out of the memory of a huge fraction of the world's HTTPS servers. One flaw, reported responsibly, triggered a global patch effort. - Log4Shell (
CVE-2021-44228) was a single logging library used in untold enterprise Java systems. One crafted string gave remote code execution. It is remembered as one of the most severe vulnerabilities of the decade, and it was found and reported, not exploited into the dark. - More recently, a decoder bug in the brotli compression library (
CVE-2026-111497) rippled across everything that bundles it, Node.js, Bun, and more, because shared libraries sit underneath software everyone uses.
And it spans every vendor. Apple, Google and Microsoft all run bounty and disclosure programs, alongside the open-source projects their platforms are built on, precisely because external researchers keep finding what internal review misses. Every one of those reports, confirmed and patched, is a piece of attack surface removed from the world before it can be turned into a weapon. That is the quiet, cumulative way the field keeps a few billion devices safer than they would otherwise be.
That is the machinery this research is a small part of: finding flaws like the one below in widely used software, reporting them to the vendor, and getting them fixed before anyone who means harm reaches them first. The rest of this post is one worked example, start to finish.
03HFS+, hfs.util, and DiskArbitration
Three pieces of Apple plumbing set the stage.
HFS+, the filesystem
HFS+ (Hierarchical File System Plus, or "Mac OS Extended") was the default Mac filesystem for nearly two decades, until APFS replaced it in 2017. It still ships everywhere, because external drives, old backups and disk images are all still formatted with it, and the system has to be able to read any of them.
Internally, HFS+ keeps its structures in trees stored as fixed-size blocks called nodes. Each node holds variable-length records, and at the very end of the node sits an offsets array: a little table of 16-bit numbers saying "record one starts at byte X, record two at byte Y." To read a record, the code looks up its offset there and jumps to it. There is also an extents overflow tree for files too fragmented to describe inline. The vulnerable code walks that tree.
The whole pointEvery number this code reads, node size, record offset, key length, comes off the disk. On a malicious volume those numbers are entirely attacker-controlled. The only thing standing between a parser and corrupted memory is whether it checks those numbers before trusting them.
hfs.util, the helper
hfs.util is a small program whose job is to answer one question: is this device HFS, and what is it called. It does so in probe mode, hfs.util -p. To answer, it reads the volume header and, if the header says there are overflow extents, walks that overflow tree, which is exactly where the bug lives.
DiskArbitration, the trigger
DiskArbitration is the macOS subsystem that notices storage appearing and disappearing. Its daemon, diskarbitrationd, runs as root, and when new media shows up it identifies the filesystem by running a registered probe helper. For HFS media, and for any unrecognised whole disk, that helper is hfs.util -p. Put together: a root daemon automatically runs an old parser on attacker-supplied bytes, with nobody in the loop.
04The attack surface
The automation comes from a property list shipped with macOS, which maps each HFS media type, and a catch-all Whole personality for any unpartitioned disk, to the probe helper:
"Apple_HFS" => { "FSProbeArguments" => "-p" "FSProbeExecutable" => "hfs.util" }
"Whole" => { // ANY unpartitioned disk, recognised or not
"FSMediaProperties" => { "Leaf" => true, "Whole" => true }
"FSProbeArguments" => "-p" "FSProbeExecutable" => "hfs.util" }
The Whole entry is the one that matters. It means the probe runs for any attached whole disk whose content is not yet recognised, which is exactly how a crafted raw image presents itself. The attacker does not even need a valid HFS+ filesystem, only a disk that reaches the probe.
05The vulnerability, line by line
The flaw is in GetCatalogOverflowExtents(). Because Apple publishes this as open source, we can look at the real loop, this is Apple's own public code, not an exploit:
for (i = 1; i <= numRecords; ++i) {
/* (a) read a 16-bit record offset from the offsets array at the node tail */
p = bufPtr + nodeSize - (sizeof(u_int16_t) * i);
/* (b) jump to the record; the offset is NEVER checked against the node */
p = bufPtr + OSSwapBigToHostInt16(*(u_int16_t *)p);
k = (HFSPlusExtentKey *)p;
/* (c) read k->fileID before p is validated; itself a small OOB read */
if (OSSwapBigToHostInt32(k->fileID) != kHFSCatalogFileID) goto Return;
extents = realloc(extents, listsize += sizeof(HFSPlusExtentRecord));
/* (d) THE BUG: source = p + keyLength + 2, keyLength is attacker-controlled, unbounded */
bcopy(p + OSSwapBigToHostInt16(k->keyLength) + 2,
&extents[*catalogExtCount], sizeof(HFSPlusExtentRecord)); // 64-byte OOB READ
}
Three on-disk, attacker-controlled 16-bit values feed a memory address with no guard rails. The record offset (b) is added to the buffer base unchecked. The key length (d) is added again, also unchecked. So the copy source lands at bufPtr + record_offset + keyLength + 2. Set the key length to 0xFFFF and a plausible record offset of 16, and that is bufPtr + 0x10011, roughly 60 KiB past a 4 KiB buffer. Then it reads 64 bytes from there. That is a textbook heap out-of-bounds read (CWE-125):
Before any validationThe read fires before the helper ever checks whether the volume is a legitimate HFS+ filesystem. One plausible volume header is enough to walk straight into the loop. No real filesystem required, just a few valid-looking bytes.
06How I found it: code review
This did not come from a scanner. It came from a method I apply to native parsers, and it is worth describing because the method is far more reusable than any single vulnerability.
Read the delta around a known fix
Apple had just fixed a different HFS memory-safety issue (CVE-2026-43773), and Apple publishes the matching source drops. I started by diffing the tree before the fix against the tree after it, reading not just what changed but what had been left alone. A fix tells you where the vendor was already looking; the untouched code right next to it is where the next issue tends to hide.
Hunt the sink family
Parser vulnerabilities cluster into a few shapes. The one I look for hardest is a count, offset or length read straight from input and used in pointer arithmetic without a bound. Reading for that pattern, GetCatalogOverflowExtents stood out immediately: a bcopy whose source is a pointer plus two attacker-controlled 16-bit values, with no comparison to the buffer size anywhere above it.
Prove it is reachable, not just present
A dangerous line means nothing if nothing can reach it, so the hardest and most valuable part was tracing reachability by hand, from the program's entry point down to the sink, confirming each branch on the way could be satisfied by attacker bytes:
Confirm the fix did not cover it
Finally, a byte-for-byte diff of this specific file between the pre-fix and post-fix drops came back identical. That proved two things at once: the sink was still present on shipping builds, and it was genuinely a different vulnerability from the one Apple had just patched, not a duplicate.
07How I proved it: fuzzing, locally and safely
Reading code builds a hypothesis. It does not prove the vulnerability fires. For that I needed a running demonstration, obtained without touching anything that was not mine. The constraints were strict: everything runs on my own machine, against a test image I generate; no Apple service or third-party host is ever contacted; no system protection is disabled and no Apple process is ever attached to or modified.
Rebuild from public source, with instrumentation
Because the shipping binary is locked down, I rebuilt hfs.util from Apple's own published source at the exact version matching the affected OS, adding only a few trace prints around the suspect loop, no logic changes. That gives a binary that behaves like the real one on the probe path but narrates what it is doing. I am deliberately not publishing the image generator or the exact crafted bytes.
Drive the fields, do not spray random bytes
My parser-fuzzing approach for filesystem code is targeted rather than brute-force: instead of throwing random data at a 200 KB volume and waiting, I identify the exact on-disk fields that steer the pointer arithmetic, the record offset and the key length, and drive those directly across their range. It is deterministic and fast, and it lands the read exactly where the code review predicted.
Make it undeniable with guard pages
An out-of-bounds read is sneaky: by default the stray address usually lands on some other mapped page and nothing crashes, so the flaw hides. To remove all doubt I ran the rebuilt binary under a guard-page allocator, which places an unmapped page immediately after every allocation. Now the read past the end hits that page and the process dies instantly, at exactly the instruction predicted:
GCOE: leaf numRecords=1 bufPtr=0x15f3e7000 nodeSize=4096
GCOE: rec 1 → recordOff=0x10 k->fileID=0x4 k->keyLength=0xffff
GCOE: ABOUT TO OOB bcopy src = bufPtr + 0xffff + 2 (= +0x10011) len=64
<SIGSEGV: read hits the unmapped guard page>
exit code 139 (128 + signal 11)
I corroborated it a second, independent way, with a stand-alone harness that maps a buffer followed by a protected page and runs the same logic, reporting the identical fault through a sanitizer. Two different tools, same instruction, same conclusion. That is the bar I hold every finding to before calling it real: a reproducible crash I can explain, not a scanner warning.
08Impact and the fix
I reported it honestly, without inflating it.
| Primitive | 64-byte heap out-of-bounds read (CWE-125). The destination is grown before every copy, so there is no out-of-bounds write and no corruption on the destination side. |
| Context | Runs as root inside the probe, with no user interaction beyond attaching the media. |
| Consequence | Root-context information disclosure: 64 bytes of adjacent heap are copied into the extents list and then used as block offsets for further reads, giving an indirect side channel. Also a denial of service of the root probe when the stray read lands on an unmapped page. |
| Trigger | Insert a crafted USB device, or open a crafted disk image. No prompt, no click. |
| Longevity | Present, unchanged, since 2001; untouched by the adjacent 2026 fix. |
The fix is the bounds check that was missing: before trusting the on-disk offset and key length, confirm they keep the computed address inside the node. Notably, that discipline already existed elsewhere in the same source tree, the filesystem-check tooling validates these very fields, the probe helper simply never adopted it. Apple addressed CVE-2026-112456 with improved input validation in iOS 26.7.2, iPadOS 26.7.2 and the corresponding platform releases. If you run Apple devices, the one thing that matters is the ordinary thing: keep them updated.
09Closing thoughts
A vulnerability like this sat on a root, near-zero-click path for close to twenty-five years. The only question ever open was who would reach it first. It was found, reported, and it is now fixed for everyone. That is the whole case for doing this kind of work and publishing it: the same careful reading that turns up a reachable flaw in a decades-old parser is what turns a silent, exploitable condition into a patched one.
This is the first post in a series. I will keep publishing real findings and the methods behind them, so the next person reading an old parser knows exactly what to look for.
10References
- Apple security release: support.apple.com/en-us/149227 (iOS/iPadOS 26.7.2). Public credit includes Yash Pandya (Lean).
- MITRE CVE record: cve.org · CVE-2026-112456
- NVD entry: nvd.nist.gov · CVE-2026-112456
- CWE-125, Out-of-bounds Read: cwe.mitre.org/data/definitions/125.html
- Apple open-source HFS: github.com/apple-oss-distributions/hfs