Skip to content

Inject serve-sim camera dylib from an unquarantined runtime copy#7174

Merged
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-6877-runtime-materialize
Jul 4, 2026
Merged

Inject serve-sim camera dylib from an unquarantined runtime copy#7174
brennanb2025 merged 2 commits into
mainfrom
brennanb2025/fix-6877-runtime-materialize

Conversation

@brennanb2025

Copy link
Copy Markdown
Contributor

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:

"message": "The binary is not signed."
"message": "The signature does not include a secure timestamp."
path: .../libSimCameraInjector.dylib.gz/libSimCameraInjector.dylib

Gzipping the dylib in afterPack hid it from the code-signing pass (a .gz is 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:

Authority=Developer ID Application: Lovecast LLC (6CX3WHS9HZ)
Timestamp=Jul 2 2026 ...
flags=0x10000(runtime)

So the actual issue in #6877 was never the build — it's runtime: the signed-but-unticketed dylib, injected via DYLD_INSERT_LIBRARIES straight 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, resolveServeSimExecutable materializes the serve-sim package into userData/serve-sim-runtime/<version>/ once per version — copy, chmod the helpers, strip com.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:

  • Bundle layout identical to rc.2: signed dylib present in both serve-sim copies, no archive payloads (nothing that can hide unsigned code from the signer).
  • Stamped com.apple.quarantine on 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.
  • Live camera injection on a booted iPhone 17 Pro simulator (iOS 26.5) driven from the materialized runtime via the packed app's own Electron binary: Injected camera into com.apple.Preferences, lsof confirmed the dylib mapped into the simulator process, and log show showed zero syspolicyd rejections.

What I can and cannot claim this time

  • Notarization safety: high confidence — packaging is unchanged from rc.2, which notarized. Still, the true confirmation is the RC mac build; I'll verify it before calling this done, rather than declaring victory pre-release.
  • The runtime injection path works from an unquarantined copy: verified above.
  • The reporter's exact 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.
  • Cosmetic residual: the bundled dylib copy still fails per-file 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-runs spctl on 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 🐋

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>
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c11fc3e1-f198-4980-b7d8-51bc79ebde48

📥 Commits

Reviewing files that changed from the base of the PR and between 6885471 and 7207e0b.

📒 Files selected for processing (2)
  • src/main/emulator/serve-sim-runtime-materializer.test.ts
  • src/main/emulator/serve-sim-runtime-materializer.ts

📝 Walkthrough

Walkthrough

This 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

Area Change
serve-sim-runtime-materializer.ts New materializer and options type
serve-sim-execution.ts macOS runtime resolution now uses cached materialized runtime
serve-sim-runtime-materializer.test.ts Tests for copy, quarantine, pruning, concurrency, and errors

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)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the macOS runtime-copy fix for serve-sim camera injection.
Description check ✅ Passed The description is detailed and covers summary, testing, security, and notes, but it lacks explicit Screenshots and checklist formatting.
Linked Issues check ✅ Passed The PR addresses #6877 by launching serve-sim from an unquarantined per-version runtime copy, which matches the Gatekeeper rejection fix.
Out of Scope Changes check ✅ Passed The new materializer and tests are directly related to the macOS quarantine/runtime-copy fix, with no obvious unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/emulator/serve-sim-runtime-materializer.test.ts (2)

1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move readdir into the top-level import.

readdir is dynamically imported inline while sibling node:fs/promises utilities 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 win

Pruning 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 renameSync fails 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.js and makes renameSync fail (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 win

Negative results are cached permanently for the process.

When materializeServeSimRuntime returns null (e.g., transient disk-full or a lost prune/rename race with a concurrent instance), materializedServeSimPackageDir is cached as null and every later resolveServeSimExecutable call 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

📥 Commits

Reviewing files that changed from the base of the PR and between 087ad66 and 6885471.

📒 Files selected for processing (3)
  • src/main/emulator/serve-sim-execution.ts
  • src/main/emulator/serve-sim-runtime-materializer.test.ts
  • src/main/emulator/serve-sim-runtime-materializer.ts

Comment thread src/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>
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in 7207e0b:

  • xattr -crxattr -rd com.apple.quarantine (the actionable one). Agreed — -cr was broader than intent. I verified the recursive -d form exits 0 even on trees where some or all files lack the attribute (only the non-recursive single-file form errors on a missing attr), so it's safe with execFileSync. Re-ran the live end-to-end with the new command: quarantine-free materialized tree, camera injection into the simulator process, dylib mapped — still good.
  • Nitpicks: moved readdir to the top-level import, and asserted the materialized return value in the prune test.
  • Untested rename-tolerance branch: added a test that simulates a concurrent instance winning the rename (relevant here since Orca supports many concurrent instances sharing userData).

@brennanb2025 brennanb2025 merged commit a4661f1 into main Jul 4, 2026
1 check passed
@brennanb2025 brennanb2025 deleted the brennanb2025/fix-6877-runtime-materialize branch July 4, 2026 00:12
nwparker added a commit that referenced this pull request Jul 4, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: macOS 1.4.106: nested simcam dylib is unnotarized and syspolicyd reports Malware rejection

1 participant