Skip to content

feat(runtime)!: confine every desktop-app launch under seatbelt - #58

Merged
winlp4ever merged 3 commits into
epic/electron-targetsfrom
feat/electron-confine
Oct 9, 2026
Merged

winlp4ever merged 3 commits into
epic/electron-targetsfrom
feat/electron-confine

Conversation

@winlp4ever

Copy link
Copy Markdown
Contributor

PR 3a of the Electron targets epic (#55). Every desktop-app launch runs confined; the project's files/ is what an app opens; ~/.kiframe/ is Kiframe's deletable work area (decisions of 2026-10-09).

Confinement (electron-confine.ts)

macOS Seatbelt (sandbox-exec) around every launch, measured with a probe app (each escape EPERM, the app driven as usual):

  • writes only into the launch's sandbox; no reads of /Users, /Volumes, the temp folders, the user's home and the work area wherever they are;
  • nothing run from the sandbox (a program it drops there, a copied launcher), no open/osascript/launchctl, no Apple Events;
  • no unix sockets but mDNSResponder; no preferences (cfprefsd), keychain (mock keychain) nor clipboard;
  • network for runs, loopback only for a trial;
  • a canary before each launch: a denied read must fail and an allowed one succeed, else no launch.

Stated limits (OBJECT-MODEL §0.9, BACKLOG): an app can still ask LaunchServices to open a file or URL (no app starts without it, measured), and it reaches localhost. The confinement guards against an app reaching the user's data by accident, not one built to escape. An app's own copy and paste does nothing in a run.

files/

Copied into each launch's sandbox (own walker: links, FIFOs, devices refused, the root too; 500 MB / 20,000 entries / depth 32; APFS clones). The app's args name files/<path>, and its edits are thrown away.

Work area ~/.kiframe/

sandboxes/ (0700, never a link, out of Time Machine and Spotlight). Each sandbox is named after its owner (kiframe-app-<pid>-<start>-…), so it's owned from the moment it exists: no shared state file to race on. The crash sweep at start:

  • removes only sandboxes no running Kiframe owns, and kills what points at them plus the app's process group;
  • reads ps in the C locale, sweeps nothing when ps can't be read, and never trusts a reused pid.

Tests

  • probes for every escape (exact DENIED EPERM), the clipboard, the canary's two sides, a home outside the denied roots;
  • files/ copy, refusals and limits;
  • a real crash (a child Kiframe SIGKILLed mid-run) swept, including a group member that points nowhere;
  • sweeps racing a starting launch;
  • another Kiframe's launch kept in a Korean locale; nothing swept without ps.

Each test fails with its fix put back.

BREAKING CHANGE: a desktop app's args must be paths in the project's files/.

Each launch runs under macOS Seatbelt: it writes only into its own sandbox,
never reads the user's folders, runs nothing it drops there, and reaches
neither preferences, the keychain, the clipboard nor other agents' sockets.
A canary checks the confinement before every launch (one read refused, one
allowed); without it, no launch.

A project's files/ is copied into each launch's sandbox (links, devices and
oversized trees refused); the app's arguments name paths in it. Sandboxes
live in ~/.kiframe/sandboxes, each named after the Kiframe that made it, so
a crash sweep removes only what no running Kiframe owns.

BREAKING CHANGE: a desktop app's args must be paths in the project's files/
(files/<name>); any other argument no longer parses.
@codecov

codecov Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.39130% with 75 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/runtime/src/electron-confine.ts 55.84% 28 Missing and 6 partials ⚠️
packages/runtime/src/electron-workarea.ts 75.70% 16 Missing and 10 partials ⚠️
packages/runtime/src/electron.ts 63.41% 10 Missing and 5 partials ⚠️

📢 Thoughts on this report? Let us know!

- each launch writes its own canary, and a canary that went missing
  proves nothing (refused): launches at once never spoil each other's
- the app's time to start begins once its files are copied
- a stop during the copy ends the launch before the app starts
- a folder in files/ swapped for a link while copied is refused
- the app's own temp folder holds its own pages again
- a folder in files/ is checked again once its entries are copied: one
  swapped for a link at any point of the copy is refused
- each canary lies beside its sandbox, named after it: a crash's is swept
- a stop ends the canary check and the copy at once, said as the stop
- a canary that was read reads as a confinement that didn't hold
- the slow-copy tests are sized to the app's measured start, built once
  and removed
@winlp4ever
winlp4ever merged commit ed1c66e into epic/electron-targets Oct 9, 2026
2 checks passed
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.

1 participant