S1.C — managedServiceDevices Registration Research¶
Date: 2026-04-12
Stage: S1.C.1 (research before implementation)
Scope: Answer three questions blocking S1.C.2 — how to get our injected
SDR into ServiceDeviceManager.managedServiceDevices so that
DeviceManagerCheckInCompleteEvent.initialDeviceSnapshots contains it.
Context¶
Post-S1.A / S1.B state (session 8 deploy):
- DeviceManagerCheckInRequest now reaches the typed dispatcher.
- CDS publishes DeviceManagerCheckInCompleteEvent.
- initialDeviceSnapshots is empty → devicectl prints "No devices found."
- Our inject calls handleDiscoveredSDR (CoreDevice +0x27E850, i.e.
install(browser:) + 0x1080) and updateIdentifier (CoreDevice
+0x27C7E0). Both logs fire. Neither appears to add the SDR to the
managed set.
Note: historical note at docs/research/coredevice-injection.md:189
claims handleDiscoveredSDR alone once made the iPhone appear in
devicectl list devices. That was before S1.A/S1.B landed, when the
codepath was different (check-in was blocked by Step 19; any
"appearance" could only have come from a different flow or a manually
provoked event publication — see Q2 discussion below). The new facts
from session 8 supersede that memory.
Q1. What does updateIdentifier(_:forIdentityManagedDevice:) do?¶
Symbol demangling¶
Mangled: $s10CoreDevice07ServiceB7ManagerC16updateIdentifier_018forIdentityManagedB0yAA0bF0OSg_AA0cB14RepresentationCtF
Breakdown:
- 10CoreDevice — module CoreDevice
- 07ServiceB7Manager — ServiceDeviceManager (Punycode-ish: B =
Device)
- C — class
- 16updateIdentifier — method name, first label unnamed
- _018forIdentityManagedB0 — second label
forIdentityManagedDevice
- y — returns Void
- AA0bF0OSg — param 1 type: CoreDevice.DeviceIdentifier?
(O enum, Sg Optional)
- AA0cB14RepresentationC — param 2 type:
CoreDevice.ServiceDeviceRepresentation
- tF — tuple, function
Resolved Swift signature:
class ServiceDeviceManager {
func updateIdentifier(
_ newIdentifier: DeviceIdentifier?,
forIdentityManagedDevice device: ServiceDeviceRepresentation
)
}
What it does¶
Diagnosis from name + signature (disassembly not available — no CDS binary on Linux host, only backups and source):
The second label is the load-bearing phrase: "for identity managed
device". This means the SDR argument is a precondition, not a
payload: the function assumes the SDR is already identity-managed by
this SDM and updates its associated identifier mapping. The first
argument is an Optional<DeviceIdentifier> — .none likely clears the
mapping, .some sets it — which matches "identity management offer"
semantics elsewhere in the CoreDevice API:
DeviceRepresentationIdentityManagementOffer.accept()returnsDeviceRepresentationIdentityControl(coredevice-representation.md:82-90), i.e. identity control is handed out through an offer/accept ceremony after a device is discovered.DeviceRepresentationProvider.consider(offer:)is the plugin hook for claiming identity control over a newly discovered device.
So "identity management" is a specific phase in the discovery pipeline:
a device is first discovered (published into the manager), then
providers are offered identity control, then the manager keeps a
map of DeviceIdentifier → SDR for the ones that accepted. That map
is what updateIdentifier mutates.
Conclusion: updateIdentifier(_:forIdentityManagedDevice:) is a
maintenance op on an internal identityManagedDevices (or similarly
named) mapping. It does NOT put a new SDR into
managedServiceDevices. If the SDR is not already in the managed set
— and specifically not already identity-managed — the call is either a
no-op, a no-op that logs a warning, or (worst case) an assertion /
trap.
Confidence: High, based on the method name. I have not
disassembled the function. If implementation diverges from what the
name implies (e.g. it lazily inserts on miss), that would be a
surprise worth verifying later. But we should NOT treat
updateIdentifier as a registration path — the name is too specific
and Swift conventions around "for
Q2. Canonical CDS-side "add discovered SDR to managedServiceDevices"¶
Evidence from the browser protocol¶
coredevice-representation.md:71-80:
protocol DeviceRepresentationBrowser {
func start(
on: DispatchQueue?,
invokingWithDiscoveredDeviceRepresentation: (ServiceDeviceRepresentation) -> Void,
invokingIfCancelled: () -> Void
) throws
}
coredevice-representation.md:556-569:
class ServiceDeviceManager {
func install(browser: DeviceRepresentationBrowser)
func deviceStateSnapshot(forDeviceIdentifiedBy: UUID) -> DeviceStateSnapshot?
func deviceStateSnapshot(for: DeviceIdentifier) -> DeviceStateSnapshot?
}
coredevice-xpc-protocol.md:241-258 (the narrative pipeline):
ServiceDeviceManager.install(browser:)
│ Browser.start() → callback with ServiceDeviceRepresentation
│ Identity management: offers to DeviceRepresentationProviders
│ Creates/updates RemoteDevice(snapshot:)
│
├── On device added:
│ ServiceDeviceManager publishes deviceManagerDevicesUpdate(.added)
│ with updated DeviceStateSnapshot
So there is a closure, passed by install(browser:) into each
browser's start(...), that receives a discovered SDR and drives the
full registration sequence. That closure is the canonical "add
discovered SDR" entry point. It:
- Inserts the SDR into
managedServiceDevices. - Offers identity management to providers.
- Publishes
deviceManagerDevicesUpdate(.added). - Later publishes state updates when
mutateState(using:)is called.
What is handleDiscoveredSDR (CoreDevice +0x27E850) then?¶
install(browser:) is at CoreDevice +0x27D7D0. Our
handleDiscoveredSDR is exactly +0x1080 from there. That offset places
it inside or just after the body of install(browser:) — it is
almost certainly either:
- (a) the closure passed to
Browser.start(...)'sinvokingWithDiscoveredDeviceRepresentation:parameter, compiled as a sibling function, OR - (b) a thunk/trampoline to that closure.
The historical session-7 observation that calling it made the device
appear (injection.md:189) is consistent with (a): it is the closure
that drives registration. But the observed effect there — "State is
unavailable because DeviceInfo has no properties set yet" — is also
consistent with partial registration: the SDR got as far as being
picked up for DeviceStateSnapshot publication but without a full
identityManagedDevices entry.
The "handleDiscoveredSDR publishes events but does NOT add to managedServiceDevices" comment in our inject is a session-8 observation against the current code, possibly with a subtly different SDM / SDR state than session 7. Two scenarios that would make the observation true today:
- Wrong self/context: session 8 calls it with
R13 = SDMdirectly; but if +0x1080 is actually a Swift closure, its ABI usesself = closure context pointer(a capture struct) in R13, NOT the SDM object. In session 7 we may have called a different address or at a point in execution where R13 was already pointing to a valid closure context. - Browser/start not active: the insertion into
managedServiceDevicesmay live in the closure body but be gated on "browser is currently running", and our synthetic call bypasses the gate because no browser is installed.
Without disassembling +0x27D7D0 and +0x27E850 we cannot distinguish
these. What IS clear is that the function name handleDiscoveredSDR is
our label, not Apple's — it was assigned in session 7 based on
observed behavior, and that behavior (from injection.md:189–199) was
always partial ("State is unavailable because DeviceInfo has no
properties set yet").
Canonical add function: the closure inside install(browser:),
invoked by a real DeviceRepresentationBrowser.start(...) callback.
We do not yet have a direct Swift symbol for it (closures are emitted
as $s...closure...-style names and are not usually exported).
Other candidate symbols (none found in existing research)¶
Searches in this repo's research docs for addDevice, addService,
registerDevice, processNew, handleNew, serviceFound,
addManaged turn up nothing in CoreDevice. The canonical path goes
through the browser callback, not a standalone "add" method. This is
consistent with Apple's Swift style: mutation happens inside closures
passed through structured protocols, not via imperative addX APIs.
Q3. How does CDS populate managedServiceDevices normally?¶
Flow¶
ServiceDeviceManager.init(...)runs at CDS startup.- Plugin manager loads
.coredevicepluginbundles, collectsDeviceRepresentationProviderinstances. - Each provider's
deviceRepresentationBrowseris passed toServiceDeviceManager.install(browser:). install(browser:)callsbrowser.start(on:, invokingWith...:, invokingIfCancelled:), passing a closure as theinvokingWith...argument. The closure capturesself(SDM) plus any local state needed.- The browser (in its own implementation) runs some discovery
mechanism (Bonjour, remotepairingd IPC, AMRestorable notifications,
static config) and calls the closure with a fresh
ServiceDeviceRepresentationfor each discovered device. - The closure:
a. Inserts SDR into
managedServiceDevices([DeviceIdentifier: SDR]or similar). b. Offers identity management to eachDeviceRepresentationProvider.consider(offer:)— the first toaccept()gets the identity control. c. UpdatesidentityManagedDevicesmapping viaupdateIdentifier(_:forIdentityManagedDevice:). d. Hooks up state change publishing viaaddDeviceRepresentationStateChangedHandler(...). e. PublishesdeviceManagerDevicesUpdate(.added)to all connected clients viaClientManager.publish(event:).
What is "browser" concretely?¶
Per coredevice-xpc-protocol.md:318-340, CDS installs three
browsers at startup:
RemotePairingDeviceRepresentationBrowser— drives remotepairingd viaRemotePairing.ConnectableDeviceBrowser(private framework API).RestorableDeviceRefDeviceRepresentationBrowser— usesAMRestorableDeviceRegisterForNotificationsForDevices(MobileDevice framework) for DFU/recovery devices.StaticDeviceRepresentationBrowser— for statically configured devices; has a.deviceRepresentationsarray field.
All three conform to DeviceRepresentationBrowser, whose only method
is start(on:, invokingWith...:, invokingIfCancelled:) throws.
Can we drive this path synthetically?¶
Three sub-options, in order of "groundedness":
Option A: Custom browser, real install. Implement a Swift (or
Objective-C with manually constructed Swift class metadata) class that
conforms to DeviceRepresentationBrowser, call
ServiceDeviceManager.install(browser:) on the real SDM instance.
install will call our start(...); our start synchronously (or
async) calls the provided closure with our pre-built SDR. The closure
— running in the real manager with real SDM self — does everything.
- Requires: Swift protocol conformance witness table for a custom
class. This is the thing that killed the "custom plugin" approach
(
coredevice-xpc-protocol.md:386-390— "witness table mismatch with the real CoreDevice"). Possible with@objc+ runtime shenanigans only if we can avoid the protocol witness table path. - Viability: hard but correct if solvable.
Option B: Call the install(browser:) closure directly with the
correct ABI. Disassemble +0x27D7D0 to locate the closure, understand
its capture format, synthesize a capture struct containing the SDM
reference, and call the closure with RDI = SDR, R13 = capture.
Semantically equivalent to Option A but skips protocol conformance.
- Requires: disassembling CoreDevice binary (not available on the Linux host — needs VM ssh access to pull a copy) and reverse engineering the closure capture layout.
- Viability: achievable but fragile across CoreDevice versions.
Option C: Find an existing installed browser instance and inject a
synthetic discovery event into it. Specifically,
StaticDeviceRepresentationBrowser has a .deviceRepresentations
array field per coredevice-xpc-protocol.md:337-340. If we can find
the installed instance via heap scan (same technique we use for the
SDM) and either:
- append our SDR to its array then trigger a re-scan, OR
- directly invoke its stored invokingWith... callback (the closure
it was given at start time — presumably retained as a field),
...the manager-side closure runs exactly once with the natural context.
- Requires: Static browser class exists in memory, heap scan finds it, its ivar layout for the discovery callback is reverse-engineerable (or nominal Swift metadata exposes it).
- Viability: LIKELY MOST TRACTABLE. No protocol conformance, no closure capture synthesis, real SDM context.
Last resort (explicitly rejected by stage plan): hook
ClientManager.publish(event:) and rewrite the
DeviceManagerCheckInCompleteEvent payload to inject a
DeviceStateSnapshot. Avoid.
Correct / grounded fix recommendation¶
Primary recommendation: Option C — StaticDeviceRepresentationBrowser.
Rationale:
- Uses the canonical registration path (the closure passed by
install(browser:) to start(...)). No bypass, no synthetic
ABI.
- No Swift protocol witness table construction (the rock Option A
broke on in session 7).
- Static browser exists specifically to support manually-added
devices. We are literally manually adding a device. The
architectural intent matches.
- Heap-scan + ivar pokes is a technique we already use successfully
(SDM discovery, RPDRB discovery).
Secondary recommendation: Option B, only if Option C turns out to be blocked by the static browser not being reachable / having unknown ivar layout.
Drop: current updateIdentifier call. Even if we fix the add
path, the correct call site for updateIdentifier is after the SDR
is already identity-managed, which happens inside the closure we are
trying to invoke, not from us. Calling it ourselves is at best
redundant and at worst corrupts the identity map. Remove it as part of
S1.C.2.
Open questions blocking S1.C.2¶
-
Does
StaticDeviceRepresentationBrowserget instantiated at CDS startup? Injection.md says "installed at startup" in the DeviceRepresentationBrowsers list, but we have not heap-scanned for its instance. S1.C.2 step 1 is: heap scan for the Static browser class, log whether it's present. -
What is the ivar layout of
StaticDeviceRepresentationBrowser? We need: - The
deviceRepresentationsarray field offset (for the append approach), OR -
The callback closure field offset (for the direct-invoke approach — probably stored at
start(...)time as a boxed closure). Dump ivars via ObjC runtime (works on Swift @objc classes), or walk Swift nominal metadata. If both fail, fall back to Option B. -
Is the Static browser's
start(...)callback retained as an ivar, or stashed in a dispatch source? If dispatched, we cannot reach it; we must append to the array and call a re-scan method. Need to check via disassembly whether Static browser has a "rescan" or similar method. -
Confirmation that the natural registration closure sets state to
.connected, not.unavailable. Session 7 reported "unavailable" even whenhandleDiscoveredSDRwas invoked directly — we need to know if that was becauseDeviceInfolacked fields at call time (most likely) or because that particular path hardcodes state. S1.A should have solved the fields part by now; re-run the session-7 scenario against the current inject to test in isolation. -
Does
install(browser:)pass per-call metadata tostart(...)? If yes, driving the callback synthetically from outsideinstallmight miss setup state. If we take Option C, we rely on the fact thatinstall(browser:)already ran at CDS startup for the Static browser, so all setup is complete.
Concrete next steps (for S1.C.2 planning — not this session)¶
- Pull a copy of
/Library/Developer/PrivateFrameworks/CoreDevice.framework/.../CoreDeviceServiceandCoreDevice.framework/Versions/A/CoreDevicefrom the macOS VM to the Linux host (read-only). - Disassemble
install(browser:)(+0x27D7D0) start-to-end. Identify: - The closure at +0x1080 offset.
- Any "add to managedServiceDevices" store instruction.
- Whether the closure stores itself into a field on the browser.
- Disassemble
StaticDeviceRepresentationBrowser.start(...). Identify where it stashes the callback and where it readsdeviceRepresentations. - Based on (2) and (3), decide Option C (preferred) vs Option B.
- Write S1.C.2 implementation plan.
File / line references¶
- Inject call sites:
/home/op/dev/myrepos/iosmux/inject/iosmux_inject.m:974-1054 - Browser protocol definition:
/home/op/dev/myrepos/iosmux/docs/research/coredevice-representation.md:71-90 - SDM API:
/home/op/dev/myrepos/iosmux/docs/research/coredevice-representation.md:556-569 - Discovery pipeline narrative:
/home/op/dev/myrepos/iosmux/docs/research/coredevice-xpc-protocol.md:215-262 - Browser list:
/home/op/dev/myrepos/iosmux/docs/research/coredevice-xpc-protocol.md:318-340 - Historical "it worked" reference:
/home/op/dev/myrepos/iosmux/docs/research/coredevice-injection.md:181-213 - Q5 (session 8):
/home/op/dev/myrepos/iosmux/docs/research/session8-five-questions.md:133-175