· Marco Schmitt
CVE-2026-112475: an out-of-bounds read in the HFS+ catalog
An HFS+ thread record carries a file name of at most 255 characters. The kernel checked that the name fit inside its B-tree node, but not that it stayed within 255.
- Component
- HFS+ file system (
com.apple.filesystems.hfs.kext) - Class
- Out-of-bounds read in the kernel
- Apple’s impact
- “An app may be able to cause unexpected system termination”
- Fixed in
- macOS Golden Gate 27.2, macOS Tahoe 26.7.2, macOS Sequoia 15.8.2
- Credit
- Shared. Apple credits this CVE to several researchers, me among them (advisory).
Background
The HFS+ catalog is a B-tree. Besides the file and folder records it holds thread records, which map a catalog node ID (CNID) back to its parent folder and its name. The name is an HFSUniStr255: a 16-bit length followed by at most 255 UTF-16 units. A thread record is therefore at most 10 + 2 × 255 = 520 bytes long.
The bug
When the kernel reads a catalog node from disk, hfs_swap_BTNode validates its records. For thread records it checked only that the name ends inside the node. A catalog node can be 32 KiB, so a length far beyond 255 passed.
Later, cat_findname reads the thread record into a fixed 520-byte buffer, the size of the largest valid record. builddesc then converts the name to UTF-8 and passes the on-disk length to utf8_encodestr. That function reads 2 × length bytes from the buffer. The output buffer is sized correctly. Only the read is unbounded.
/* hfs_swap_BTNode, thread record, before the fix */
len = rec->nodeName.length;
if (end_of_name(rec, len) > end_of_node) /* the only bound */
reject();
/* cat_findname -> builddesc */
buf = allocate(520); /* 10 + 2 * 255 */
read_thread_record(cnid, buf);
utf8_encodestr(buf + 10, 2 * len, ...); /* reads 2 * len bytes */
Simplified. The function names are Apple’s; the logic was reconstructed from the shipping kernel binary.
Reaching it
The path needs a crafted HFS+ disk image, mounted read-only. In it, the chain of a hard link points to a CNID that has a thread record but no file record. Calling fsgetpath(2) on the hard link’s inode leads through hfs_vfs_vget and hfs_first_link into cat_findname. The converted name is handed back to the caller as a path component.
I tested this on two macOS 27.0 beta builds (26A5388g and 26A5421a) on an M2 Pro, as a non-root user. Neither attaching the image nor calling fsgetpath(2) needed elevated privileges there.
What it did
With a name length of 780, utf8_encodestr is asked to read 1,560 bytes from a 520-byte buffer, 1,050 bytes past its end. The result goes back to the caller as a file name. That turns the out-of-bounds read into a channel: whatever lies behind the buffer is encoded as UTF-8 and returned by fsgetpath(2).
The channel has limits. The syscall returns at most 1,023 bytes of path, and the conversion is not fully lossless: a zero unit comes back as U+2400, a slash as a colon, and 0xFFFE or 0xFFFF end the name. Within that, the first 255 units are the record itself and everything after them is neighbouring heap. In five triggers, each returned 514 to 522 bytes from beyond the buffer. Four held live heap data, one only zeroes.
What lies behind the buffer can be steered. From a second process of my own, I placed marker data into the neighbouring heap allocations, and in six of six triggers the returned name contained that marker.
The same read can also bring the machine down. When the buffer sits at the end of a page and the read runs into unmapped memory, the kernel faults and panics. Apple rates the impact as unexpected system termination; the heap disclosure is what I observed on the affected builds.
The fix
In macOS 27.2 (checked on beta 26B5086k), hfs_swap_BTNode rejects a thread record whose name length is greater than 255. The new error path carries a new message: catalog thread record #%d name too big. The crafted image no longer mounts, and fsck_hfs reports Invalid thread record length. The code that later reads the name is unchanged. The fix is the single check at validation time.
Timeline
- Reported to Apple.
- New check confirmed in a macOS 27.2 beta (26B5086k).
- Fix released as CVE-2026-112475 in macOS Golden Gate 27.2, Tahoe 26.7.2 and Sequoia 15.8.2.
Takeaway
The length was validated against its container, the node, but not against the limit of its own type. Both bounds are needed: the node limit keeps the record on disk consistent, the type limit keeps every fixed-size buffer downstream safe.