David Strnadel
← All investigations
CVE-2026-65398Technical writeup

One missing check.
A kernel boundary crossed.

How a sibling comparison led from an unchecked array index to a repeatable, crash-based kernel-read witness.

Tested build
iOS 27 beta 4 · 24A5390f
Entry point
IOKit · selector 95
Evidence
Physical-device archive
Attribution
Shared Apple credit
Inside this investigation
The short version60 SEC

An array index became a pointer.

Selector 95 omitted a bound enforced by its sibling. An out-of-range slot could reinterpret adjacent provider state as an object pointer. The investigation progressed from register control to a kernel-load witness, with each claim tied to a separate experiment.

IOMobileFrameBufferGo to the mechanism ↓
01

Context & attack surface

#

During the iOS beta cycle, I compared new kernel functionality with neighbouring operations. Selector 95 in IOMobileFramebufferUserClient used the same provider array as selector 93, but the two paths disagreed about index validation. [E1]

The tested entry point was reachable from an ordinary development-signed app through connection type 0. Its dispatch contract accepted one scalar and a 20-byte structure, with no outputs and no private entitlement requirement. The experiment was performed on a physical iPhone14,4 / D16AP, running iOS 27 beta 4, build 24A5390f.

02

The missing invariant

#

The intended array has 189 entries. A valid index must satisfy (uint32_t)index < 0xbd. The adjacent selector 93 enforced that condition before computing a slot; selector 95 did not. [E1]

Listing 01 The sibling’s guardARM64 · unslid addresses
010xfffffff009a868d8  cmp  w2, #0xbc020xfffffff009a868dc  b.hi 0xfffffff009a8699c
The unsigned comparison rejects indexes above 0xbc. This excerpt is from selector 93, not a proposed patch for selector 95.

The general slot computation also includes signed-low-32-bit and pointer-poisoning behaviour. For the small index used in the read demonstration, the relevant arithmetic simplifies to:

Listing 02 Why index 0xd4 reaches the carrierDerived arithmetic
01array_base  = provider + 0x174002slot        = array_base + 0xd4 * 0x3803            = provider + 0x45a004slot + 0x18 = provider + 0x45b8
This equation is specific to index 0xd4. It is not a complete reconstruction of the handler’s overflow behaviour.

The out-of-range slot overlaps provider state populated by a legitimate transaction. The bug is the unchecked reinterpretation of that state as an array element. SwapDebugInfo is a value carrier here, not a second vulnerability. [E2]

Figure 01 / Value flow24A5390f · explanatory diagram
APPLICATIONCurrent-boot targettransaction + 0x48
PROVIDER STATEUnchecked index 0xd4slot + 0x18
KERNELPointer interpretationldr x16, [x0]

The app reads the current-boot Target Address dynamically. A legitimate transaction carries that qword into provider state.

Read the complete value flow
  1. The app captures the current-boot Target Address with three equal, nonzero reads.
  2. SwapDebugInfo places it at transaction +0x48; selector 5 submits the transaction.
  3. The DCP path copies transaction +0x40..+0x4f into swap-node +0xc40..+0xc4f.
  4. Layer-2 completion copies node +0xc48 into provider +0x45b8.
  5. Index 0xd4 selects provider +0x45a0. The stored qword is now interpreted as slot +0x18.
  6. The helper loads that qword into x0 and executes ldr x16, [x0].
03

From hypothesis to witness

#

The original report demonstrated full 64-bit register control. The helper loaded the app’s qword into x0, then faulted on an out-of-bounds access without overwriting it. That established a register primitive; it did not yet establish a selected-address kernel read. [E1]

Listing 03 The original register-control witnessARM64 · annotated excerpt
010xfffffff009a866a4  ldur x0, [x3, #0xc]020xfffffff009a866a8  cbz  x0, 0xfffffff009a867f0030xfffffff009a866ac  mov  w21, #0040xfffffff009a866b0  ldr  w9, [x19, #0x28]
Line 1 loads the full application qword. Line 4 is the first invalid operation in this witness; x0 remains unchanged between them.

The follow-up used the same missing check to reach a stronger, distinct witness: a kernel load from the current-boot Apple Target Address. It used _COMM_PAGE_ASB_TARGET_KERN_ADDRESS, rather than the Target Value used for register control. [E2]

Listing 04 The read witnessARM64 · relevant operations
010xfffffff009a867f4  x0 ← [slot + 0x18]02                  …030xfffffff009a867fc  ldr x16, [x0]
Line 1 is a semantic annotation of the load recorded in the technical note, not a verbatim disassembly line. Line 3 is the faulting native instruction.
Two independent boots, one installed binary
CheckRun 1Run 2
Runtime target captureThree matching readsThree matching reads
Panic x0 and FARMatch same-boot targetMatch same-boot target
Index in x220xd40xd4
Unslid PC0xfffffff009a867fc0xfffffff009a867fc
Recorded pair verification27 / 2727 / 27

The retained supplement records 8/8 cross-run checks. Both runs used the same installed binary and different boot sessions. These are archived results, not fresh device runs performed for this article. Boot identifiers and raw panic files are not reproduced here. [E2]

04

Impact & limits

#

What the evidence supports

  • Ordinary-app reachability on the tested build.
  • A missing index check and a native out-of-bounds operation.
  • Full-width kernel register control in the original witness.
  • A crash-based kernel-load demonstration satisfying the recorded Target-Flag read contract.

What it does not establish

  • Kernel data returned to the application as a general readback oracle.
  • An arbitrary-write primitive.
  • Program-counter control or kernel code execution.
  • The exact beta-4 mechanism on every affected Apple platform.

Apple’s public advisory describes an out-of-bounds access issue with potential system termination or kernel-memory corruption, addressed through improved bounds checking. Credit is shared with other researchers. [E3]

05

Remediation & research history

#
Listing 05 Restore the array invariantProposed remediation · pseudocode
01if ((uint32_t)index >= 0xbd)02    return kIOReturnBadArgument;0304// Only then calculate and use the slot.
This is the remediation proposed in the original report. It is not presented as Apple’s source code or a verified patch diff.
  1. DISCOVERY
    A sibling comparison

    Binary diffing narrowed the search; the inconsistent guard supplied the hypothesis.

  2. VALIDATION
    Register control, then a kernel-read witness

    The second demonstration reused the original root cause and established a different primitive.

  3. DISCLOSURE
    Reported to Apple

    Technical reports and reproducible evidence were supplied through Apple Security.

  4. PUBLIC RECORD
    CVE-2026-65398

    Apple lists shared credit and a bounds-checking remediation.

I directed target selection, hypothesis formation and validation. AI accelerated reverse analysis, PoC development and reporting. The conclusions came from the evidence. This chronology does not assert that Apple’s patch was created in response to my report.

06

Evidence & references

#

The article separates retained research records from the vendor’s public acknowledgement. The downloadable excerpt contains the selected technical passages used here, not the complete original submission or a runnable exploit.

E1

Original register-control report

Author’s retained APPLE_REPORT.md: dispatch contract, sibling check, register witness and proposed remediation.

E2

Kernel-read supplement

Author’s retained TECHNICAL_NOTE.md: same-root-cause value flow, exact executed binary and cross-boot results.

E3

Apple security advisory

Public CVE attribution and vendor impact description. The linked advisory is for macOS Golden Gate 27; the experiment in this article was on iOS beta 4.

Open source ↗
Download selected evidence · TXT
Continue exploring

A different path through the same boundary.

The AuthKit branch of a preference-access investigation.

Read the related investigation