Skip to content

feat(graalvm): run the native image under the macOS App Sandbox - #899

Open
kdroidFilter wants to merge 4 commits into
nucleus-3.0from
feat/graalvm-sandbox
Open

kdroidFilter wants to merge 4 commits into
nucleus-3.0from
feat/graalvm-sandbox

Conversation

@kdroidFilter

@kdroidFilter kdroidFilter commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

What

A GraalVM app can now ship a Mac App Store PKG. When a macOS App Store format is configured, the plugin builds a second, sandboxed native image beside the regular one and packages the PKG from it. The regular image (command line, inputs, bundle, DMG/ZIP/Developer ID PKG) is unchanged.

Why a regular image could not run sandboxed

  1. Signal handling: Substrate creates the named POSIX semaphore /cSunMiscSignal-<pid> at start-up; the App Sandbox denies it and the image died before main. The sandboxed image is compiled with -R:-EnableSignalHandling, and TerminationSignals gives SIGINT / SIGTERM / SIGHUP their shutdown hooks back through libdispatch (exit 128 + signal, like HotSpot). The handler is an empty sigaction, not SIG_IGN, so child processes still die on SIGTERM.
  2. user.home: SubstrateVM reads it from the password database, outside the container. GraalVmInitializer follows HOME when APP_SANDBOX_CONTAINER_ID is set.
  3. Native code extracted at run time (JNA, zstd-kmp, compose-webview, tray) is quarantined in the sandbox. The image reuses the JVM store pipeline: libs extracted at build time, signed in Contents/Frameworks, load sites routed to NucleusSandboxLoader.

New tasks

stripGraalvmNativeLibsForSandboxing, stageGraalvmSandboxedJars, extractGraalvmNativeLibsForSandboxing, nativeImageCompileSandboxed, createSandboxedGraalvmNativeDistributable, runSandboxedGraalvmNativeDistributable. Details: docs/internals/graalvm-app-sandbox.md.

Verified (Lab, macOS 27 arm64, GraalVM CE 25.4)

  • Sandboxed Lab launched through LaunchServices: reports sandboxed, checks.json written in the container and reloaded, the 4 extracted libs load from Contents/Frameworks (zstd round trip identical), no file-access denial.
  • SIGTERM / SIGINT / SIGHUP: clean exit 143 / 130 / 129, same as the regular image.
  • packageGraalvmPkg packages the sandboxed bundle; the app taken from the PKG payload behaves the same (single instance included).
  • graalvm-runtime: tests (incl. new TerminationSignalsTest on a -Xrs child JVM), apiCheck, detekt, ktlint.
  • Plugin: detekt/ktlint clean on changed files; :plugin:test locally shows the same 7 host-dependent failures as the base branch (LinuxSigner, RenameMacAppBundle, ElectronBuilderLinuxDesktopEntryConfig), which pass in CI.

Also fixed (found while torturing the sandboxed Lab)

  • Pre-existing: any native image started through a symlink (or, sandboxed, a relative path) died with "Fontconfig head is null"; GraalVmInitializer.execDirOf now canonicalizes the launch path.
  • An explicit -Duser.home is kept inside the sandbox.

Torture run: every Lab probe opened sandboxed without crash; signal storms and start-up signals match the regular image; concurrent launches, kill -9 relaunch and accented paths behave. Details in the doc.

Not verified

  • A PKG signed with a real "3rd Party Mac Developer" identity and installed (no identity on the test machine).
  • Known and unchanged: an FSEvents watch inside the container logs harmless file-read-data denials on its ancestors (notify's WatchRoot flag), independent of GraalVM.

A Mac App Store PKG of a GraalVM app is now packaged from a second,
sandboxed image, built only when such a format is configured:
- third-party native libraries are extracted at build time into
  Contents/Frameworks and their load sites routed to NucleusSandboxLoader,
  reusing the JVM store pipeline's strip/extract tasks;
- the image is compiled without Substrate signal handling, whose named
  POSIX semaphore the sandbox denies;
- GraalVmInitializer wires Frameworks, the shim manifest and JNA for that
  bundle, and points user.home at the container when sandboxed.

The regular image, bundle and packages are unchanged.
The sandboxed image is compiled without Substrate's signal handling, so
SIGINT/SIGTERM/SIGHUP killed it without running the shutdown hooks.
TerminationSignals receives them through libdispatch (kqueue, allowed by
the App Sandbox) and exits like HotSpot: shutdown hooks, then 128 + signal.

The handler is an empty sigaction rather than SIG_IGN, which would be
inherited across exec and make every child process ignore SIGTERM.
…cit user.home

- execDirOf canonicalizes the launch path: an image started through a
  symlink (a CLI link in /usr/local/bin) looked for its fonts and
  libraries next to the link and died with "Fontconfig head is null".
  A relative launch path is resolved against PWD, since SubstrateVM's
  user.dir is not the start directory inside the App Sandbox.
- Inside the sandbox user.home only moves to the container while it still
  holds SubstrateVM's default, so an explicit -Duser.home wins.
- Document the torture run of the sandboxed Lab.

This branch has not been deployed

No deployments
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