S1.A.1 — Properties source audit¶
Date: 2026-04-12 (session 8) Stage: S1.A.1 (research only, no code change)
Question¶
Where in our inject does CDS / MobileDevice get device properties from, and what keys does it expect?
Findings¶
1. Two HTTP-fetch sites for 127.0.0.1:62078/device-info¶
| File | Function | Live in current flow? | What it does |
|---|---|---|---|
iosmux_md_proxy.m:30-44 |
fetch_device_properties |
YES — live | HTTP fetch → JSON parse → returns NSDictionary. Used by IosmuxMDProxy.retrievePropertiesForUUID:type:withReply: for our UUID. |
iosmux_md_proxy.m:47-52 |
prefetch_device_properties |
YES — live, at init | Calls fetch_device_properties once during install_proxy_on_connection to warm the cache. |
iosmux_xpc_proxy.m:557-602 |
iosmux_build_device_properties |
dead path | Used by iosmux_proxy_init for g_device_properties. Current flow uses iosmux_rsd_init instead (see below), so this fires only as a fallback if RSD init fails. |
The OS_remote_device._properties slot is NOT touched by any of the
above. It is set at iosmux_inject.m:609-616:
xpc_object_t props = iosmux_rsd_get_handshake_properties();
if (!props) {
LOG("No RSD Handshake properties, falling back to Go relay API");
props = iosmux_proxy_get_properties();
} else {
LOG("Using REAL Handshake properties from iPhone");
}
[device setValue:(__bridge id)props forKey:@"properties"];
So _properties already uses the in-memory handshake dict directly.
The "Failed to load remote device properties" log we see in CDS comes
from a SEPARATE path: MobileDevice's MDRemoteServiceDeviceLoadBaseProperties
calls back into the MDRemoteServiceSupport XPC service (which we've
intercepted via md_proxy), and md_proxy's fetch_device_properties is
the one that hits HTTP and fails.
2. What MobileDevice expects¶
MobileDevice's MDRemoteServiceDeviceLoadBaseProperties consumes the
dict that md_proxy returns. The expected keys are NOT enumerated in
MobileDevice docs but are observable from the live RSD Handshake we
captured at docs/research/rsd-info-ios2641-reference.json (session 8,
2026-04-12 from a fresh iPhone reconnect).
That handshake contains 46 Properties keys. The full list, sorted:
AppleInternal BoardId
BootSessionUUID BuildVersion
CPUArchitecture CertificateProductionStatus
CertificateSecurityMode ChipID
DeviceClass DeviceColor
DeviceEnclosureColor DeviceSupportsLockdown
EffectiveProductionStatusAp EffectiveProductionStatusSEP
EffectiveSecurityModeAp EffectiveSecurityModeSEP
EthernetMacAddress HWModel
HWModelDescriptionForUserVisibility HardwarePlatform
HasSEP HumanReadableProductVersionString
Image4CryptoHashMethod Image4Supported
IsUIBuild IsVirtualDevice
MobileDeviceMinimumVersion ModelNumber
OSInstallEnvironment OSVersion
ProductName ProductType
ProductTypeDescForUserVisibility RegionCode
RegionInfo RemoteXPCVersionFlags
RestoreLongVersion SecurityDomain
SensitivePropertiesVisible SerialNumber
SigningFuse StoreDemoMode
SupplementalBuildVersion ThinningProductType
UniqueChipID UniqueDeviceID
These are the EXACT key names — no rename, no case change. The
xpc_dictionary parsed by our handshake handler at
iosmux_xpc_proxy.m:724-728 has the
same keys verbatim (it's the same XPC dict object, just retained).
Conclusion: no key translation needed. The handshake dict is already in the form MobileDevice expects.
The only synthesized key is LocationID, which the existing
iosmux_build_device_properties injects locally as 0 (xpc_proxy.m:597).
Apple's note from research: "MDRemoteServiceDeviceLoadBaseProperties
checks LocationID first." This means LocationID must be present (any
value, 0 is fine for our injected device).
3. NSDictionary vs xpc_object_t mismatch¶
md_proxy.fetch_device_properties returns NSDictionary * (because
the rest of md_proxy's code paths use ObjC types — XPC reply blocks,
dictionary literals like @{@"state": @"connected"}).
g_rsd_handshake_properties is xpc_object_t (XPC dict).
The conversion direction we need: xpc_object_t → NSDictionary. This is possible because XPC dicts and NSDictionaries are isomorphic for the value types involved (string, bool, int, possibly uuid).
A clean conversion helper: walk xpc_dictionary_apply and build an
NSMutableDictionary. Each value type maps directly:
| XPC type | NS type |
|---|---|
xpc_string |
NSString |
xpc_int64 |
NSNumber numberWithLongLong: |
xpc_uint64 |
NSNumber numberWithUnsignedLongLong: |
xpc_bool |
@(YES/NO) |
xpc_uuid |
NSUUID (for BootSessionUUID) |
We can either write this helper inline in md_proxy.m or as a shared util. Inline is simpler for one call site.
4. Init order trap¶
iosmux_install_md_proxy is called at iosmux_inject.m:645
BEFORE iosmux_rsd_init at line 648. Inside install_md_proxy,
prefetch_device_properties runs immediately. So at the moment
prefetch fires, g_rsd_handshake_properties is still NULL.
Two ways to fix this without reordering init:
- Option A — make fetch lazy: drop
prefetch_device_propertiesentirely.fetch_device_propertiesis called by the swizzledretrievePropertiesForUUID:only when MobileDevice actually requests properties for our UUID — that happens AFTER CDS finishes setup, so RSD is guaranteed to be initialized by then. - Option B — reorder init: move
iosmux_rsd_init()BEFOREiosmux_install_md_proxy()iniosmux_register_device. md_proxy install only swizzles methods, no dependency on having a remote device yet.
Option A is cleaner because it removes a class of init-order
coupling. Prefetch is an optimization, not a correctness requirement —
the swizzled method can fetch on first use. There is no measurable
latency cost: g_rsd_handshake_properties is already a process-memory
dict, conversion is microseconds.
Decision: drop prefetch, make fetch_device_properties lazy and
read from g_rsd_handshake_properties directly.
5. Dead-path cleanup in xpc_proxy.m¶
iosmux_build_device_properties in iosmux_xpc_proxy.m is currently
dead in the live flow (RSD path is taken instead). But it remains a
fallback if RSD init fails. Two options:
- Leave it alone: it's a fallback we hope to never use, and if we do use it, the relay would need to be running anyway for any other reason.
- Convert it: also make it use
g_rsd_handshake_properties. But then the fallback purpose is lost — if RSD init failed, the dict is also empty. - Delete it /
#if 0it: declare the fallback dead, since the live flow always succeeds at RSD init.
Per the project rule (correct/grounded solution), the right move is
to clean up dead code. Convert the xpc_proxy version to use the
in-memory dict for symmetry, AND remove the HTTP fetch path entirely
(or #if 0 per the S0.3 pattern). This gets rid of the relay's
property-feeding role completely. The relay still has other roles
(per-service TCP relay, see internal/relay/daemon.go), so we don't
delete the whole binary, but its /device-info endpoint becomes
unused.
Mark /device-info as deprecated in the Go relay or leave it as a
no-op endpoint for now — that's a follow-up detail, not blocking S1.A.
S1.A.2 implementation plan (informed by this audit)¶
-
Add a small inline helper in
Walks viaiosmux_md_proxy.m:xpc_dictionary_apply, buildsNSMutableDictionary. -
Rewrite
fetch_device_propertiesiniosmux_md_proxy.m: - No HTTP fetch
- Call
iosmux_rsd_get_handshake_properties()(already declared extern iniosmux_inject.m:25area; need similar extern in md_proxy) - If NULL, return nil with a warning log (means the call came too early — should never happen with lazy fetch)
-
If non-NULL, convert to NSDictionary, add
@"LocationID": @0, cache, return -
Delete
prefetch_device_propertiesand its call site ininstall_proxy_on_connection. Add a comment at the call site explaining why prefetch was removed (init order, lazy fetch is safe). -
Remove
g_relay_apiconstant (unused after step 2). -
In
iosmux_xpc_proxy.m, replace the body ofiosmux_build_device_propertieswith the same in-memory dict approach, OR#if 0it and leave a comment that the fallback path is dead. Decision: convert it for symmetry. Keeps the fallback functional and removes the HTTP dependency from the inject entirely. -
Build, deploy, test against S1.A.4 exit criteria.
What we explicitly do NOT change in S1.A¶
- The Go relay binary stays. Its per-service TCP relay role is
separate from
/device-infoand we are not touching that. - The order of
iosmux_install_md_proxyandiosmux_rsd_initiniosmux_register_devicestays as-is. Lazy fetch removes the ordering dependency. - The MD proxy swizzling itself stays as-is (only the body of
fetch_device_propertieschanges). - DeviceInfo construction (the separate Swift object built later in
iosmux_inject.m) is untouched — that's a different consumer.
Risk¶
The only meaningful risk is that MobileDevice rejects our dict for a reason unrelated to keys (e.g. value type mismatch — strings vs numbers, signed vs unsigned). The conversion helper handles all observed types from the reference handshake. If there's a stray unsupported type at runtime we'll log it and fall through.