Inject serve-sim camera dylib from an unquarantined runtime copy#7174
Conversation
The bundled libSimCameraInjector.dylib is Developer-ID-signed and notarizes fine inside the app (this is unchanged), but Apple issues no Gatekeeper ticket for its iOS-simulator arch. Injected straight from the quarantined app bundle via DYLD_INSERT_LIBRARIES, that can trip syspolicyd on some installs (#6877). On macOS packaged builds, serve-sim now runs from a per-version copy in userData with com.apple.quarantine stripped, so the dylib it injects is not subject to Gatekeeper assessment. Packaging is deliberately untouched — the signed dylib stays in the bundle exactly as it ships today, so notarization is unaffected. Falls back to the bundled entry if materialization fails. Fixes #6877 Co-authored-by: Orca <help@stably.ai>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThis PR adds a new serve-sim runtime materializer that copies the bundled package into a per-version directory, clears quarantine, preserves executable bits, prunes stale versions, and handles concurrent or failed materialization cases. macOS serve-sim execution now resolves through a cached materialized runtime under the app’s user data directory when available, with fallback to the bundled script. A new Vitest suite covers success, idempotency, pruning, race tolerance, and failure paths. Changes
Sequence Diagram(s)See the hidden review stack diagrams. Estimated code review effort: Medium Related issues: None specified Related PRs: None specified Suggested labels: emulator, macOS, testing Suggested reviewers: None specified 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (3)
src/main/emulator/serve-sim-runtime-materializer.test.ts (2)
1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove
readdirinto the top-level import.
readdiris dynamically imported inline while siblingnode:fs/promisesutilities are already statically imported at Line 1.♻️ Proposed fix
-import { mkdtemp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises' +import { mkdtemp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'- const { readdir } = await import('node:fs/promises') - const leftovers = (await readdir(targetRootDir)).filter((name) => name.startsWith('.staging')) + const leftovers = (await readdir(targetRootDir)).filter((name) => name.startsWith('.staging'))Also applies to: 118-120
85-100: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPruning test doesn't assert the new version materialized successfully, and the rename race-tolerance branch is untested.
The upstream implementation has a documented tolerance path where
renameSyncfails but materialization is still considered successful if another process already produced the entry file. Neither this test nor others exercise that branch, and this test doesn't verify the return value/new version directory here.✅ Suggested strengthening
- materializeServeSimRuntime({ + const materialized = materializeServeSimRuntime({ bundledPackageDir, targetRootDir, version: '1.2.3', clearQuarantine: () => {} }) + expect(materialized).toBe(join(targetRootDir, '1.2.3')) await expect(stat(join(targetRootDir, '1.0.0'))).rejects.toThrow()Consider adding a dedicated test that pre-creates the target version's
dist/serve-sim.jsand makesrenameSyncfail (e.g., by pre-populating the staging path collision) to cover the "another instance won the race" tolerance branch.src/main/emulator/serve-sim-execution.ts (1)
74-90: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winNegative results are cached permanently for the process.
When
materializeServeSimRuntimereturnsnull(e.g., transient disk-full or a lost prune/rename race with a concurrent instance),materializedServeSimPackageDiris cached asnulland every laterresolveServeSimExecutablecall in the session keeps falling back to the quarantined bundle without retrying — even after the transient condition clears. Consider caching only successful materializations so failures are retried.♻️ Cache only successful results
- if (materializedServeSimPackageDir !== undefined) { - return materializedServeSimPackageDir - } - materializedServeSimPackageDir = materializeServeSimRuntime({ + if (materializedServeSimPackageDir) { + return materializedServeSimPackageDir + } + const materialized = materializeServeSimRuntime({ bundledPackageDir, targetRootDir: join(app.getPath('userData'), 'serve-sim-runtime'), version: app.getVersion() }) - if (materializedServeSimPackageDir === null) { + materializedServeSimPackageDir = materialized + if (materialized === null) { console.warn( '[serve-sim] runtime materialization failed; running from the app bundle ' + '(camera injection may hit Gatekeeper on quarantined installs)' ) } - return materializedServeSimPackageDir + return materialized
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 3aa75e51-eadd-4b0c-a02d-2f38f570e4f0
📒 Files selected for processing (3)
src/main/emulator/serve-sim-execution.tssrc/main/emulator/serve-sim-runtime-materializer.test.tssrc/main/emulator/serve-sim-runtime-materializer.ts
Address CodeRabbit review: use `xattr -rd com.apple.quarantine` instead of `-cr` so only the quarantine flag is removed rather than every extended attribute (recursive `-d` exits 0 even for files that lack it — verified). Add a test for the concurrent-instance rename-tolerance path and tidy the test's imports. Co-authored-by: Orca <help@stably.ai>
|
Addressed the CodeRabbit review in 7207e0b:
|
…guard gate validators (#7321) Quality pass on main-process/build PRs merged 2026-07-03: - ios-emulator-backend: resolve the serve-sim executable via a lazily-cached getter instead of eagerly in the constructor. The bridge is built before the main window is shown, so the one-time recursive copy + xattr subprocess (first launch after each version bump) no longer blocks macOS startup for a feature that may go unused (#7174). - index: collapse the two near-identical `{webContentsId, until}` reload flags (expectedRendererReload / recoveryReloadInFlight) into one `createWebContentsTimedFlag` primitive; behavior preserved, including consume-on-read for the recovery reload (#7290). - check-reliability-gates: coerce gate.commands/testFiles/platforms/providers with an `asArray` helper before `.includes`, so a hand-edited manifest with a missing/mistyped field reports a validation failure instead of throwing an uncaught TypeError; extract `hasCompleteRedGreenEvidence` for the duplicated status check (#7295). - claude-pty: derive FABLE_WEEKLY_LABEL_RE from WEEKLY_RE.source so a future weekly- wording change stays in one place and can't reopen the parsing gap it just closed. - macos-tcc-login-shell: trim the 30-line flag-by-flag JSDoc to the two non-obvious whys (TCC identity, env(1) SHELL re-assertion) per the repo comment guidance (#7003). Typecheck, oxlint, oxfmt, `check:reliability-gates`, and touched unit suites all pass.
Summary
Fixes #6877. Third and — I believe — correct attempt, after diagnosing why the first two failed.
What the first two attempts got wrong
Both #7077 (notarize the dylib separately) and #7168 (gzip it out of the signer's view) were built on a false premise: that a simulator-platform dylib cannot be in a notarized bundle. The rc.3 notary failure disproved it — the errors were not "simulator arch rejected", they were:
Gzipping the dylib in
afterPackhid it from the code-signing pass (a.gzis not a Mach-O), so notary decompressed the archive and found unsigned code. The proof it was never a "sim binaries can't notarize" problem: rc.2 notarized successfully with the raw dylib present, because there electron-builder deep-signed it (Developer ID + hardened runtime + secure timestamp). Verified against the shipped rc.2 artifact:So the actual issue in #6877 was never the build — it's runtime: the signed-but-unticketed dylib, injected via
DYLD_INSERT_LIBRARIESstraight from the quarantined app bundle, can trip syspolicyd's Gatekeeper assessment on some installs.The fix
Packaging is deliberately untouched. The signed dylib stays in the bundle exactly as it ships today, so notarization is byte-for-byte the proven rc.2 path — this removes the entire class of failure that sank the last two PRs (they can't recur because Apple's notary never sees anything new).
The only change is runtime: on macOS packaged builds,
resolveServeSimExecutablematerializes the serve-sim package intouserData/serve-sim-runtime/<version>/once per version — copy, chmod the helpers, stripcom.apple.quarantine(xattr -cr), atomic-rename, prune old versions — and runs serve-sim from there. serve-sim resolves the dylib and camera helper relative to its own entry, so the whole package moves together. The injected dylib is then an unquarantined copy, not subject to Gatekeeper assessment. On any failure it falls back to the bundled entry, so emulator streaming still works.Validation
Unit (206 emulator tests incl. 5 new materializer tests), typecheck, oxlint, oxfmt — all pass.
End-to-end against a real local
pnpm build:unpack:com.apple.quarantineon every packed serve-sim file (simulating a downloaded install), ran the real materializer: result had zero quarantine xattrs, dylib byte-identical to the bundled copy.Injected camera into com.apple.Preferences,lsofconfirmed the dylib mapped into the simulator process, andlog showshowed zero syspolicyd rejections.What I can and cannot claim this time
syspolicyd"Malware rejection": not reproduced. I could not reproduce it on my dev machine even with the old artifact (quarantine enforcement varies by machine/OS state), so I can't show a literal before/after of that log line. This fix is the correct defensive measure for that class of problem (inject from unquarantined code), but I'm flagging the residual uncertainty rather than overstating.spctl -a(no ticket for sim arch). That is expected and harmless for a simulator binary that's never executed by Gatekeeper directly — only the unquarantined materialized copy is ever injected. If the reporter re-runsspctlon the bundled file they'll still see "rejected"; that specific line is not fixable (Apple won't ticket sim arch) and does not indicate a functional problem.Made with Orca 🐋