Skip to content

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 - 07ServiceB7ManagerServiceDeviceManager (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() returns DeviceRepresentationIdentityControl (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:" labels are precise.

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:

  1. Inserts the SDR into managedServiceDevices.
  2. Offers identity management to providers.
  3. Publishes deviceManagerDevicesUpdate(.added).
  4. 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(...)'s invokingWithDiscoveredDeviceRepresentation: 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 = SDM directly; but if +0x1080 is actually a Swift closure, its ABI uses self = 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 managedServiceDevices may 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

  1. ServiceDeviceManager.init(...) runs at CDS startup.
  2. Plugin manager loads .coredeviceplugin bundles, collects DeviceRepresentationProvider instances.
  3. Each provider's deviceRepresentationBrowser is passed to ServiceDeviceManager.install(browser:).
  4. install(browser:) calls browser.start(on:, invokingWith...:, invokingIfCancelled:), passing a closure as the invokingWith... argument. The closure captures self (SDM) plus any local state needed.
  5. The browser (in its own implementation) runs some discovery mechanism (Bonjour, remotepairingd IPC, AMRestorable notifications, static config) and calls the closure with a fresh ServiceDeviceRepresentation for each discovered device.
  6. The closure: a. Inserts SDR into managedServiceDevices ([DeviceIdentifier: SDR] or similar). b. Offers identity management to each DeviceRepresentationProvider.consider(offer:) — the first to accept() gets the identity control. c. Updates identityManagedDevices mapping via updateIdentifier(_:forIdentityManagedDevice:). d. Hooks up state change publishing via addDeviceRepresentationStateChangedHandler(...). e. Publishes deviceManagerDevicesUpdate(.added) to all connected clients via ClientManager.publish(event:).

What is "browser" concretely?

Per coredevice-xpc-protocol.md:318-340, CDS installs three browsers at startup:

  1. RemotePairingDeviceRepresentationBrowser — drives remotepairingd via RemotePairing.ConnectableDeviceBrowser (private framework API).
  2. RestorableDeviceRefDeviceRepresentationBrowser — uses AMRestorableDeviceRegisterForNotificationsForDevices (MobileDevice framework) for DFU/recovery devices.
  3. StaticDeviceRepresentationBrowser — for statically configured devices; has a .deviceRepresentations array 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

  1. Does StaticDeviceRepresentationBrowser get 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.

  2. What is the ivar layout of StaticDeviceRepresentationBrowser? We need:

  3. The deviceRepresentations array field offset (for the append approach), OR
  4. 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.

  5. 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.

  6. Confirmation that the natural registration closure sets state to .connected, not .unavailable. Session 7 reported "unavailable" even when handleDiscoveredSDR was invoked directly — we need to know if that was because DeviceInfo lacked 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.

  7. Does install(browser:) pass per-call metadata to start(...)? If yes, driving the callback synthetically from outside install might miss setup state. If we take Option C, we rely on the fact that install(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)

  1. Pull a copy of /Library/Developer/PrivateFrameworks/CoreDevice.framework/.../CoreDeviceService and CoreDevice.framework/Versions/A/CoreDevice from the macOS VM to the Linux host (read-only).
  2. Disassemble install(browser:) (+0x27D7D0) start-to-end. Identify:
  3. The closure at +0x1080 offset.
  4. Any "add to managedServiceDevices" store instruction.
  5. Whether the closure stores itself into a field on the browser.
  6. Disassemble StaticDeviceRepresentationBrowser.start(...). Identify where it stashes the callback and where it reads deviceRepresentations.
  7. Based on (2) and (3), decide Option C (preferred) vs Option B.
  8. 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