Skip to content

feat(sandboxfuse): macOS FSKit FUSE backend + kext→FSKit auto-fallback#130

Open
raphaelvigee wants to merge 3 commits into
masterfrom
feat/macos-fskit-fuse-backend
Open

feat(sandboxfuse): macOS FSKit FUSE backend + kext→FSKit auto-fallback#130
raphaelvigee wants to merge 3 commits into
masterfrom
feat/macos-fskit-fuse-backend

Conversation

@raphaelvigee

Copy link
Copy Markdown
Member

What

Adds a userspace FUSE backend selector on macOS and an auto-fallback so the sandbox overlay works on machines where the macFUSE kext isn't approved.

fuse: { backend: kext | fskit } pins a backend. Omit it and the engine tries the kernel-extension backend first (fastest), then falls back to Apple FSKit (unprivileged, macOS 15.4+) if the kext mount can't come up. Linux is unchanged — single kernel backend.

Changes

  • configFuseConfig.backend (FuseBackend::{Kext,Fskit}) + on/auto/with_backend constructors; documented in example/.hephconfig2.
  • sandboxfuseMountBackend threaded through Mount::mount; configure_mount branches per platform/backend (kext keeps AutoUnmount + RootAndOwner ACL; FSKit omits them and passes -o backend=fskit).
  • engine — backend candidate ordering, per-backend mountpoint (FSKit under /Volumes, kext at <root>/lower), per-backend actionable failure hints, and a /Volumes sweep that force-umounts stale FSKit volumes whose pid is dead. The supervisor umounts the registered mountpoint directly.

Bug fixes folded in

  • OwnedBlob self-referential move — the pooled sqlite connection backing the seekable FUSE reader was moved out from under the blob borrowing it, aborting reads with "RefCell already mutably borrowed". Box the connection for a stable address. Regression-tested by test_fuse_dep_output_read_through_layer.
  • macOS-only SIGSEGV in testkit::fuse_mount_works — libfuse is weak-linked on macOS (sandboxfuse build.rs), so its symbols bind to null when macFUSE is absent. The probe called Mount::mount without first clearing the support_check gate → null-symbol deref → crash instead of returning false. Now gated on support_check, so forced-on FUSE e2e tests skip cleanly on hosts without macFUSE.

Tests

New crates/e2e/tests/fuse.rs exercises the real mount path: dep read-through-layer, large byte-exact streaming, upper-layer write-back, concurrent shared-dep reads. Each skips (not fails) when no usable FUSE mount exists.

Verification (macOS, this host has no macFUSE)

  • cargo test -p config → 59 passed (incl. 6 new backend-parsing tests)
  • cargo test -p e2e --test fuse → 4 passed (skip cleanly; no SIGSEGV after the probe fix — it crashed before)
  • cargo clippy --all-targets -- -D warnings → clean; cargo fmt clean

⚠️ The FSKit mount path and OwnedBlob fix run for real only on a FUSE-capable host (Linux CI / a mac with macFUSE); this host skips them.

🤖 Generated with Claude Code

raphaelvigee and others added 3 commits June 25, 2026 14:04
Add a userspace FUSE backend selector on macOS. `fuse: { backend: kext | fskit }`
pins a backend; omit it and the engine tries the kernel-extension backend first
(fastest) and falls back to Apple's FSKit (unprivileged, macOS 15.4+) when the
kext mount can't be brought up. Linux is unchanged — one kernel backend.

- config: `FuseConfig.backend` (`FuseBackend::{Kext,Fskit}`) + `on`/`auto`/
  `with_backend` constructors; documented in `.hephconfig2`.
- sandboxfuse: `MountBackend` threaded through `Mount::mount`; `configure_mount`
  branches per platform/backend (kext keeps AutoUnmount + RootAndOwner ACL;
  FSKit omits them and passes `-o backend=fskit`).
- engine: `backend_candidates` ordering, per-backend mountpoint (FSKit mounts
  under `/Volumes`, kext at `<root>/lower`), actionable per-backend failure
  hints, and a `/Volumes` sweep that force-umounts stale FSKit volumes whose
  pid is dead. The supervisor now umounts the registered mountpoint directly.

Fix a self-referential `OwnedBlob` bug surfaced by the FUSE read path: the
pooled sqlite connection backing the seekable reader was moved out from under
the blob that borrowed it, aborting reads with "RefCell already mutably
borrowed". Box the connection so its address is stable across moves.

Fix a macOS-only SIGSEGV in the new `testkit::fuse_mount_works` probe: libfuse
is weak-linked on macOS (sandboxfuse build.rs), so its symbols bind to null when
macFUSE is absent. The probe called `Mount::mount` without first clearing the
`support_check` gate, dereferencing a null symbol and crashing instead of
returning false. Gate the probe on `support_check` so forced-on FUSE e2e tests
skip cleanly on hosts without macFUSE.

Add e2e `fuse.rs` exercising the real mount: dep read-through-layer (the
RefCell regression test), large byte-exact streaming, upper-layer write-back,
and concurrent shared-dep reads. Each skips when no usable FUSE mount exists.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The e2e fuse suite gates on `fuse_mount_works()`, which only attempted the
default (Kernel/kext) backend at a temp dir. On a kext-less macOS host the
engine auto-falls-back to FSKit (`FuseConfig::on()` → kext, else FSKit), so the
mount would succeed for real while the probe reported "no FUSE" and the whole
suite skipped — masking the FSKit path the PR adds.

Mirror `engine::backend_candidates` / `backend_mountpoint`: try the kernel
backend at a temp `lower`, then FSKit under `/Volumes`, and report usable when
either mounts. Cache the result in a `OnceLock` so the probe mounts at most once
per process (parallel tests don't race on the shared FSKit `/Volumes`
mountpoint). The `support_check` null-symbol gate is preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Omitting `enabled` (or the whole `fuse:` block) previously resolved to off
everywhere. Make the omitted default platform-gated via `DEFAULT_OMITTED_MODE`:
`auto` on Linux (the kernel backend is always available, so let the engine
decide per target) and `off` on macOS/elsewhere (the kext and FSKit backends
each need a one-time, often admin- or MDM-gated approval that can't be assumed
present, so FUSE stays opt-in). Explicit `enabled: true|false|auto` is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
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