vphone-duo (duobase-01) against the simulator baseline: notes
VM duobase-01, bundle 2.14.0-local.21dc0c06 (upstream main 82afdb9a, two-display Duo path),
iPhone19,4, iOS 27.1 24A94425. Probes run through tools/vm-probe.zsh (guest debugserver,
host lldb over NAT). Raw transcripts under raw/vm/.
EXC_GUARD (code 261, subcode 0xd63f091f) in lldb expressions: isolated
Evidence: raw/vm/guard/ (diag1, b1, b2, b2-compile, b2-unwind, t1, t23, t4, tv, tfix; JIT
objects saved with target.save-jit-objects-dir in jit*/).
- lldb sees guest processes as
arm64e.x1-apple-ios27.1.0and JITs arm64e code (PAC calls), and the guest enforces the IA key. code 261 = 0x105 =EXC_ARM_PAC_FAIL; subcode 0xd63f091f is the instructionblraaz x8. - Bisecting
smoke-springboard.lldb: the[UIScreen screens]half with all its blocks runs (b1). Adding the coordinator half (b2) faults every time. expr -u falseleft SpringBoard at the fault (b2-unwind):blraaz x8withx8 = 0x0000000188000800 libobjcMsgSend.dylibobjc_msgSend` — an unsigned pointer.- The JIT objects show why. A cast call
((T (*)(…))objc_msgSend)(…)becomes a ptrauth constant (IA, discriminator 0). lldb signs such constants in the target, in the module initializerlldb.arm64.sign_pointers(mov x16,#0; pacia1716), into a private slot (l___unnamed_N). But only one call site uses that slot. Every other cast-call site ofobjc_msgSendin the same expression loads the plain GOT entry_objc_msgSend(the one ordinary message sends use withblr), which lldb fills with the raw address and never signs, and calls it withblraaz(tv: v1–v4, any return types, with or without loops). - Confirmed by execution in Preferences (tfix): two cast
objc_msgSendcalls in one expression → EXC_GUARD, process killed. One cast call (t1), one cast call inside afor … inloop (t23, t4), blocks, nested blocks: all fine.
Trigger: two or more cast objc_msgSend call sites in one expression. Size, blocks and
loops are irrelevant. smoke-springboard.lldb has two (displayContentModeForWindowScene: and
isAppliedDisplayContentModeUserInitiatedForWindowScene:). The simulator never hits it
because simulator processes are arm64 (no PAC). The same probably holds for any other C
function referenced both directly and through a cast in one expression; not tested.
Fix used by every probes/vm-*.lldb: take the pointer once and cast the variable:
void *vpMsg = (void *)objc_msgSend; then ((long (*)(id, SEL, id))vpMsg)(…). One reference
→ one signed slot (tfix "fix ok", two types, inside a for … in). A simulator probe with at
most one cast objc_msgSend runs unchanged; one with more needs this one-line substitution
(sed 's/))objc_msgSend)/))vpMsg)/' plus the declaration).
SpringBoard restarts caused while isolating it: 522 → 647 (b2), 647 → 706 (b2-unwind). Preferences was killed once on purpose (tfix).
Two more JIT traps on the VM, and one guest-code trap
strcmp/strncmpreturn garbage in expressions (raw/vm/guard/strncmp.txt:strncmp("{CGRect=x", "{CGRect=", 8)= -1944147536,strcmp("ab","ab")≠ 0;strlenis right). lldb finds libsystem_c'sstrncmpwith no address and binds something else. Every sim probe that dispatches on a type encoding (display-*,chrome-state) silently took the "unsupported struct return" path, and skip lists keyed onstrcmpstopped skipping.probes/vm-convert.zshnow also declares block versionsvpStrncmp/vpStrcmpfirst in each expression and rewrites the calls.- EXC_BAD_INSTRUCTION, subcode 0xdac1a7fe =
PACIBSPPC(FEAT_PAuth_LR, the "x1" part ofarm64e.x1). The VM's CPU does not implement PAuth_LR, which is why the dyld cache is X1-lowered; code that is not lowered faults on its first instruction. Found in backboardd:objc_copyClassListrealizes every class, and one of them runs unlowered code (raw/vm/guard/bbd*.txt; markers written to/var/tmp/vpprog.txtin the guest located it).IOHIDServiceCopyProperty/IOHIDServiceGetRegistryIDthroughdlsympointers are fine.probes/vm-sensors-backboardd.lldbchecks named classes instead (the sim list was empty anyway). Avoidobjc_copyClassListin guest probes. Each of these faults was returned to the pre-expression state by lldb; backboardd (pid 61) survived all of them. display.auto_lock.setdid not change the effective Auto-Lock here: requests for 0 and 2147483647 (Never) both read back 60 s, also 10 s later. It was set back to 60 s at once. For measurements the probes take SpringBoard's own idle-timer disable assertion instead (probes/vm-springboard-disable-autolock.lldb, as in the sim), which a SpringBoard restart drops.probes/vm-sensors-coremotion.lldb: three more VM-only changes.?: [NSNull null]on the handler dictionaries (the VM's_cmMagicPoseManageris nil, so the sim's literal threw);+[CMAngleManager shouldEmitWakeLatencySignpostsOnStateChangeForTesting]not called (EXC_BAD_ACCESS at 0x4c); no fresh[[CMAngleManager alloc] init](it blocked SpringBoard's main thread until lldb gave up with SIGSTOP). SpringBoard restart 706 → 1440 followed those two runs (start time 20:20:03, seconds after the SIGSTOP run detached).probes/vm-display-backboardd.lldb, VM-only changes beyond vm-convert:-[CADisplay _copyDebugProperties]not called (SIGABRT in backboardd; the VM's CADisplays have nostateControl),+[BKDisplayBrightnessController sharedInstance]and+[BKDisplayRenderOverlayManager sharedInstance]not called (both EXC_BAD_INSTRUCTION 0xdac1a7fe, the same unlowered PAuth_LR code). Found withsrc/vm/display/iterate-vm.zsh(the VM version ofsrc/display/iterate.zsh: reads the guest's/var/tmp/vptrace.txt) and, because the last traced getter is not always the culprit (it walks backwards through the getter list when the fault comes later), a debug copy that traces every record add (src/vm/display/dbg.lldb). backboardd survived every interrupted expression (pid 61 throughout).
How the VM column was measured
- Posture: vphoned
display.posture({"hinge":"closed","position":"closed"},{"hinge":"partially-open","position":"book"},{"hinge":"open-flat","position":"open"}), which sets SpringBoard's test-automation pose override and sweeps the virtual hinge HID. The sim used SpringBoard's own display-swap coordinator instead; values fromSBFDevicePoseProvideronward compare. - Rotation:
display.rotation {orientation: degrees}(lit-panel interface orientation; inner 0 landscape-left, 90 portrait, 180 landscape-right, 270 upside-down; cover 0 portrait, 90 landscape-right, 270 landscape-left). - Each step:
probes/vm-run.zsh snap(screen.unlock, display.pose, display.orientation, device.screen, a screen.screenshot with its size and lit/dark, the SpringBoard pid, then the probes), driven bysrc/vm/run-plan.zsh(phases postures, rotations, safari, preferences),src/vm/run-autolock.zshandsrc/vm/run-lockchrome.zsh. Step order:raw/vm/steps/ORDER.probes/vm-retag.zshrebuilds each step'srecords.jsonlfrom the transcripts;probes/vm-merge.zshwritesvm-<domain>.json. Helpers used to read the results:probes/vm-summary.zsh(posture table),vm-chrome-keys.zsh,vm-app-keys.zsh,vm-compare.zsh(sim vs VM by api+call, inner display names mapped). - SpringBoard's idle timer was held off with its own disable assertion
(
vm-springboard-disable-autolock.lldb, re-taken after a SpringBoard restart), as the sim'sSBDisableAutoDimdefault does; the guest's Auto-Lock setting stayed 60 s. - The VM shows one composited panel (the cover
LCD, the only AppleParavirtDisplay, 1398×2034).screen.screenshotcaptures it: 1398×2034 portrait when closed, 2034×1398 when book/open (the inner content scaled onto the landscape panel). "Which panel is lit" therefore comes from SpringBoard (content mode, backlight state) and backboardd (window-server enabled/blanked), with the screenshot size as a cross-check. display.rotationreadsorientationonly as a JSON string ({"orientation":"90"}); a number is silently ignored and the call just reads back. The first rotation phase used numbers; those steps are inraw/vm/steps/discarded/*-numeric-orientation-ignoredand were all re-run (run-plan.zsh rot2).-[SBLockScreenManager lockUIFromSource:withOptions:]on the primary scene worked in open but hit EXC_BREAKPOINT in closed (raw/vm/steps/closed-lockscreen-pose/lockcmd.txt, lldb returned SpringBoard to its prior state). Stepclosed-lockscreenis therefore not locked; the cover lock screen was measured with a simulated lock-button press instead (closed-lockscreen-button).
Data
Steps, in order (raw/vm/steps/ORDER; the posture label is in each record):
open-sbstart, closed-after-open, book-after-closed, open-after-book, book-after-open,
closed-after-book, open-after-closed, Safari (closed, unfold to open, book, fold),
Preferences (closed, book, open, fold), rotations (closed Preferences 90, closed Safari 90 /
270 / 0, closed home 90, open 0 / 180 / 270 / 90), history (Preferences launched on the
inner then folded, Safari with the cover landscape then unfolded), lock screen (open,
closed), auto-lock (open auto-locked, folded, 20 s later, fold after a lock-button press),
closed lock screen via lock button. raw/vm/rpc/ holds the boot-state display.modes and
device.displays. Not run: VPAppKitProbe (callback sequence, public UIHingeInteraction),
notification pushes (platter widths), CMQuery/blocking probes.
Results worth reading before the table
- Posture policy, primary scene, content modes and SpringBoard backlight states match the
sim in every posture and history path. Book is the only posture with a value off: the
override derives 90° from hinge state 2 (
display.posture's reply saysangle 100and the HID gets 100°, butSBFDevicePoseProvideranduiHingeStatusread 90°). - Display stack: the VM keeps one real panel (
LCD, 1398×2034 @3, 120 Hz) and adds the inner as a second CADisplayVPhone Duo Inner(displayId 7, native 2853×2007 landscape, pointScale 1, sRGB, no physical size). No CADisplay has astateControl. FBS/UIKit see identityUnknown[7](apps:Unknown.main[7]), not UIKitMainLike, not embedded. FBS bounds, sbh screen types, exclusion area, BKS surface type, window-server orientations and the inner rot90 all match. - Everything an app sees matches: screens count, main screen, bounds, size classes,
orientation, safe areas, corners, surface type, scene regions with
innerCoveractive only in book, the Preferences split widths, hinge-state delivery, and both history effects on client orientation. - Rotation matches in open (4/1/2/3, per-panel pose orientations). Closed, the cover's pose orientation equals the device orientation (sim: the opposite landscape). The cover home screen stays portrait (mask 2) and vphoned reports the rotation as not applied; a landscape app (Safari) does turn the cover landscape. The earlier "VM refuses cover landscape outright" was the home-screen case.
- CoreMotion: no magic-pose, device-state or angle manager (all
isAvailableNO); the class-method tables (orientationForMagicPose:deviceSurface:,propertyAForConfigType:…) are CoreMotion's own and equal the sim's everywhere, tabletop and A=7 included. - No AOD on the VM, so auto-lock turns the panels off instead of dimming them; folding while auto-locked moves primary to the cover lock screen with the cover off. Nothing lit the folded inner.
SBFDevicePoseProvider:internal.isOnVMYES (sim NO),_accelerometeris a BKSAccelerometer (sim nil),SBHingeMotionUnintendedInteractionMonitornil.- After
screen.unlockin open, the cover scene's lock-screen manager stays locked and visible while the inner is unlocked (sim: both unlocked). It clears after a fold.
SpringBoard restarts during this work
backboardd (pid 61) never restarted. Preferences was killed once on purpose (tfix).
Final guest state
duobase-01 running, SpringBoard pid 2384, open (display.pose hinge 3), device portrait
(inner interface orientation 4), unlocked, Safari and Preferences terminated, Auto-Lock
setting 60 s (unchanged; display.auto_lock.set was set back to 60). SpringBoard holds the
probe's idle-timer disable assertion ("vphone baseline: auto-lock off again") so the guest
stays unlocked; it goes away with the next SpringBoard restart.
2026-10-30: second pass after the duo-sim-align fix
Bundle 2.14.0-local.9064efca (branch duo-sim-align, worktree HEAD aa274dcc at
measurement time): V68 product-id, /product/display0 + display1 panel nodes,
MGGetLogicalDeviceDisplayCount 2, CoreMotion's device-state relay fed by libvphonehinge's
VPhone Host Controls (0xFF61/0x5B). Same VM, same iOS. Only the rows the fix can affect
were measured; BASELINE.md marks them "2026-10-30" and keeps the first-pass value as
"(was: …)".
Method
- Driver
src/vm/run-fix.zsh(phases respring, postures, postures2, statusbar, coremotion, safari, lock, final), same probes as before. Every lldb attach waits until 45 s have passed since the previous one ended, postures settle 20 s and rotations 25 s before any attach, and the SpringBoard pid is read before and after each attach. Steps areraw/vm/steps/fx-<label>/, in order inraw/vm/steps/ORDER.fix-9064efca; logsraw/vm/plan-fix-*.log.src/vm/merge-fix.zshappends them tovm-*.jsonwith"bundle": "2.14.0-local.9064efca"andstepfx-<label>(meta:meta.fix, and the step snapshots inmeta.stepscarry the bundle). Re-running it replaces the fx records. display.posereads SpringBoard's test-automation snapshot, which this bundle no longer writes;rpc.jsonkeeps it asdisplay_pose_stale. The pose comes fromSBFDevicePoseProvider(sensors-springboard), anddisplay.posturerepliessource: coremotion.- HID service names come from vphoned's
device.input_devices(stepfx-hid-rpc, recordvphoned.device.input_devicesinvm-sensors.json), not from lldb (next section). - CoreMotion managers:
sensors-coremotionran in MobileSafari first as a canary (now that+isAvailableis YES,+sharedManagercreates objects), then in SpringBoard. Neither hung. No freshCMAngleManagerwas created. - Lock screen: two
input.button {"name":"power"}presses in closed (lock, then wake to the lock screen), thenchrome-lockwithout unlocking (fx-closed-lockscreen, lit screenshot).
Incident 1: the backboardd HID probe wedged CoreMotion
probes/vm-sensors-backboardd.lldb in backboardd (step fx-open-after-book, 01:29) was
interrupted with SIGSTOP about 9 s after it started, without output; lldb returned backboardd
to its prior state. From then on SpringBoard's pose stayed at open/180° through closed, open,
book and closed requests (raw/vm/steps/discarded/fx-*-locationd-wedged), although
libvphonehinge kept sweeping and dispatching host-control events (its status counters kept
rising). vphoned's device.input_devices, which copies properties of every HID service, then
hung for 120 s and the guest agent dropped its connection once (raw/vm/steps/fx-diag/).
The probe calls IOHIDServiceCopyProperty for every service from backboardd's main thread.
On this bundle that includes the two CoreMotion_DeviceStateRelay_* services, which
locationd owns; on the first-pass bundle they did not exist and the same probe ran cleanly
(step open-sbstart). Which side was left blocked is not settled. Recovery: SIGTERM to locationd
(processes.kill with force, pid 66). launchd restarted it (1060), and backboardd
(516 → 1061) and SpringBoard (518 → 1064) restarted with it, start times within 0.3 s. After
that device.input_devices answered at once and CoreMotion followed the fold again.
Do not run vm-sensors-backboardd.lldb (or any lldb IOHIDServiceCopyProperty walk in
backboardd) on a guest with the relay; use device.input_devices. systemEventOfType:0x2c
was therefore not re-read in this pass.
Incident 2: SpringBoard 1064 had no idle-timer assertion
The restart in incident 1 dropped the probe's idle-timer assertion. In the next run the
guest auto-locked in closed; the unfold woke it to the lock screen, isUILocked read YES,
the resolver's prevailing policy read Yoink(inner) in open and the following book, and one
unfold made while the screen was dark was never published (SpringBoard stayed on the cover,
device.screen 466×678, while the hinge provider was at 180°). display.rotation was refused
while locked. Those steps are in raw/vm/steps/discarded/fx-*-sb1064-autolock-on and
fx-sb-open-aborted / fx-open-rot0-aborted. The assertion was taken again
(raw/vm/autolock/fix-disable-pid1064.txt), the fold resynced with the screen lit (closed,
then open), and the posture paths re-run. run-fix.zsh now stops a step when SpringBoard
does not hold the assertion (keep).
Results
- Posture table: FullyClosed / FoldingDual, primary scene, content modes 2/1 and 3/2, backlight 1/4 and 4/1 match in closed, book and open on both history orders. Book reads 99.444° (position 0.552; mechanical 100); closed region 0→0.2747 and toClosed 0.5525→0.03 in book, 0→0.47 otherwise.
- CoreMotion: all three managers available, SpringBoard's provider holds CoreMotion's
managers (handlers registered, angle updates at 0.01 s),
sharedManagerexists and is a separate instance,_accelerometernil: the sim's structure, with live values. - HID:
CoreMotion_DeviceStateRelay_Crane0xFF0C/0x21 and_DeviceState0xFF0C/0x22 (Virtual), plusVPhone Host Controls0xFF61/0x5B. - Display identity:
extendedPropertiesand MobileGestaltDisplayExtendedPropertiesequal the sim's except the cover corner radii ([5,5,59,59] against [8,8,59,59]); display count 2; inner 460 ppi, physical size 6.2022×4.3630 in; inner mode 669×951@3x at 120 Hz, sRGB, with the extra 951×669@3x mode. Names and FBS identity unchanged. - Transition style 2 straight after the forced respring (closed), 2 after every swap, 3 with Safari in front.
- Inner portrait-shaped status bar (interface orientation 1): {592,0,77,951} in both captures. At orientation 1 straight after a SpringBoard start, before any rotation: 0-rect, as in the sim.
- Closed with Safari on the cover: device 4 → cover 4, device 3 → cover 3, from CoreMotion's
cmMagicPose. CoreMotion's table maps a closed hinge to the cover unchanged, so the sim's opposite landscape is its swap tool's. - What Safari sees in closed / book / open matches the sim (screens, bounds, size classes, orientation, safe areas, corners, surface type, regions, hinge state 0 / 99.4 / 180).
- Lock-screen clock: the VM's active lock-screen poster is
com.apple.MercuryPoster:configuration:v68.colorB, a photo poster with an adaptive (stretched) time:adaptiveTimeTextHeight213.01, font weightwght3200000, 128.77 pt, clock 372×240.28. The sim's iscelosia.timeOfDay: adaptive height 105,wght2800000, 110.03 pt, clock 372×132.26. The layout limits (104.74 / 303.68) and styles are the same, so the height difference is the poster's personalization, not layout.
SpringBoard restarts in this pass
No attach changed the SpringBoard pid.
Final guest state (2026-10-30)
duobase-01 running bundle 2.14.0-local.9064efca, SpringBoard 1064, backboardd 1061,
locationd 1060. Open (SBFDevicePoseProvider .openFlat, 180°), device portrait, inner
landscape-shaped home screen, unlocked, Safari terminated. SpringBoard 1064 holds the
probe's idle-timer assertion ("vphone baseline: auto-lock off again") until its next
restart; the Auto-Lock setting is still 60 s.