Skip to content

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_properties entirely. fetch_device_properties is called by the swizzled retrievePropertiesForUUID: 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() BEFORE iosmux_install_md_proxy() in iosmux_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 0 it: 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)

  1. Add a small inline helper in iosmux_md_proxy.m:

    static NSDictionary *xpc_dict_to_nsdict(xpc_object_t xd);
    
    Walks via xpc_dictionary_apply, builds NSMutableDictionary.

  2. Rewrite fetch_device_properties in iosmux_md_proxy.m:

  3. No HTTP fetch
  4. Call iosmux_rsd_get_handshake_properties() (already declared extern in iosmux_inject.m:25 area; need similar extern in md_proxy)
  5. If NULL, return nil with a warning log (means the call came too early — should never happen with lazy fetch)
  6. If non-NULL, convert to NSDictionary, add @"LocationID": @0, cache, return

  7. Delete prefetch_device_properties and its call site in install_proxy_on_connection. Add a comment at the call site explaining why prefetch was removed (init order, lazy fetch is safe).

  8. Remove g_relay_api constant (unused after step 2).

  9. In iosmux_xpc_proxy.m, replace the body of iosmux_build_device_properties with the same in-memory dict approach, OR #if 0 it 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.

  10. 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-info and we are not touching that.
  • The order of iosmux_install_md_proxy and iosmux_rsd_init in iosmux_register_device stays as-is. Lazy fetch removes the ordering dependency.
  • The MD proxy swizzling itself stays as-is (only the body of fetch_device_properties changes).
  • 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.