HPM_RE.md (23319B)
1 # HPM / USB-C "Ace" (TI CD3217 / TPS6598x) DisplayPort alt-mode restart — RE notes 2 3 Static analysis only. No hardware was touched. Target: decompressed arm64e fileset 4 kernelcache macOS 15.6.1 (24G90), `tools/kernel/kernelcache.macho`. 5 6 Fixup base (for chained ptrs): `BASE = 0xfffffe0007004000`; decoded pointer = 7 `BASE + (raw & 0xffffffff)`. Helper scripts used live in the session scratchpad; 8 all addresses below are runtime VAs. 9 10 Kext text ranges: 11 - `com.apple.driver.AppleHPM` `__TEXT_EXEC` = `0xfffffe0009b3d8c0` .. `+0x51f00` 12 - `com.apple.iokit.IOAccessoryManager` `__TEXT_EXEC` = `0xfffffe000aba2ab0` .. `+0xf2d9c` 13 - `com.apple.iokit.IOPortFamily` `__TEXT_EXEC` = **0x84 bytes (stub)** — all logic lives in IOAccessoryManager. 14 15 --- 16 17 ## 0. TL;DR — ranked recipes (gentlest/fastest → most drastic) 18 19 All go through `AppleHPMLib.plugin` on the **AppleHPMDevice** node for Port-USB-C@2 20 (RID 1), i.e. the same connection you already use for `iecsRead`. Opening the user 21 client requires **root AND the `com.apple.USBCEntitlement` entitlement** (see §1); 22 `hpmdiagnose` has that entitlement, your own tool must be signed with it or injected 23 some other way. 24 25 `route` = 0 (the controller's own port, as you already use). 4CCs are **little-endian 26 ASCII u32**. 27 28 | # | Action | Call (plugin vtable → kernel selector) | Confidence it restarts DP | 29 |---|--------|----------------------------------------|---------------------------| 30 | 1 | **Re-send DP Status / re-assert HPD** by replaying the controller's own DP-status VDM to the partner | `sendVDM` (vtable +0x40 → **selector 3**), SOP=port-partner, payload = DP VDM (see §4) | Medium — mirrors the driver's own `sendDpStatusUpdates`; cheapest, no teardown | 31 | 2 | **DP alt-mode Exit then Enter** (true "replug" of just the alt mode) | two `sendVDM` calls: Exit Mode VDM then Enter Mode VDM, SVID 0xFF01 (§4) | Medium-High — this is the literal software unplug/replug of DP | 32 | 3 | **'DRST' USB-PD Data Reset** (renegotiate PD + re-enter all alt modes) | `iecsCommand` (vtable +0x38 → **selector 2**), cmd=`'DRST'`=0x54535244, flags=0 — or write it raw via `iecsWrite` (§3) | Medium-High — this is exactly what the driver's `exitPDMode` issues (§4) | 33 | 4 | **Force port re-evaluation / re-run mode logic** (driver re-runs `configureDP`) | `forceMode` (+0x58 → **selector 6**) or `forceUpdateMode` (+0x60 → **selector 7**) | Medium — re-drives mode flags, may re-configure DP | 34 | 5 | **Raw host-interface VDM**: write DATA1(0x09)+CMD1(0x08)='VDMs' yourself | `iecsWrite` (+0x30 → **selector 1**) ×2 (§3) | Medium — most control, you supply exact VDM bytes | 35 | 6 | **Full controller reset 'Gaid'/'GAID'** = reboot the Ace = hard replug of the whole port | `iecsCommand` selector 2, cmd=`'Gaid'`=0x64696147 (or `'GAID'`=0x44494147), flags=0 | High that it replugs, but **drastic/slow**: drops USB + power + DP on that port, ~seconds | 36 37 Notes that de-risk this: 38 - **There is NO 4CC allow/deny list in the kernel AppleHPM user client.** Selector 2 39 passes `(route, cmd, flags)` straight through to the provider (§2). I scanned every 40 `IOReturn` construction in AppleHPM: **`kIOReturnBadMessageID` (0xe00002c6) is never 41 built anywhere in the kext.** So the `0xe00002c6` you got for `'Gaid'` did **not** 42 come from a kernel 4CC filter — it came from the plugin's own userspace wrapper or 43 from somewhere else on the path (unresolved). 44 **CORRECTION (vr1, verified on hardware 2026-10-20 22:2x):** the earlier advice here to 45 "re-try on the AppleHPMDevice node" is WRONG. `IOCreatePlugInInterfaceForService` on the 46 AppleHPMDevice (RID 1) node returns 0xe00002c7: AppleHPMLib's personality is 47 `IOProviderClass = AppleHPM`, which only the `AppleHPMARMI2C` parent is. `iecsRead` works 48 on the PARENT (not the Device node). On the parent, `iecsCommand` 'Gaid' and 'DRST' both 49 return 0xe00002c6, with and without the com.apple.USBCEntitlement entitlement, and with no 50 kernel log output. Where 0xe00002c6 originates is still being traced. 51 - Start gentle (#1/#2) and keep #6 as the fallback you already wanted. 52 53 --- 54 55 ## 1. Access control (who may open the user client) 56 57 `AppleHPMUserClient::initWithTask` @ **0xfffffe0009b49570**: 58 59 ``` 60 0x…495b4 x1="root" ; IOUserClient::clientHasPrivilege(task,"root") 61 0x…495bc bl clientHasPrivilege 62 0x…495c8 x1="com.apple.USBCEntitlement" ; copyClientEntitlement(task, …) 63 0x…495d0 bl copyClientEntitlement 64 0x…495e8 cmp x0, kOSBooleanTrue ; entitlement must == true 65 … requires BOTH → else logs and returns 0 (open fails) 66 ``` 67 Strings: `com.apple.USBCEntitlement` @0x47b6bb, "does NOT have 68 kIOClientPrivilegeAdministrator" @0x47b6d5, "does NOT have entitlement…" @0x47b733. 69 70 **Requirement: run as root *and* ship `com.apple.USBCEntitlement` (= true).** No 71 per-command privilege check beyond this. 72 73 --- 74 75 ## 2. AppleHPMUserClient external-method dispatch 76 77 Dispatch is **not** an `IOExternalMethodDispatch` array — it is a 78 `getTargetAndMethodForIndex`-style switch @ **0xfffffe0009b497dc** (vtable idx 272) 79 that tail-calls one of the class methods by vtable offset. Argument-count validation 80 is a shared helper at UC vtable **+0x990** (`0xfffffe0009b49b10`, 81 `checkArguments(args, scalarIn, structIn, scalarOut, structOut)`) that returns 82 **`kIOReturnBadArgument` 0xe00002c2** on count mismatch (reads 83 `IOExternalMethodArguments` fields at +0x28 scalarInCnt, +0x38/+0x40 structIn, 84 +0x50 scalarOutCnt, +0x60/+0x68 structOut). 85 86 Selector map (selector → UC handler → forwards to provider vtable offset): 87 88 | sel | plugin vtbl | name | handler | checkArguments | forwards to provider | 89 |----:|-------------|------|---------|----------------|----------------------| 90 | 0 | +0x28 | iecsRead | 0x9b49c24 | struct-out path | reg read | 91 | 1 | +0x30 | **iecsWrite** | 0x9b49e3c | scalarIn **5** | provider +0x218 (mem) then write | 92 | 2 | +0x38 | **iecsCommand** | 0x9b4a044 | scalarIn **3** | **provider +0x8f0**(route,cmd,flags,5) | 93 | 3 | +0x40 | **sendVDM** | 0x9b4a118 | scalarIn **5** | **provider +0x990**(route,SOP,md,len,flags) | 94 | 4 | +0x48 | receiveVDM | 0x9b4a314 | — | provider +0x998-ish | 95 | 5 | +0x50 | receiveVDMAttention | 0x9b4a540 | — | — | 96 | 6 | +0x58 | **forceMode** | 0x9b4a76c | scalarIn **2** | provider +0x938(mode) | 97 | 7 | +0x60 | **forceUpdateMode** | 0x9b4a834 | scalarIn **4** | provider +0x940 | 98 | 8 | +0x68 | IECSAtomicCommand | 0x9b4a908 | scalarIn **2** | provider +0x948(route,u8) | 99 | 9 | +0x70 | EnableOptions | 0x9b4a9d4 | scalarIn **0xb** | up to 4 copyin buffers | 100 | 10| +0x78 | (reserved) | 0x9b4b0e4 | — | — | 101 | default | | | | | **kIOReturnUnsupported 0xe00002c7** @0x9b49a88 | 102 103 Provider object = UC+0xe0, set in `start` @0x9b49660: casts the IOService provider to 104 the required metaclass (GOT `0xfffffe0007f7fcd0` → runtime metaclass `0xc334688`; name 105 is runtime-initialized, this is the **AppleHPMDevice** RID node), then calls 106 `provider->vtbl[+0x6b0]()` to get the command target and stores it at +0xe0. Per-call 107 lock at UC+0xe8 via `IOLockLock`=0x8f1dafc / `IOLockUnlock`=0x8f1da7c. copyin helper = 108 `0x8f3decc`. 109 110 ### Exact ABIs (all `IOConnectCallScalarMethod`, selector as above) 111 112 - **iecsRead (sel 0):** as you use it — `(route, reg u8, …)`, data returned in the 113 struct-output buffer + `outLen`. 114 - **iecsWrite (sel 1):** scalars `[route u32, reg u8, len, userBufPtr, flags]`. 115 Handler (0x9b49e3c): `len` must be **≤ 0x40 (64)**; copies `len` bytes from the user 116 buffer and writes them to register `reg` on `route`. **This is the raw host-interface 117 write** — lets you poke CMD1 (0x08) / DATA1 (0x09) directly. 118 - **iecsCommand (sel 2):** scalars `[route u32, cmd u32(4CC), flags u32]`. Handler 119 (0x9b4a044) only validates "3 scalars" then calls `provider+0x8f0(route,cmd,flags,5)`. 120 No 4CC filtering in the UC. The constant `5` is a fixed source/SOP selector the driver 121 always passes here. 122 - **sendVDM (sel 3):** scalars `[route u32, SOPtype u32, len, userBufPtr, flags u32]`. 123 Handler (0x9b4a118) range-checks `len`, copies the VDM byte buffer in, and calls 124 `provider+0x990(route, SOPtype, IOMemoryDescriptor*, len&0xff, flags)`. `SOPtype` is an 125 `AppleHPMSOPType` passed straight through (the driver's own code used value **5** when 126 issuing a Discover-Identity to SOP′, so the enum is *not* the raw USB-PD SOP ordinal — 127 determine the partner-SOP value empirically; 0 is the natural first guess for SOP). 128 - **forceMode (sel 6):** scalars `[route, mode]` → `provider+0x938(mode)` 129 (drives the HPM mode register / `overrideHPMMode`). 130 - **forceUpdateMode (sel 7):** scalars `[route, u8, u32, u32]` → `provider+0x940` 131 (firmware/update-mode forcing; less relevant to DP). 132 133 --- 134 135 ## 3. 4CC commands and the raw host-interface encoding 136 137 4CCs the kernel itself materializes (movz/movk sites in AppleHPM `__text`): 138 `'VDMs'`=0x734D4456, `'DRST'`=0x54535244, `'SSPS'`=0x53505353, `'SCfg'`=0x67664353, 139 `'MCDL'`=0x4C44434D, `'HChk'`=0x6B684348, `'DFUf'`=0x66554644, `'CRYm'`, `'!CMD'`, 140 `'Gaid'`=0x64696147, `'GAID'`=0x44494147, `'ADFU'` (@0x483a73, used by `sendHPMReset` 141 "resetting port / out of ADFU"). `AppleHCPM::hpmCmdWrite` @0x9b6cc44 is the software 142 *emulation* path and enumerates the accepted set (incl. VDMs, Gaid/GAID) — it is NOT 143 the live I2C path, but it confirms which 4CCs are "known". 144 145 **Two ways to send a 4CC from user space:** 146 147 1. **Structured:** `iecsCommand` (selector 2) — `[route, 4CC, flags]`. Simplest for 148 data-less commands (`'DRST'`, `'Gaid'`, `'GAID'`, `'SSPS'`). 149 150 2. **Raw TPS6598x host interface** via `iecsWrite` (selector 1), mirroring the TI Ace 151 register model you already see (reg 0x03 = mode "APP ", reg 0x58 = DP SID status): 152 - write the **DATA1 register `0x09`** with the command payload (VDM header + VDOs, 153 or command args), then 154 - write the **CMD1 register `0x08`** with the 4CC (e.g. `'VDMs'` / `'DRST'`). 155 The Ace executes and clears CMD1 on completion; poll reg 0x08 / read result from 156 DATA1 (reg 0x09) or the relevant status register (reg 0x58 for DP). 157 158 --- 159 160 ## 4. How the driver itself drives DP / VDMs (templates to copy) 161 162 ### 4a. The driver's own VDM send ('VDMs') — payload layout 163 `AppleHPMInterface` @ **0xfffffe0009b5d7bc** (log: "role is wrong, issuing discovery on 164 sop′"): 165 ``` 166 mov w8,'VDMs'(0x734D4456) ; str [sp+0x3c] ; the 4CC → CMD1 167 mov w8,#5 ; strb [sp+0x38] ; SOP type byte = 5 168 mov w8,0xACA00121 ; str [sp+0x34] ; VDM header dword (Discover Identity here) 169 … 170 blraa provider+0xb10 (this, reg=**8**, &fourcc, &dataStruct{header,SOP,…}, count=1) 171 ``` 172 So the generic executor is **AppleHPMInterface vtbl +0xb10** = 173 `cmdExecWithData(reg=0x08/CMD1, &4CC, &data, dataLen/VDOcount)`, with the DATA1 payload = 174 `[u32 VDM header][u8 SOP type][VDOs…]`. For a DisplayPort VDM you replace the header/VDOs 175 with SVID **0xFF01** (DisplayPort) structured VDMs: 176 - **DP Status Update** (cmd 0x6) to re-request HPD/status from the partner; 177 - **Exit Mode** (cmd 0x5) then **Enter Mode** (cmd 0x4) to restart the alt-mode session; 178 - **DP Configure** (cmd 0x7) to re-apply pin assignment. 179 180 From user space, reproduce this either via `sendVDM` (selector 3, buffer = `[VDM header 181 dword][VDOs]`, `SOPtype` = partner) or raw via `iecsWrite` DATA1+CMD1='VDMs'. 182 183 ### 4b. 'DRST' / Data Reset — `AppleHPMInterface::exitPDMode` @ 0xfffffe0009b5bc44 184 Reads two IORegistry properties into 0x40-byte buffers — **"Exit PD Mode TX Identity"** 185 (@0x47fd66) and **"Exit PD Mode DRST Data"** (@0x47fd7f) — then: 186 ``` 187 blraa provider+0xb10 (this, reg=8, &'DRST', drstData(0x40 bytes), len=0x40) 188 … log "exitPDMode … DRST complete - status = 0x%08x" 189 ``` 190 `'DRST'` performs a **USB-PD Data Reset**: it tears down and re-negotiates the PD 191 contract and re-enters alt modes = a clean soft replug of the data path. Reachable from 192 user space as `iecsCommand(route, 'DRST', 0)` (data-less form) — the property-driven 193 TX-Identity/DRST-Data payloads are optional refinements the driver supplies internally. 194 195 ### 4c. DP status / HPD plumbing (for understanding, not a direct trigger) 196 - `configureDP` @0x9b40160 / `createDPPortObject` / `configureDPPortObject` — build/activate 197 the DP transport; re-run on port re-evaluation. 198 - `sendDpStatusUpdates` @ **0xfffffe0009b58490** — reads the cached DP status word at 199 `this+0x1244`, **HPD = bit 15** (`ubfx …,#0xf,#1`), and on change sets IORegistry 200 property **"HPDAsserted"** (`this+0x298 setProperty`) and pushes HPD down to the DP 201 transport (`this+0x770`). This is consistent with your reg 0x58 decode (partner DP 202 Status VDO, HPD bit). 203 - `communicateDPHPDChange` @0x9b3fc14, `clearDpIRQ`, `resetDataControl`, `repeaterReset`, 204 `disablePort`, `forcePortEvaluation` (@0x9b4764c and others) — the internal re-eval / 205 teardown primitives; `forceMode`/`forceUpdateMode` (selectors 6/7) are the 206 user-reachable entry points that can re-drive this logic. 207 208 --- 209 210 ## 5. Boot-args and settable properties 211 212 PE_parse_boot_argn = `0xfffffe0008fed024`. AppleHPM boot-args (all config/diagnostic — 213 **none force a DP reset or change HPD timing**): 214 `hpmI2CRetries`, `hpmI2CRetryInterval`, `hpmI2CNonCompliant`, `hpmDisableAppleUVDMSOP`, 215 `hpmDisableAppleUVDMSOPp`, `-hpmUnplugForceUpdateMode`, `hpmlog` (log mask). There is no 216 "HPD Lost period" / "hot plug delay" boot-arg in this kext (searched; absent). 217 `hpm-exit-dfu` (@0x9b68514) and `boot-command` are IORegistry/NVRAM properties, not 218 boot-args. No `setProperties` key in AppleHPM was found that triggers a DP re-entry from 219 user space — use the selectors instead. 220 221 --- 222 223 ## 6. IOAccessoryManager / IOPortFamily user clients (item 4) 224 225 `IOPortFamily` is a stub; everything is in **IOAccessoryManager**. Relevant class 226 `IOPortTransportStateDisplayPort` exposes: 227 - `setHPDState` @0xabe5adc (log "hpdState: %d"), `HPD_State` property, `triggerHPDIRQ` 228 @0xabe5f98 (log "HPD IRQ triggered!"), `setDPOverdriveActive` @0xabf17bc / `DPOverdrive`, 229 `setDisplayPortPinAssignment` @0xac13718 / `DisplayPortPinAssignment`, 230 `getDPDNConfig` / `configureDPDNConfigOverrideGated`. 231 232 **Important:** these operate on the *OS-side* DP transport node — they set/propagate the 233 HPD state and pin config that the display pipeline sees. They are driven **from the HPM 234 side** (AppleHPM → `communicateDPHPDChange` → `setHPDState`). Faking HPD here makes macOS 235 re-examine its view of the link, but it does **not** make the Ace re-run alt-mode 236 negotiation with the physical adapter. For the VR "the adapter dropped DP" case, the 237 AppleHPM paths in §4 (which talk to the controller/partner) are the right lever; 238 `triggerHPDIRQ` is at best a complementary "make the GPU re-read EDID / re-train" nudge. 239 240 User clients and their gates: 241 - `IOPortTransportUserClient` — entitlement **`com.apple.iokit.IOPort.user-access`** 242 (@0x898584; TRM variant `com.apple.iokit.IOPort.TRM.user-access` @0x89855d). Class refs 243 @0xabc7984/0xabdf764/0xabdf898/0xabe041c; entitlement check @0xabdfba8. 244 - `IOPortFeatureLDCMUserClient` — `IOExternalMethodDispatch2022` array @ 245 **0xfffffe00082890f0**, 5 methods, each requiring entitlement 246 **`com.apple.iokit.IOPortFeature.LDCM`**, each `scalarIn=1`. (LDCM = the SBU/pin 247 low-data-rate config path; ties to `setLDCMPin` in AppleHPM.) 248 - `IOPortUserClient`, `IOAccessoryManagerUserClient`, plus AID-bus reset primitives 249 `resetAccessoryPowerGated` @0xabc1c24, `sendBusReset` @0xac245f0, `goldbondDisablePort` 250 — these reset the **accessory-ID bus**, not the USB-C/DP data path; not useful here. 251 252 No external method in these clients was found that re-runs USB-C DP alt-mode negotiation; 253 they manipulate OS-side transport/HPD/pin state only. The AppleHPM user client (§2–§4) 254 remains the correct path. 255 256 --- 257 258 ## 7. Suggested concrete attempts (in order) 259 260 1. On the **AppleHPMDevice** node (where `iecsRead` works), retry 261 `iecsCommand(route=0, 'DRST', 0)` — data-less Data Reset. Watch reg 0x58 HPD bit. 262 2. If that is too heavy or no-ops, `sendVDM(route=0, SOP=partner, DP Status Update VDM)`; 263 then `sendVDM` Exit Mode + Enter Mode (SVID 0xFF01) for a full alt-mode replug. 264 3. If structured sendVDM mis-encodes, do it raw: `iecsWrite(0x09, <VDM bytes>)` then 265 `iecsWrite(0x08, 'VDMs')`, mirroring §4a (SOP byte + header + VDOs). 266 4. `forceMode` / `forceUpdateMode` to force the driver's port re-evaluation. 267 5. Fallback: `iecsCommand(route=0, 'Gaid', 0)` (or `'GAID'`) — full Ace reset = guaranteed 268 replug of the whole port, slow; expect USB + charging + DP to drop and re-enumerate. 269 270 Caveat: everything here is static analysis; argument *values* that are enum-like 271 (`SOPtype` for sendVDM, `mode` for forceMode, exact DP VDM VDOs) could not be verified 272 without touching hardware. Start with the data-less `'DRST'` and `forceMode` paths, 273 which have the least ambiguity. 274 275 --- 276 277 ## 8. CORRECTION: selector 2 expects BIG-endian (C multichar) 4CCs (vr1, static, 2026-10-20 ~[time redacted]) 278 279 - `0xfffffe0009b8c830(this, x1=route, x2=cmd, x3=flags, x4)` matches the user client's 280 `provider+0x8f0(route, cmd, flags, 5)` call by signature (VERIFIED: prologue moves x1..x4 into 281 x21/x23/x20/x22). Reached via vtable only (no direct BL callers), so the +0x8f0 -> 0x9b8c830 link 282 is CONJECTURE until the vtable is resolved. 283 - At `0x9b8c970`: `rev w8, w23` (byte-swap cmd), then `this->vtbl[+0x8c0](this, route, 8, &w8, 4)`, 284 i.e. a 4-byte write of the SWAPPED value to register 0x08 (CMD1). VERIFIED in disassembly. 285 - Neighbouring code builds 4CCs as BE constants (LE-read 'sMDV' = BE 'VDMs', 'pelS' = 'Slep'). 286 - So on this path pass 'HRST' = 0x48525354, 'DRST' = 0x44525354, 'Gaid' = 0x47616964. 287 The earlier "4CCs are little-endian ASCII" advice (section 2 / summary) holds for code that stores 288 4CC bytes directly (e.g. exitPDMode's 'DRST' at 0x9b5bbd4), NOT for selector 2. 289 - Consequence: the 'Gaid'/'DRST' (vr1) and 'HRST' (vr0) attempts were sent LE, arrived byte-reversed 290 ('diaG', 'TSRD', 'TSRH'), and !CMD in CMD1 is the chip rejecting those. They remain UNTESTED. 291 - Flags bit0 (x3) gates a branch at 0x9b8c8b0 that skips a pre-check; use flags 0. 292 293 ### 8a. Endianness boundary, CONFIRMED on hardware (vr0, 2026-10-20 [time redacted]) 294 295 | layer | 4CC representation | evidence | 296 |---|---|---| 297 | AppleHPMLib plugin `iecsCommand` (vtbl +0x38) | passes the u32 through unchanged as scalar #2 of selector 2 | plugin disasm 0x2614-0x2668 (vr1, vr0 independently) | 298 | AppleHPMUserClient selector 2 handler 0x9b4a044 | passes u32 unchanged to provider+0x8f0(route, cmd, flags, 5) | static (subagent) | 299 | provider+0x8f0 = 0x9b8c830 | **byte-swaps** (`rev w8,w23` @0x9b8c970) then writes 4 bytes to CMD1 (reg 0x08) | static (vr1); vtable slot -> 0x9b8c830 confirmed by vr0 chained-pointer scan (7 slots, PAC discriminator 0x191d matching the UC +0x8f0 call) | 300 | driver-internal helpers that store 4CC bytes to memory first (e.g. exitPDMode 'DRST' @0x9b5bbd4: `mov #0x5244; movk #0x5453` -> 0x54535244 stored LE) | little-endian ASCII in memory | static | 301 302 **Rule for user space (selector 2): pass the C multichar big-endian value**, e.g. `'DRST'` = 0x44525354, 303 `'HRST'` = 0x48525354, `'Gaid'` = 0x47616964. Do NOT copy the little-endian constants seen in the 304 driver's internal helpers. 305 306 Hardware results with corrected BE encoding (vr0): 307 - `DRST` 0x44525354 -> IOReturn 0x00000000 in 50 ms (140.855422). Accepted. 308 - `HRST` 0x48525354 -> 0xe00002c6 (126.592422). Rejected (reason TBD: see 0x9b8c830 return paths). 309 - All earlier LE attempts (Gaid, DRST by vr1; HRST by vr0) were byte-reversed on the wire. 310 311 ### 8b. Return codes of provider+0x8f0 = 0x9b8c830 (vr1, static, VERIFIED in disassembly) 312 - Pre-check: reads CMD1 (vtbl+0x8d0, reg 8); if a previous command is still pending it clears CMD1 first. 313 - Writes rev(cmd) to CMD1 (vtbl+0x8c0, reg 8, 4 bytes). 314 - flags bit1 set: sleep 100 us, return 0 (fire-and-forget). 315 - Otherwise polls CMD1 every 100 us (w0=0x64 delay), up to 1000 tries (flags bit0 clear) or 100000 (bit0 set): 316 - CMD1 == 0x444d4321 (LE u32 of "!CMD") -> returns 0xe00002d8 - 0x12 = **0xe00002c6 (kIOReturnBadMessageID)** 317 = firmware did not recognize the command. 318 - CMD1 == 0 -> returns 0 (accepted/completed). Task result is then in DATA1 (reg 0x09) byte 0. 319 - tries exhausted -> 0xe00002d8 - 2 = **0xe00002d6** (timeout). 320 So with correct BE encoding: HRST -> 0xe00002c6 means this CD3217 APP firmware does not implement HRST; 321 DRST -> 0 means it does. 322 323 ### 8c. SOFTWARE RECOVERY CONFIRMED (vr0 hardware, 2026-10-20) 324 Stuck state (adapter partner status HPD=0 since [time redacted], induced by HID screen_off) recovered by 325 `iecsCommand(route 0, 'DRST' = 0x44525354, flags 0)` on RID 1 (Port-USB-C@2): 326 returned 0 in 50 ms at 140.855422; CMD1 = 0, DATA1 byte0 = 0; display ADD 4320x2160@60 at 327 143.064422 (+2.209 s). No physical replug. Repeatability trials in progress (vr0). 328 329 ## 9. User-client access gate is OR, not AND (vr1, static, VERIFIED in disassembly) 330 `AppleHPMUserClient::initWithTask` @ 0xfffffe0009b49570: 331 - super::initWithTask; on failure -> false. 332 - w21 = IOUserClient::clientHasPrivilege(token, "root") (0 = caller is root) 333 - ent = copyClientEntitlement(task, "com.apple.USBCEntitlement"); w22 = (ent == kOSBooleanTrue) 334 - allow if (w21 == 0) || w22 (branches 0x9b49614 / 0x9b4961c / 0x9b49620 / 0x9b49624) 335 - deny: logs two strings (0x747f6d5, 0x747f733), returns false -> plugin creation fails (seen as 0xe00002be). 336 => An unprivileged (non-root user) process ad-hoc signed with com.apple.USBCEntitlement=true is admitted (AMFI is off, 337 so the entitlement is honoured). No sudo needed. Matches hardware: unsigned non-root user failed; signed non-root user read OK. 338 Per-selector: handlers seen (0, 2, 3) only validate argument counts; no extra privilege check observed on the 339 selector-2 path. Not every handler audited. 340 341 ## 10. Unattended recovery: integration review (vr1, design, no hardware) 342 - Privilege: either sign the app itself with com.apple.USBCEntitlement, or (narrower) a small signed helper the 343 app execs. No root, no sudo cache. Both rely on AMFI staying off (amfi_get_out_of_my_way=1). 344 - Target selection, once per attempt, never polled: AppleHPMDevice with RID == 1 whose child is 345 `Port-USB-C@2` (PortNumber 2), and open the plug-in on its AppleHPMARMI2C parent. Refuse RID 0 (hub, headset 346 USB, Mac power). Optional: confirm reg 0x03 == "APP " and reg 0x58 byte0 bit1 (DP alt mode active) before DRST. 347 - Command: iecsCommand(route 0, 0x44525354 'DRST', flags 0). Driver itself polls CMD1 for ~100 ms. 348 Check: return 0 -> then read DATA1 (reg 0x09) byte0 == 0. 0xe00002c6 = firmware !CMD; 0xe00002d6 = timeout. 349 - Bound it: whole helper run <= ~1 s; app waits for CGDisplay ADD of vendor 8718 with a timeout (e.g. 5 s; 350 observed +2.2 s), then re-captures. Rate-limit (e.g. max 3 DRST per minute, exponential backoff) and log 351 every attempt with epoch timestamps + return codes. 352 - Trigger: headset display REMOVE (track the captured CGDirectDisplayID, not vendor of a removed id) and no 353 ADD within a short grace window. Self-heals were ADD within ~0.7-0.8 s, so a ~1 s grace avoids DRST 354 racing a self-heal; tune with trial data. HID 0x05 panel-off is a secondary signal only. 355 - Never read IOPortTransportStateDisplayPort properties in a loop; one-off reads only, or none.