Skip to content

SPTM M5 boot bring-up: findings 23–28 + the H1 GENTER probe - #1

Open
qaustria wants to merge 30 commits into
antiapplefox:mainfrom
qaustria:sptm-bootkc-spans
Open

SPTM M5 boot bring-up: findings 23–28 + the H1 GENTER probe#1
qaustria wants to merge 30 commits into
antiapplefox:mainfrom
qaustria:sptm-bootkc-spans

Conversation

@qaustria

Copy link
Copy Markdown

What this is

The SPTM-era M5 (T8142) boot-bring-up research line: reverse-engineering how iBoot
and the Secure Page Table Monitor gate the BootKC on Apple Silicon, and building the
tooling + probes needed to get a non-XNU kernel accepted. 29 commits of findings,
tooling, and analysis notes, culminating in the H1 GENTER probe.

All hardware experiments run in 1TR on an isolated SPTMTest volume group at Full
Security; Macintosh HD is never touched. Build artifacts (the 121 MB .kc files)
are gitignored — only source, scripts, and analysis notes are here.

Highlights

H1 GENTER probe (this branch's headline, f591774)

  • scripts/patch-bootkc-genter.py detours a late XNU symbol (default
    IOPlatformExpert::start, keeping the pacibsp landing pad) and issues one raw
    SPTM GENTER from inside the blessed BootKC cave
    — the concrete test of whether
    our detour inherits XNU's domain-1 authority (the "domain-1 launchpad" hypothesis).
  • Default call is (0x9,0x0) _sptm_cputrace_is_mode_supported — a pure predicate that
    writes no memory and mutates no SPTM state.
  • Verified end-to-end without guessing: the emitter round-trips to the exact
    25-instruction design; a byte-exact pristine KC was recovered by inverting the
    detour edits (hash matched); the built KC passes all 30 check-bootkc.py
    invariants; and it is byte-identical to the proven-booting detour KC except inside
    the cave
    , making it a clean controlled experiment. Readout is by boot-survival —
    no framebuffer needed.

Boot-object findings (24–28) — established that a modified boot object executes on
the M5 (coih is settable), that raw/unblessed objects are refused before our first
instruction, and that our patched instruction actually executes (entry-nop boots,
entry-brk hangs). Probe tooling: patch-bootkc-entry.py, patch-bootkc-hook.py,
patch-bootkc-paint.py, and 1TR staging/collection scripts.

BootKC + packaging — the six SPTM role-spans are contiguous spans (not per-segment
labels); collections build with kmutil without an Apple KDK; check-bootkc.py
validates 30 structural invariants; pack-bootkc.py / frame-type tooling.

Interrupt + firmware RE — recovered the T8142 AIC3 register map and IACK/IPI
protocol (finding 23); extracted SPTM firmware tables for 25B78/25C56/25F71 with a
decoder (sptm-decode.py) and its tests.

Testing

check-bootkc.py, test-sptm-decode.py, check-frame-types.py, and
check-doc-tables.py pass. Hardware probes are documented in analysis/boot-reports/
with their readout method (boot-survival triangulation). The H1 probe is built and
desk-verified but not yet run on hardware.

🤖 Generated with Claude Code

qaustria and others added 30 commits July 27, 2026 13:27
Recover the frame-type, violation-code, domain and dispatch-state tables
directly from the SPTM payloads rather than transcribing them by hand,
and pin the generated header to a named build.

- analysis/sptm/extract-*.py produce the CSVs; gen-frame-types.py emits
  asm/sptm.h from them and --check re-verifies the header against its CSV
- sptm-decode.py turns a raw SPTM violation into a named diagnosis;
  test-sptm-decode.py covers it
- check-header-syntax.sh confirms the header parses and its compile-time
  guards hold

Firmware drift is real and mid-table: 25B78 (26.1) and 25C56 (26.2) are
byte-identical, but IDs renumber between 26.2 and 26.5 --
VIOLATION_ILLEGAL_DISPATCH_PERMISSION is 41 on 25B78 and 43 on 25F71.
Hence the per-build CSVs and the --check gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured against Apple's own kernelcache.release.mac17g (25B78, t8142).
kmutil emits fourteen Image4 tags -- lowest-VA, entry point, then
(offset,size) pairs for ro, rs, rx, bx, rw, le. Each pair is not the
extent of one named segment: it runs from an anchor segment to the start
of the next anchor, absorbing every segment in between.

  __TEXT           ro anchor      __TEXT_BOOT_EXEC  bx anchor
  __DATA_SPTM      rs anchor      __PRELINK_INFO    rw anchor
  __TEXT_EXEC      rx anchor      __LINKEDIT        le anchor

__PRELINK_TEXT, __DATA_CONST and __DATA are interior -- never named,
never separately labelled. The partition is exact: ro closes on
0x18fc000 (= __DATA_SPTM's fileoff) and le ends at 0x7354000 (= the file
length).

Consequences:

- check-bootkc.py gains five invariants: canonical VMA order,
  filesize == vmsize per segment, VMA and file contiguity, all six
  anchors present, anchors ascending in role order. 27/27 on Apple's
  reference.
- Those invariants catch a real defect the previous 22 could not see.
  Our packed m1n1 orders segments __TEXT __TEXT_EXEC __DATA_CONST __DATA
  PYLD __TEXT_BOOT_EXEC __DATA_SPTM __LINKEDIT, which places read-only
  data inside the rx span and yields two negative-length spans. Four
  correct failures.
- pack-bootkc.py now refuses to write an image whose final VMA order is
  wrong instead of emitting an incoherent one.

docs/bootkc-packaging.md's old segment table was wrong twice over: it
mislabelled the roles (__DATA_CONST as ro, __DATA as rs, __DATA_SPTM as
an unnamed SPTM window) and its vmsizes came from a different
kernelcache than the one it named. Replaced with today's measurement and
a Correction note rather than silently swapping the numbers.

README findings 20 and 21: the span partition with its movk evidence,
and the rusch95/asahi_neo XNU-shim prior art -- link a shim into Apple's
real kernelcache, let XNU bring SPTM up normally, intercept late. That
removes m1n1 from the critical path and dissolves the iBoot-validation
and SPTM-domain-1-registration coin flips. It does not solve the late
intercept, the t8142 device tree, or drivers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renumber the surviving patches and drop the ones the SPTM findings
invalidated. 0004 now carries the XNU-shim-shaped boot handoff.

Note that nothing under build/m1n1/ is touched, here or anywhere else in
this tree. Upstream m1n1's AGENTS.md forbids AI/LLM contributions
outright (https://asahilinux.org/slop/), so the m1n1 side stays a
human-authored patch series applied by script; build/ is gitignored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two bugs, both of which stopped the script dead on a perfectly good
installer.

1. The guard read CFBundleShortVersionString, which is the installer
   app's own version sequence and not the macOS version it installs --
   the 26.1 installer reports "21.1.01", so the script rejected it. Read
   DTPlatformVersion for the marketing version and, authoritatively,
   Assets.0.Build from the asset manifest inside SharedSupport.dmg. The
   downloaded installer verifies as build 25B78, macOS 26.1, which is
   what the pinned SPTM tables describe.

2. startosinstall on Tahoe gates --volume off when run from a booted
   macOS. The flag is still compiled in -- the binary contains
   "Usage: startosinstall --volume <target volume path>" -- but it is
   omitted from the runtime usage text and passing it makes the tool
   print usage and exit 0, installing nothing. Probe the usage text and,
   when the flag is missing, fall back to the graphical installer, which
   still ships its destination pane ("Select the disk where you want to
   install macOS.", IATargetSelectItem in IA.bundle).

   --eraseinstall is explicitly called out as not a substitute:
   --preservecontainer spares the other volumes in the container, not
   the system volume being erased.

Also gitignore fw3/ -- 156 MB of extracted Apple firmware payloads
(sptm.t8142.release.im4p, kernelcache.release.mac17g and friends) whose
redistribution status is unclear. They are reproducible from the IPSW.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
macOS republishes the ADT that iBoot hands XNU as the IODeviceTree
registry plane, so the real hardware description for this exact board is
readable without unpacking DeviceTree.<board>.im4p out of an IPSW and
without guessing which variant applies. scripts/dump-adt.sh captures it.

Measured, not assumed:

  identity     J704AP / Mac17,2, chip-id 0x8142, board-id 0x22,
               arm-io,t8142, production-cert 1, debug-enabled 0
  arm-io       physical = reg_base + 0x2_1000_0000, derived from uart0
               where the ADT gives both the raw reg and the translated
               IODeviceMemory address
  DRAM         base 0x100_0000_0000, size 0x4_0000_0000 (16 GiB)
  UART0        0x3_A520_0000, 16 KiB, uart-1,samsung, IRQ 0x4cf,
               carries boot-console
  AIC          0x3_8100_0000, 0x1cc000, #main-cpus 10
  CPUs         6xE (reg 0x0-0x5) + 4xP (reg 0x100-0x103), apple,sawtooth

Two of these change the plan.

DRAM base sits at 1 TiB, far above the M1 generation's 0x8_0000_0000.
Early MMU bringup cannot assume a 32- or 36-bit physical window.

The interrupt controller reports compatible "aic,3". Linux's
irq-apple-aic binds apple,aic and apple,aic2 only; there is no aic3
support upstream and no public register documentation. AIC2 was already
a substantial rework of AIC1, so a third revision should be budgeted as
real reverse engineering rather than a compatible-string addition. It is
on the critical path -- unlike a GPU or display driver, nothing boots
without interrupts.

The one piece of good news is uart-1,samsung: the same core Apple has
shipped since the M1, already driven by both m1n1 and Linux. Serial is
the one bringup step where we inherit working code.

Privacy: the full dump carries the ECID, serial numbers, MAC addresses
and platform UUID, so adt-full.{txt,plist} are gitignored and stay
machine-private. dump-adt.sh scrubs those properties from the broken-out
node files and exits non-zero if any survive; only the scrubbed files
and the derived peripheral list are committed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The span tables in README.md and docs/bootkc-packaging.md carried
numbers from a restore-staged/ path that does not exist on disk. The
original figures were right; a previous "correction" broke them.

Re-measured against the BootKC this machine actually boots, cited by
sha256 rather than by path: 120,668,160 bytes, ro closing on
__DATA_SPTM at 0x1808000, le ending exactly at 0x7314000.

Paths go stale between reboots and installs. Hashes do not.

Adds HANDOFF.md so another agent can pick this up cold: the hard
constraints (no m1n1 edits, no credentials, primary volume group stays
at Full Security), the state of the SPTM research, and the two things
found today -- kernel.release.t8142 already ships on the machine, and
the configure-boot validation experiment needs no KDK.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Apple never shipped a release KDK for 25B78, the build this M5 runs --
developer.apple.com has only the 26.1 betas. But kmutil's KDK
requirement is a directory probe, not a signature check, and its os_log
names what it probes for: a SystemVersion.plist reporting the right
build, System/Library/Kernels, and System.kext's pseudo-kext binaries.

All of that ships in macOS except the pseudo-kext executables, which is
what a KDK actually adds. build-bootkc-kmutil.sh assembles a synthetic
KDK from the system's own files in a shadow root and drives kmutil
through -R. Nothing outside the output dir is written, /Library/Developer
is untouched, no sudo.

The result confirms findings 19 and 20 by construction rather than by
inference. All fourteen kc* tags land exactly on the span partition,
kclo is the lowest vmaddr, kcep is the base of __TEXT_BOOT_EXEC, and the
spans tile the file with no gaps. __DATA_SPTM comes out at 0x54000 --
byte-identical to Apple's shipping BootKC, since the monitor fixes that
region's shape.

verify-kc-tags.py re-derives the spans from the load commands and diffs
them against the emitted tags, so this is checked per-run, not trusted
from a table.

Caveat recorded in both docs: the pseudo-kext binaries are stand-ins,
not real KPI stubs, so only explicit-only collections build. Nothing
here has been booted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The invariant checker was derived entirely from static RE of Apple's
shipping kernelcache, before we could build a BootKC at all. It now
passes all 22 invariants on an artifact produced by Apple's own
packager -- an independent check on the checker.

Two invariants are upgraded by this. The entry point landing inside
__TEXT_BOOT_EXEC was inferred from one image; it is now confirmed as
what kmutil deliberately emits. And __DATA_SPTM being 0x54000 of
file-resident zeros in both images shows the region is sized by the
monitor's interface rather than by the payload.

Also updates HANDOFF.md: the KDK thread is closed, with the dead ends
recorded so the next agent does not retry --allow-missing-kdk, --sdk or
--kdk. Real KPI symbol stubs are now the open sub-thread, and the 1TR
validation experiment is the top-ranked task.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
t8142-hardware.md called AIC3 the biggest driver gap on the critical
path and assumed a third AIC revision would have to be reversed from
scratch. It doesn't.

Apple ships com.apple.driver.AppleInterruptControllerV3 unstripped --
544 symbols, including the seven getAIC*Offset methods that compute
register addresses. Each has a device-tree-override path and a built-in
fallback; the fallback constants are the map. Independently, this
board's own ADT node names every offset and stride.

The two sources agree on every value, so the map is doubly sourced
rather than inferred: globals at 0x0/0x4/0xc/0x14, IACK at 0x40000,
per-die timestamps at 0x1000 stride 0x10, and a per-die interrupt block
at 0x10000 stride 0x4a00 holding the config array plus set/clear/monitor
bitmaps at +0x4400/+0x4600/+0x4800.

Also records that there are two AIC3 sub-layouts, selected by a flag the
driver reads rather than assumes -- t8142 is the compact one. A driver
that hardcodes it will break on whatever ships the extended variant.

Honest about what is still missing: the IACK read-acknowledge protocol
and its return encoding, the IPI registers behind
SoftwareInterruptTrigger, and CPU affinity. Nothing here has been read
from live hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AppleInterruptControllerV3 ships unstripped with its panic format
strings intact, and the format strings name the fields. Reading
handleInterrupt, SoftwareInterruptTrigger, enableVector,
disableVectorHard and start gives:

  - AIC_IACK is read-to-acknowledge, no separate EOI. The returned word
    packs the IRQ number in bits 0..11, the vector type in bits 16..18
    (1 = hardware interrupt, 0 = drained) and the die in bits 24..26.
    Apple's own panic string prints the extracted type alongside the raw
    word, which is what pins the field widths.
  - SW_INT_SET at +0x4000 and SW_INT_CLR at +0x4200 within the die
    block. These have no ADT property and no accessor method; they are
    hardcoded in SoftwareInterruptTrigger, which is why the first pass
    missed them.
  - INT_MASK_SET / INT_MASK_CLR are write-1-to-act. enableVector writes
    a bare 1<<bit, not a read-modify-write.
  - AIC_CAP0 bits 0..15 = interrupt count, bits 24..27 = dies minus one.
    AIC_MAXNUMIRQ bits 0..15 = interrupts per die, bits 24..27 = max
    dies (start panics above 8). AIC_GLBCFG bit 0 is the global enable.

Correct the per-die layout: the config array is 0x4000 bytes, not
0x4400. SoftwareInterruptTrigger puts SW_INT_SET at die_base + 0x4000,
which caps it. That makes it 4096 u32 entries, one per interrupt --
matching both the 0x200-byte bitmaps (4096 bits) and the 12-bit IRQ
field in the IACK word. Three independent numbers agreeing on 4096 is
the strongest internal check in the document.

Also record that t8142 must report AIC_REV & 0xff < 0x0b: above that the
driver requires an aic-numdies device-tree property this board does not
have, and start would panic. That one is an inference from absence, and
is flagged as such.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README gets finding 23 (the AIC3 recovery, including the generalisable
part: check llvm-nm and strings before budgeting an Apple block as a
hard RE job -- two pessimistic estimates in this repo have now been
wrong the same way). Correct the findings preamble, which claimed
everything came from the 25F71 IPSW; 18-23 come from the target
machine's own 25B78 firmware and live device tree.

HANDOFF gets a new section 4.4 for AIC3, an updated critical-path note
in 4.2, and a rewritten open-thread 3 -- the IACK protocol is no longer
the ask; confirming AIC_REV < 0x0b is. Also a warning against moving the
driver row in the progress table just because a register map exists: the
column measures code that runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every table in this repo that quotes an address, a size or a register
offset was transcribed by hand from a Mach-O, an IM4P or a device-tree
dump. check-doc-tables.py re-derives all of them and fails on any
disagreement -- 63 checks across README.md, docs/bootkc-packaging.md,
docs/aic3.md and docs/t8142-hardware.md.

It exists because a doc in this repo once got "corrected" with figures
from a path that did not exist, and the wrong numbers survived a commit.
The rule from that was "cite the artifact by hash, not by path"; this
enforces it.

It found one on its first run: README abbreviated the kernelcache sha256
as d457335f...d3214d, but the last six digits are 3d214d. Fixed.

What it checks:
  - the sha256 and byte count the docs cite are this file's
  - every row of the segment table against LC_SEGMENT_64
  - both span tables, in both documents, against the anchor partition,
    including that the spans tile the file with no gaps
  - every AIC3 register offset, stride and mask against node-aic.txt,
    and that the per-die block offsets are extint-baseaddress plus the
    documented layout
  - the hardware doc's identity, DRAM, UART and AIC figures, and that
    the arm-io translation really is IODeviceMemory minus reg

Verified negatively as well as positively: perturbing a span end and a
globals-table offset produces exactly the two corresponding failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
check-doc-tables.py is written and green; the stale known-bad
m1n1.bootkc.macho build output is gone. Record both as done rather
than leaving them on the open list for the next session to re-derive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ran the section 5 experiment from 1TR on the target machine. coih on the
SPTMTest volume group went from absent to E6554B97...D07F6764CF. The
payload was byte-identical to the kernelcache that already boots that
volume group, so the only variable under test was the install path.

nsih did NOT change, which is the more interesting half: a custom boot
object is additive, not a replacement. coih is a separate slot alongside
the normal next-stage object.

Also records the ioreg chosen-node properties that name which manifest
actually booted, and is explicit that this proves installation and not
execution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The section 5 run proved a custom boot object installs, not that it
runs -- the payload was byte-identical to nsih, so both outcomes look
the same from userspace.

patch-version-string.py stamps a 3-byte marker into the timezone field
of XNU's version string. Inert text, fixed width, file size unchanged,
6 bytes differ in total. uname -v after a boot then answers the
question directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The results volume is readable from every boot, so a script that writes
next to itself removes the camera from the loop entirely -- and gives
exact hashes rather than a picture of them.

Baseline from Macintosh HD is in the commit-adjacent report: uname -v
reads PDT, boot-manifest-hash matches that volume group's nsih. Also
surfaces IODeviceTree boot-object-manifests, which enumerates every
Image4 object iBoot loaded (krnl, sptm, trxm, ibot, lpol, dtre); an
extra entry there would be a second signal that a custom object was
consumed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
uname -v on SPTMTest reads "M5L 2025". The 6 bytes we changed are
running on the machine.

The stronger evidence is the device tree: IODeviceTree
boot-object-manifests lists krnl on Macintosh HD and, on SPTMTest with
coih set, no krnl at all -- a fuos entry in its place, every other tag
unchanged. So "CustomKC or fuOS Image4 Hash" is literal. A custom boot
object is loaded through the fuOS path and substitutes for the kernel
object, which is why nsih stays put.

sptm and trxm still load. We are handed control in the kernel's slot
underneath the monitor -- the position finding 21's shim assumes and
that dispatch_table[0][0]'s 0x02 mask requires.

Payload was still Apple's kernel with 6 bytes changed, so this does not
yet prove an arbitrary Mach-O of ours boots. Progress moved 4% -> 8%,
with row 2 at 40% and reasoning recorded so it does not get rounded up.

Boot reports archived under analysis/boot-reports, UUIDs and hostname
scrubbed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The obvious next milestone -- print to the UART -- does not work on this
machine. uart0 is real and Linux drives it, but a MacBook has no serial
header; the debug UART needs a DCSD-class USB-C cable we do not have.
docs/observability.md records that, because it silently invalidates the
standard bare-metal first step.

What we do have is the panic channel: NVRAM panicmedic-telemetry plus
DiagnosticReports survive a reboot, and Macintosh HD is always bootable,
so a failed probe can be recovered and decoded with sptm-decode.py. That
makes failure informative, so probes should be built to fail usefully
rather than to emit output.

build-probe.py emits flat binaries for configure-boot --raw, loaded at
the real BootKC __TEXT anchor. probe-spin is four bytes of `b .`: it
touches no memory and assumes nothing about MMU state, x0, or exception
level, so it cannot fault for a reason we invented. A silent hang is
therefore the success case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Outcome was neither predicted branch: no hang and no panic artifact, but
a repeating black -> Apple logo (~3s) -> reset cycle.

Two NVRAM variables turn out to record it. panicmedic-telemetry's event
counter went 1 -> 5, so the failures were counted by firmware.
boot-breadcrumbs holds iBoot's own progress trace as four-CC tags.

Important limit on reading that trace, recorded so the next session does
not over-claim: the final cycle is the SUCCESSFUL Macintosh HD boot, and
it is byte-identical in shape to the failed ones. Breadcrumbs stop at
lpol/ibot and never mention krnl, fuos, sptm or trxm on any boot. So the
absence of fuos there is NOT evidence our object failed to load -- the
channel simply does not reach that far.

Whether our four bytes got the PC is still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
spin never faults, so "rejected before handoff" and "ran until a
watchdog fired" produce the same observable: a reset a few seconds in.
brk #0 faults on its first instruction, so the two hypotheses have to
diverge -- identical behaviour means control never reached our bytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
probe-spin produced an ambiguous boot loop rather than the hoped-for
black-screen hang. It perturbs the boot path but does not distinguish
"our code ran then reset" from "the object was refused before first
instruction". Record it as finding 26 and document the brk
discriminator in the observability notes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add stage-1tr-probes.sh (builds the probes onto SPTMTest and drops the
go/show helpers) plus run-1tr-brk.sh and show-1tr-log.sh. run-1tr-brk
verifies the probe is exactly brk #0 (000020d4) and that the target is
the expected SPTMTest volume group before invoking kmutil configure-boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extracted j704/j813/j815 iBoot Stage 2 from the remote IPSW (analysis/iboot/,
gitignored) and found the boot logo goes through DCP surface creation
(display_dcp_create_surface CS_ARGB8888 -> send_swap -> enable_normal_mode),
not a static linear scanout. Records the finding and the surface-address
recovery paths; next step is disassembling the create_surface call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Traced iBoot's DCP display function (create_surface call at 0x120030 -> bl
0x16d1e4). The ARGB8888 surface backing base is computed at runtime (helpers
bl 0x11c9b0 / 0x0004dc / 0x000500 into x21, then add x4,x21,x25 per plane) --
no framebuffer address is baked into the binary. Rules out a hardcodable
static base and collapses the plan onto reading boot_args->Video at probe
runtime, which is gated on the boot_args pointer (x0) at entry.

Adds dependency-free RE tooling: iboot-xref.py (ADRP/ADR/ADD string-xref
finder, resolves to file offsets without the load base) and iboot-dis.py
(address-correct region disassembler over llvm-mc).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…instruction

The brk #0 probe produced the identical signature to spin (black -> Apple
logo ~3s -> loop, same lpol/ibot breadcrumbs, no panic artifact). A retired
brk would have faulted synchronously and left a recoverable artifact; none
appeared. Per the pre-registered discriminator, control never reached our
bytes: a raw 4-byte object at 0xfffffe0007004000 is refused/reset before
first-instruction retirement.

Combined with finding 25 (a structurally-valid marked kernelcache DID run
via the same coih install), the acceptance gate is structural: the boot
object must be a valid BootKC/Mach-O, not a raw blob. The --raw probe line
is exhausted; the next probe carries our code inside a real BootKC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ow-up)

Raw objects are refused before execution (finding 27); a valid BootKC runs
(finding 25). scripts/patch-bootkc-entry.py isolates the container as the one
variable: it overwrites only the LC_UNIXTHREAD entry instruction (base of
__TEXT_BOOT_EXEC) of the pristine kernelcache with brk/spin/nop, same size,
every other byte Apple's. All three pass check-bootkc.py's 22 invariants.

entry-brk is the decisive contrast: raw brk left no panic; the same brk as a
valid BootKC's entry should fault and leave one. entry-nop is the control
(should still boot). Staged on the SPTMTest volume and wired into m5.sh as
install entry-{nop,brk,spin}. Plan in analysis/boot-reports/entry-patch-experiment.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Installing the entry-nop kernelcache (coih 48FEA9EC -> 323B8DAE) and booting
SPTMTest brought up macOS normally, with no panic artifact. The control holds:
patching the first executed instruction and reinstalling does not break the
boot, so any brk/spin difference is the instruction, not the patch pipeline.
Also confirms replacing Apple's `bti c` landing pad with nop raises no
Branch-Target fault here, so a later brk fault will be the brk itself.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xecutes

The controlled pair is decisive: entry-nop and entry-brk are byte-identical
except the single entry instruction, yet nop boots to macOS and brk does not
(hang, no recoverable panic — the trap is too early for XNU's logger). iBoot
gates on the image hash (coih, valid for both) and does not interpret
instruction semantics, so the only stage that can turn those 4 bytes into
different behaviour is the CPU executing the entry instruction.

Counterpart to finding 27: a raw object is refused before execution; a
structurally valid BootKC is not. The structural gate appears crossed.
Confidence high (rests on the control, not one artifact) but not yet a direct
positive readout. Next: corroborate with entry-spin, and begin rung 4 (pack
m1n1 as a BootKC), now justified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds scripts/patch-bootkc-genter.py, the H1 probe emitter. It detours a late
XNU symbol (default IOPlatformExpert::start, keeping the pacibsp landing pad at
+0) and lays a save-x0..x17+x30 / set-x0,x16 / GENTER / restore / replay-
displaced / branch-back trampoline into the __TEXT_BOOT_EXEC cave. Default call
is (0x9,0x0) _sptm_cputrace_is_mode_supported (x16=0x900000000) — a pure
predicate that writes no memory and mutates no SPTM state. Because the cave lives
inside the blessed BootKC (domain 1), the GENTER fires from domain-1 territory;
boot-survival alone reads out accept-vs-fault (no framebuffer needed).

Verified without pushing another untested 121 MB artifact blindly:
- assembler round-trips to the exact 25-instr design (frame balanced, one GENTER,
  displaced replayed before the branch, branch-back to start+8);
- recovered a byte-exact pristine KC by inverting full-hook-detour.kc's two edits
  (hash d457335f… — confirms the detour transform is byte-exact);
- built full-hook-genter.kc: passes all 30 check-bootkc.py invariants;
- controlled pair proven — byte-identical to the proven-booting hook-detour KC
  except inside the cave, so any hardware divergence is due solely to the GENTER.

Fixes a size-probe bug (cave_vm=0 tripped the branch-range check) and corrects
the design note's movz x16,#0x9,lsl#32 hex (0xd2c00130, not 0xd2a00130).

Also lands the supporting scripts (patch-bootkc-hook.py, patch-bootkc-paint.py)
and five boot-report analysis notes that were untracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tlbi wrappers

The Linux MMU-port scaffold modelled TLB invalidation as a GENTER call
(sptm_broadcast_tlbi_{all,asid,va}), and the scope doc budgeted ~100 LoC to
reroute arch/arm64/include/asm/tlbflush.h through it. That call does not exist.

Evidence from the real M5 target firmware (25B78/t8142 BootKC, build/bootkc):
XNU's own __TEXT_EXEC contains 25 native `tlbi` instructions (VMALLE1IS/OS,
VALE1IS, VAE1OS, RVAE1IS) against only 2 GENTERs; subsys 0 has no TLBI dispatch
entry; the sole coupling is VIOLATION_POSSIBLE_PENDING_TLBI, a pending-TLBI
obligation the OS discharges with the native TLBI+DSB it already issues after a
pte update. So the governed OS invalidates the TLB natively — Linux keeps stock
flush_tlb_* unchanged.

Changes:
- remove the three sptm_broadcast_tlbi_* decls/stubs from asm/sptm.h and
  mm/sptm.c; replace with a documentation block
- correct docs/linux-mmu-port-scope.md (TLBI section, tlbflush.h row, risk antiapplefox#1)
- README: mark the broadcast_tlbi TODO resolved
- analysis/boot-reports/finding-30-tlbi-is-native.md: full evidence + repro

Net: the port gets smaller and more correct — a ~100 LoC item deleted, not done.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant