Use forkpty from a sandboxed MAS app

My IDE like app built with electron allows to spawn a terminal by means of forkpty. Although this is also working in the MAS build, the sandboxed environment results in the terminal being mostly unusable (which I expected).

From a technical perspective, what would be the best way to allow an unrestricted terminal experience? Obviously I'd need the terminal process to be spawned outside the sandboxed environment.

My initial thinking was that I could create some kind of "Helper" app that the user has to run manually. This app would then run outside the sandbox and would provide a terminal API to my sandboxed app by means of TCP or IPC Socket communication. But this would have multple drawbacks:

  • Uncomfortable for the user because he has to spawn the helper app
  • The communication sockets could be abused by others

I'm sure there exist many other apps on the Mac App Store that face the same problem (running a process outside the sandboxed environment).

What is the best way to solve this? Is it even allowed?

Replies

I don’t really understand your goal here. Can you post more details about the desired user experience? Perhaps you could walk me through a scenario, at the user level, for what you’d like to be able to support?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Sure. The IDE provides a built-in Terminal (powered by node-pty, if that's relevant). The goal is to spawn an interactive terminal process (PTY) that behaves just like the regular Terminal.app. This means that the zsh profile is loaded correctly, and browsing the file system or accessing binaries in PATH works without sandbox restrictions.

Now, spawning the PTY process from within the sandboxed app creates a terminal environment that is also running in a sandboxed context. The access to the file system is limited to the container. Several commands fail because they are either not in PATH, or access to them is denied by the sandbox.

For example, forking a pty process for zsh, you will immediately get an error printed out: zsh: can't set tty pgrp: operation not permitted. You will then get a prompt, but ls /Users will print ls: Users: Operation not permitted because the sandbox does not allow access to it.

To be fair, this is to be expected, and it demonstrates how effective the sandboxing is. In the end the whole purpose of the sandbox is to prevent harmful command execution.

Unfortunately, in the case of a terminal, the user expects an unrestricted terminal environment that works just like he is used to from Terminal.app.

So I'm looking for a way to launch the PTY process outside the sandbox. I was hoping there was some API to gain such a permission (maybe by asking the user for consent), but such a thing does not seem to exist. I'm also not aware of any entitlements that would support such a use-case.

This left me with the following conclusion: Any process spawned by my sandboxed app will also be sandboxed and inherits the restrictions. The only way to spawn a process that is not sandboxed is by having the user manually launch a separate, non-sandboxed app (let's say a daemon-like app without UI that runs in the background and may start automatically when the user logs in), that my sandboxed app could then talk to. The daemon would be responsible for creating the pty processes, and the pty process IO would be piped to the sandboxed app.

Do you agree? And if so, is there an established technical pattern for building such a fully-privileged daemon?

I'm looking for a way to launch the PTY process outside the sandbox. I was hoping there was some API to gain such a permission (maybe by asking the user for consent), but such a thing does not seem to exist.

Yes, but also no (-: More on this below.

Any process spawned by my sandboxed app will also be sandboxed and inherits the restrictions.

This depends on what you mean by “spawned”. A sandboxed app can start processes in two ways:

  • As a child process, using fork/exec, posix_spawn, NSTask, and so on

  • As an independent app, using Launch Services, NSWorkspace, and so on

A child process always inherits its sandbox from its parent. An independent app does not. If the app is sandboxed, it starts in a new sandbox set up based on its App Sandbox entitlements. If not, it starts without a sandbox.

The sticking point for you is that App Review requires that all code within your app be sandboxed. So you can’t include a non-sandboxed helper app within your app, nor can you ask the user’s permission to start some app without its sandbox.

In summary, your conclusion is correct (-:

The only way to spawn a process that is not sandboxed is by having the user manually launch a separate, non-sandboxed app (let's say a daemon-like app without UI that runs in the background and may start automatically when the user logs in), that my sandboxed app could then talk to …

Do you agree?

Not really. The approach you’ve described should work, but I’m not sure it’s the only way to achieve your user-level goal.

Before we go any further, however, I want to clarify some terminology. On macOS a daemon is a privileged component that runs system wide. This is definitely inappropriate for your user case. What you’re describing is an agent (like a daemon but run once per login session), a background-only app (an app-like executable that shows no UI), or a UI element (an app-like executable that shows limit UI, for example, a system-wide status menu).

Coming back to your main issue, I have a further question about your user-level goals. You wrote:

The goal is to spawn an interactive terminal process (PTY) that behaves just like the regular Terminal

If that’s the case then why not use Terminal itself. What’s the ‘value add’ of you doing all this work within your app?

My thinking here is that, rather than extending your app to provide a terminal UI, you could extend Terminal to provide access to your IDE’s features. That’s a lot easier because Terminal is not sandboxed.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

First of all, thanks for your time and patience. I'm a big fan of yours and your answers in many other threads across this forum have helped me many times.

What’s the ‘value add’ of you doing all this work within your app?

The IDE like app allows you to open a Workspace (folder), and then you can edit files within this workspace. Additionally, you can spawn multiple Terminal panels within a workspace, that you can have side-by-side with the files you're editing.

Now, the key selling point of the app is that you can also open Workspaces on remote targets via SSH, SFTP, FTP, Docker and other remote access providers. Most of these remote targets also support launching terminals.

I want this experience to feel seamless, whether the user is working locally or in a remote workspace.

Opening an external application (Terminal.app) for local workspaces will ruin this user experience. It could be the last resort if nothing else works, but I'm really keen to find a better integrated solution that works with the MAS build of the app.

The sticking point for you is that App Review requires that all code within your app be sandboxed. So you can’t include a non-sandboxed helper app within your app, nor can you ask the user’s permission to start some app without its sandbox.

One idea that comes to my mind: Inside the MAS app, when the user tries to open a local terminal, I will ask the user to download and install a non-sandboxed helper app. The helper app itself registers a custom protocol handler, let's say for an URL in the shape of customterminalhelper://<path to a unix socket that was created by the mas app>. When this helper is installed and a local terminal shall be spawned, the MAS app creates a local unix socket that it is listening on, and then call NSWorkspace:openApplication to trigger the launch of the helper app (e.g. customterminalhelper:///path/to/unix/socket). The helper app would then be opened by the OS because it is handling this protocol, spawn a pty process and pipe it to the unix socket created by the mas app.

Do you think something like this would pass the app review process? Is there a better solution?

Do you think something like this would pass the app review process?

App Review has the final say about what is or isn’t allowed on the App Store. I don’t work for App Review and thus can’t give you a definitive answer. However, my experience is that App Review wants apps to be functional as is; they are very wary of apps that require help from a non-App Store component to achieve their core functionality.

Now, the key selling point of the app is that you can also open Workspaces on remote targets

Is there some massive functional difference between local and remote targets? If not, one option here would be to not provide the local target at all. The user can then enable SSH on their machine if they want to run locally.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"