Repository navigation
Conversation
parawanderer
left a comment
There was a problem hiding this comment.
Requesting changes. Item 1 blocks the merge; 2–4 are required alongside it.
-
Existing sessions are moved onto a new machine on upgrade.
AppDependencies.java:63makesEmulatedMacAnisettethe source for every caller, so on restore_preferLocalAnisette(main.py:896) calls itsensureReady(), which downloads CoreADI and provisions a new Mac at app start, then swaps it into the restored account. That session's token was issued to the Android machine, and per #248 a token works only with Anisette from the machine that obtained it, so it should start answering 401.identityForRestore(main.py:863) also carries the old Android device id across, so Apple receives the new Mac'sX-Apple-I-MD-Malongside the AndroidX-Mme-Device-Id. Sessions established before 2026-10-12 still fetch reports today; @parawanderer's does. Fix: keepLocalAnisettefor any session it established and give the emulated Mac only to fresh sign-ins. A restored session must not provision anything. -
libmacadi.soloads in the app process with no crash guard.MacCoreAdi.java:67callsSystem.loadLibrarydirectly, and thecatch (Exception | LinkageError)atEmulatedMacAnisette.java:114cannot catch a SIGSEGV from Unicorn or from Apple's code. That is the #232 failure mode (AGENTS.md rule 4): a native crash on the sign-in screen makes the app unopenable. Run it behind the:adiprobeprocess /TryItElsewhereFirstandNativeLoadGuard, or host the emulator in its own process and call it over binder. The second option also keeps the GPL code in a separate program from the Apache-2.0 libraries and the proprietary map SDKs (item 4). -
Session provenance is lost on upgrade.
EmulatedMacAnisettestoressessionEstablishedLocallyin a new preferences file,anisette-emulated-mac(line 41), so for every existing sessionwasSessionEstablishedLocally()reads false and the machine-change warning in_preferLocalAnisettenever fires. Read the flagLocalAnisettealready wrote. This mostly goes away with item 1. -
Unicorn is accepted as GPL-2.0. Add a licence notice for it to the app or the release, and see item 2 for keeping it out of the app process. Decision and reasoning:
docs/emulated-mac-emulator-choice.md(#275). -
Tests. The pure logic here is untested:
EmulatedMacAnisette's state machine (first provisioning, the lost-store branch that registers a new machine, failure falling back to remote,adi.pbnever saved),MacIdentitygeneration and save/load round trip, and the request shapesMacProvisioningsends. Put them on the JVM with the native layer behind an interface and aFakeApkServer-style local socket for provisioning (rule 13). Add one for item 1 as well: a restored session established withLocalAnisettekeepsLocalAnisetteand never provisions. -
The C++ port has no byte-for-byte comparison against os-findmy-e's Rust
adi-core, as the PR says.scripts/adi-emulator-diff/(#275) drivesadi-corewith a frozen clock and a fixed host UUID; run the same sequence throughlibmacadi.soand diff. A mismatch on these offline calls is a port bug that a single successful sign-in will not show. -
Move the
hsc409 handling into parawanderer/FindMy.py's_gsa_authenticateinstead of wrapping_gsa_requestat runtime (main.py:343,main.py:898). malmeloo/FindMy.py#279 makes the same change upstream.
Non-blocking: MacBookPro13,2 never shipped macOS 13.1, which is what rule 11 forbids. Keeping the values os-findmy-e was observed to get searchPartyToken with is defensible; note it in MacIdentity alongside the Darwin mismatch already noted there.
Interactively co-authored by Claude Code and @parawanderer
…icle diff parawanderer#274 runs Apple's macOS CoreADI under Unicorn, which is GPL-2.0. The decision is to ship it, with a licence notice and the library in its own process, and to swap it only if somebody has a reason to. A swap is invisible to users only if the replacement produces the same bytes, so scripts/adi-emulator-diff/ puts icicle (MIT OR Apache-2.0) under os-findmy-e's adi-core and diffs it against Unicorn. Every offline call matched byte for byte; the OTP path, which needs a provisioned machine, has not been compared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A C++ port of sacca97/os-findmy-e's adi-core (MIT, see macadi/NOTICE.md): a Mach-O loader, a Unicorn x86_64 VM with the few pieces of macOS userland the library calls, and the CoreADI dispatcher framing. Unicorn 2.1.4 (GPL-2.0) is fetched at build time, pinned by SHA-256, and linked only into libmacadi.so. The library itself is never in the repository or the build. In iTunes 12.8.3 for Mac the package holding it is stored uncompressed and contiguous, so it is one HTTP range request of 3.5 MB; scripts/locate_mac_coreadi.py re-derives the offsets and checks both pinned hashes, and macadi-host.yml builds the core for the host and loads the library into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JDrrt9ff7WwPepHs6B2Gx
Since 2026-10-12 Apple issues searchPartyToken only to Mac- or iPhone-minted Anisette, and the edge now refuses any client claiming Android, so the Android ADI path cannot reach Find My. EmulatedMacAnisette fetches the macOS library from Apple, provisions one emulated Mac with akd's headers, keeps its identity and store for the life of the install, and is what AppDependencies hands out. Also: read Grand Slam's hsc 409 without au as a second factor; send the routing number Apple assigned at provisioning; give CloudKit a UUID, since a Mac's device id is not one; drop the legacy adi.pb before every session, which otherwise made a freshly provisioned machine report unprovisioned (ec -80009). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JDrrt9ff7WwPepHs6B2Gx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012JDrrt9ff7WwPepHs6B2Gx
…its own process A session LocalAnisette signed in keeps LocalAnisette, and nothing is provisioned to restore it. AppDependencies made EmulatedMacAnisette the source for every caller, so a restore provisioned a new Mac at app start and paired its X-Apple-I-MD-M with the account's Android device id. Per parawanderer#248 that session's token only works with Anisette from the Android machine, so a working session would start answering 401. AnisetteForSession now chooses: the emulated Mac for a fresh sign-in, and for a restore whichever machine signed the session in. SessionProvenance records that in LocalAnisette's own preferences file, reading the sessionEstablishedLocally flag every older install already has and adding sessionMachine=emulatedMac beside it for the Mac's sessions. The separate anisette-emulated-mac file is gone, so the machine-change warning works again for upgraders. A Mac session is restored as Purpose.RESTORE, which never creates or provisions a machine: a lost store falls back to remote with the warning. libmacadi.so now loads only in a :macadi process. MacAdiService hosts the emulator and RemoteMacAdi calls it over binder, so a SIGSEGV from Unicorn or Apple's code kills that process rather than the sign-in screen (rule 4, parawanderer#232), and the GPL-2.0 emulator is a separate program from the rest of the APK. A death during a call is recorded per app version and library build, and the Mac is not tried again until one of them changes. OpenAirTagApplication skips its start-up in that process. The native layer is behind MacAdi, so the logic is tested on the JVM: EmulatedMacAnisette's state machine, MacIdentity's shapes and round trip, MacProvisioning's requests against a local FakeGrandSlam socket, and the restore choice for an install with only LocalAnisette's flag. TheEmulatedMacInItsOwnProcessTest checks the process boundary on a device, including that the app process never maps libmacadi.so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
…ease libmacadi.so links Unicorn 2.1.4, which is GPL-2.0. The APK now carries Unicorn's own licence files (COPYING, its QEMU licence, LGPL2, GLIB) under assets/licences/unicorn/, with a NOTICE.txt naming the exact source tarball and hash and saying the code runs in its own process. UnicornNoticeTest fails if that notice stops matching MacAdi.cmake. The source is fetched by FetchContent rather than being in the tag, so build-release.yml now downloads the same tarball, checks it against the pinned hash, and attaches it to the release beside the APK. The root NOTICE gains Unicorn and os-findmy-e entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
compare-libmacadi.sh builds adi-core's adi-diff (the driver run.sh uses for Unicorn against icicle) and the port's new macadi_diff, runs both over the same offline CoreADI calls with a frozen clock and a fixed host UUID, and diffs the output. macadi-host.yml runs it on every change to the port. To make that possible CoreAdi exposes the raw calls adi-core has (begin_session, IsMachineProvisioned, RequestOTP, GetIDMSRouting, ProvisioningStart) and the VM's files. headers() and provisioningBegin() are now built from those same calls, so the comparison covers the code the app runs. Result on x86_64 Linux: all 20 lines identical, with the port on Unicorn 2.1.4 and adi-core on unicorn-engine 2.1.5. Checked for teeth by breaking vm.cpp on purpose: a stray byte in created files turns six lines red, while changing arc4random_buf or truncating write changes nothing, because the offline calls never reach them. Both drivers take an optional provisioned store for the OTP path, which has not been run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
The fix belongs in FindMy.py's _gsa_authenticate, where malmeloo/FindMy.py#279 adds _normalize_2fa_status; the same change is prepared for parawanderer/FindMy.py. Until the pin moves, _treatHsc409AsSecondFactor keeps wrapping _gsa_request, but returns without touching the account when the pinned FindMy.py has the function. test_the_pinned_findmy_does_not_yet fails on the pin move, which is the cue to delete the wrapper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
The 2016 13-inch MacBook Pro stops at macOS 12, so the pair is not a real release, which rule 11 asks for. These are the values os-findmy-e was observed to get searchPartyToken with, and changing either is a new machine to Apple, so they stay until a consistent pair is shown to work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
Records what the review taught: never move an existing session onto the emulated Mac, where provenance lives, why libmacadi.so loads only in :macadi, the GPL obligations and where each is met, the byte-for-byte check, and that the hsc 409 wrapper goes when the FindMy.py pin moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
d9e695c to
6100a00
Compare
|
Thanks for the thorough review. I've rebased onto main (after #275 and #278) and pushed the fixes as separate commits. Item by item: 1. Restored sessions keep 2. Native-crash guard. I took the separate-process option. 3. Provenance. The 4. GPL-2.0 notice. The APK now carries Unicorn's own licence files from the 2.1.4 tag under 5. JVM tests. The native layer is behind a
I made sure they can fail. Reverting the restore choice, the restore-never-provisions guard, the 6. Byte-for-byte comparison. 7. hsc 409 in FindMy.py. I've prepared the change for parawanderer/FindMy.py, and will open it there as a PR if you're happy with this shape: Non-blocking. I've added a note to AGENTS.md rule 21 is updated with the above. Upstream's numbering only goes to 20, so it stays 21. On my fork's CI, the JVM suite, the bridge tests, the debug APK build (including the NDK build of What I haven't verified: I haven't done a sign-in on a phone since these changes. The emulator suite swaps Anisette for a fake, so a real sign-in through |
|
I've opened the FindMy.py change for item 7 as parawanderer/FindMy.py#9. Once that's merged I'll move the pin in this PR and delete |
Fixes #248.
Since 2026-10-12 Apple only issues the Find My token (
searchPartyToken) to a sign-in whose Anisette machine was minted by a Mac or an iPhone. An Android-minted machine getsmmeAuthTokenalone and report fetches return 401 (malmeloo/FindMy.py#278). Since about 2026-10-20 the GSA edge also returns 503 for anyX-MMe-Client-Infowhose OS token isAndroid(probe results on #261). So the Android ADI libraries can't reach Find My any more, whatever headers are sent.This runs Apple's macOS CoreADI on the phone instead, under an x86_64 emulator - the "derived library that can be run on android directly" from #248, built off sacca97/os-findmy-e.
How it works
libmacadi.so(app/src/main/cpp/macadi/): a C++ port of os-findmy-e's Rustadi-core- Mach-O loader, Unicorn x86_64 VM with the handful of macOS calls the library makes, and the CoreADI dispatcher framing. Unicorn 2.1.4 is fetched by CMake at build time, pinned by SHA-256, and only linked into this library.CoreADI.pkgpayload is stored uncompressed and contiguous, so a single HTTP range request gets it. The payload and the extracted library are both pinned by SHA-256.scripts/locate_mac_coreadi.pyre-derives the offsets (from Apple, or from a local copy of the DMG) and reads its constants from the Java sources. Nothing of Apple's is in the repo or the APK.AnisetteForSessionpicks the machine inAppDependencies. A fresh sign-in getsEmulatedMacAnisette. A restored session keeps the machine that signed it in:LocalAnisettefor every session it established (and for installs that recorded nothing), and the emulated Mac only for sessions it signed in, restored without ever provisioning. Provenance lives inLocalAnisette's preferences (SessionProvenance).EmulatedMacAnisetteprovisions one emulated Mac withakd's headers (from os-findmy-e'sgsa.py), keeps its identity and ADI store underfiles/anisette/mac/for the life of the install, and serves the OTP, machine id and Apple-assigned routing number to the existing Python sign-in.libmacadi.soloads only in a non-exported:macadiprocess (MacAdiService), and the app calls it over binder (RemoteMacAdi). A native crash there reaches the app asMacAdi.EmulatorDiedExceptionand is recorded per app version and library build, the same scheme asTryItElsewhereFirst, rather than making the app unopenable. It also keeps the GPL code out of the app process.hsc409 with noauon GSAcompleteis treated as a second factor (the pinned FindMy.py otherwise skips 2FA). The proper fix,_normalize_2fa_statusin_gsa_authenticateas in Fix and integrate with anisette mac backend malmeloo/FindMy.py#279, is prepared for parawanderer/FindMy.py; until the pin moves the runtime wrapper stays, and it steps aside once the pinned FindMy.py has the fix; the routing number comes from Java; CloudKit and the keychain session get a view of the account whosedevice_uuidis a UUID, because a Mac's device id (00006000-…) isn't one and FindMy.py's CloudKit client parses it as one.Things to decide
docs/emulated-mac-emulator-choice.md). The APK carries Unicorn 2.1.4's licence files and aNOTICE.txtwith the exact source tarball and SHA-256 underassets/licences/unicorn/, andbuild-release.ymlattaches that verified tarball to each release. The rootNOTICElists Unicorn and os-findmy-e (MIT).0PENTAGV…, as before). Apple could start fingerprinting this; it has changed the rules four times in five weeks.Testing
adi.pbin the store, and while it exists the library reports "not provisioned", so the first sign-in after provisioning sent an empty machine id (ec -80009);badly formed hexadecimal UUID string).macadi-host.yml, which range-fetches the library from Apple exactly as the app does, checks both hashes, builds the native core for the host and loads the library ("not provisioned").MacCoreAdiFetcherTest(cpio walk, universal-binary slice, refusing a server that ignores the range),test_emulated_mac.py(409 shim, routing, CloudKit device id view), and on the JVMEmulatedMacAnisetteTest,MacIdentityTest,MacProvisioningTest(against a localFakeGrandSlamsocket),RestoringASessionOnTheMachineThatSignedItInTestandUnicornNoticeTest. Instrumented:TheEmulatedMacInItsOwnProcessTest(real CoreADI loads in:macadi, the app process never mapslibmacadi.so, a SIGSEGV there arrives as a death).adi-core:scripts/adi-emulator-diff/compare-libmacadi.shruns the same offline calls throughlibmacadi.soand os-findmy-e'sadi-diff; all 20 lines match, in CI as a secondmacadi-host.ymljob.Not verified: a device sign-in since the review changes (the Pixel test above predates moving the emulator into
:macadi); the byte-for-byte comparison of the OTP and provisioning paths, which needs a provisioned store; how long a provisioned emulated Mac and its token stay accepted over days; more than one account.Credit to @sacca97 for os-findmy-e, which this is ported from, and to @hoorayman and @poterpan on #248 for establishing that a Mac-minted machine is what gets
searchPartyToken.🤖 Generated with Claude Code