Running ffmpeg from a sandbox

I'm trying to run ffmpeg from a sandboxed macOS app. ffmpeg itself is installed through homebrew in:
Code Block
/usr/local/bin/ffmpeg

which is actually a symlink to:
Code Block
/usr/local/Cellar/ffmpeg/4.3_1/bin/ffmpeg


I'm using NSOpenPanel to let the user locate the binary and if I open the symlink I can read the contents of the binary, but I can't execute it. Same if I open the destination file.

Here is some sample code:
Code Block swift
let panel = NSOpenPanel()
panel.begin { response in
guard response == .OK, let url = panel.url else {
return
}
print("> \(url.path)")
do {
let data = try Data(contentsOf: url)
print("> \(data.count) bytes")
let p = Process()
p.executableURL = url
try p.run()
} catch {
print("ERROR: \(error.localizedDescription)")
}
}


This generates the following output:
Code Block
> /usr/local/Cellar/ffmpeg/4.3_1/bin/ffmpeg
> 297536 bytes
ERROR: The file “ffmpeg” doesn’t exist.


Accepted Reply

However I do get access OK and a successful execution when I open e.g. /usr/bin/uname instead.

Right. A sandbox has static and dynamic extensions. The static extensions represent all areas of the file system that you can access based on the way your code is signed. For example, all sandboxed apps have a static extension to access /System/Library/Frameworks, while an app signed with the com.apple.security.temporary-exception.files.absolute-path.read-only entitlement has a static extension for each path listed therein.

In contrast, a dynamic extension presents access you’re granted at runtime, via the standard file panels, drag’n’drop, AppleScript, security-scoped bookmark resolution, and so on.

/usr/bin is covered by a static extension whereas your access to /usr/local requires a dynamic one. It would seem that this dynamic extension is blocking execution.

Share and Enjoy

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

Replies

What are you trying to accomplish here? You can execute other apps from a sandboxed Mac app. But those apps will run in your app's sandbox. There is no guarantee that any other app will work properly. Furthermore, if that other app does something strange, like use a symlink, then you are going to be at the mercy of how well said app is implemented.

Essentially, homebrew is not compatible with the sandbox. It is installed in one of the few system locations that is inaccessible without a sandbox escape. This isn't something that you can fix or that homebrew will fix. If you want to provide FFmpeg support in your sandboxed app, the best approach would be to provide your own FFmpeg installer on your website. You can't bundle it inside your app due to licensing issues. But you can provide a stand-alone version that will be compatible with your app.
Your concerns are valid but they don't answer why I get the error that ffmpeg does not exist, even though I can obviously read its contents. At this point I'm just trying to understand how the sandbox works.

What’s happening here is that the open panel is extending your sandbox to allow read access to the item but not execute access. Try adding this to your code:

Code Block
let success = access(url.path, X_OK) >= 0
if success {
print("access OK")
} else {
print("access failed, error: \(errno)")
}


It prints:

Code Block
access failed, error: 1


where 1 is EPERM.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Thanks, indeed I get access failed. However I do get access OK and a successful execution when I open e.g. /usr/bin/uname instead.

Why is there a difference and how can I grant access to execute a user selected binary?

However I do get access OK and a successful execution when I open e.g. /usr/bin/uname instead.

Right. A sandbox has static and dynamic extensions. The static extensions represent all areas of the file system that you can access based on the way your code is signed. For example, all sandboxed apps have a static extension to access /System/Library/Frameworks, while an app signed with the com.apple.security.temporary-exception.files.absolute-path.read-only entitlement has a static extension for each path listed therein.

In contrast, a dynamic extension presents access you’re granted at runtime, via the standard file panels, drag’n’drop, AppleScript, security-scoped bookmark resolution, and so on.

/usr/bin is covered by a static extension whereas your access to /usr/local requires a dynamic one. It would seem that this dynamic extension is blocking execution.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
I see, so basically there is no way to give access to run an external binary from a sandboxed app? (unless it happens to be in one of the system dirs that have an exception).

And if that is the case, can even what Etresoft suggested ever work? Is there a path that a binary could be downloaded that can then be executed? It would seem that this is not possible, otherwise I could just copy the binary from /usr/local/bin there. That would mean that any GPL tool is completely off limits for a sandboxed app (unless the app itself is GPL), which is unfortunate.

there is no way to give access to run an external binary from a sandboxed app? (unless it happens to be in one of the system dirs that have an exception).

There are a number of ways to run an external binary from a sandboxed app. You are just trying one, particularly difficult way. Due to the nature of this particular tool, running it from the sandbox may be challenging.

Is there a path that a binary could be downloaded that can then be executed? It would seem that this is not possible, otherwise I could just copy the binary from /usr/local/bin there. That would mean that any GPL tool is completely off limits for a sandboxed app (unless the app itself is GPL), which is unfortunate.

To clarify a couple of things...

First of all, the GPL limitation is for the Mac App Store. You can sandbox your app and distribute it directly. Apple would encourage that. And you could give yourself any kind of temporary exception you want (within reason). The GPL is a legal restriction, not a technical one.

Assuming you are talking about the Mac App Store here, Apple explicitly allows plug-ins in the Mac App Store, "Apps distributed via the Mac App Store may host plug-ins or extensions that are enabled with mechanisms other than the App Store."

You would still have technical restrictions due to the sandbox. But those restrictions only app to processes that your sandboxed app launches via traditional fork (or fork-like) operations. What I'm saying is that you could wrap FFmpeg in a stand alone app that listens for input via Bonjour or some other IPC. Then, your sandboxed app could launch that FFmpeg wrapper (via NSWorkspace) and pass any work to be done via your IPC channel. You might be able to do this all in the URL, perhaps via a Universal Link.

Your wrapper would have to be GPL to comply with FFmpeg's license. Or your could develop a more general wrapper that wrap any open-source tool. Then you wouldn't need the GPL. At this point, your only remaining hurdle would be to deliver useful functionality in the Mac App Store without the plugin. Apple requires Mac App Store apps to do something functional and useful without any great area "extras".

From an implementation standpoint, all of this is much easier than it sounds. I would suggest a wrapper that is specific to FFmpeg and supports only the options that your app needs to use.

An even better idea would be to see if you can implement this using a supported Apple API. Then you don't have to do any extra work.
There are business issues and technical issues here. On the business front you have GPL and you have the App Store Review Guidelines (especially clause 2.4.5 (iv)), and I can’t comment on either of those.

On the technical front, one option here is to put the executable within a place that’s covered by your static sandbox extensions. An obvious location is within your app bundle. Or within your app’s container directory.

On the dynamic extension front, based on the simple tests I’ve run I don’t think that’s possible. However, I can’t be 100% sure without doing more research, and I can only do that in the context of a DTS tech support incident.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Thank you eskimo and Etresoft for your helpful replies.

I think you both verify that there is no way to let a user select a binary to run from inside a sandboxed app. The same from any binary downloaded from the internet.

The only possible solution would be to bundle it inside the app, which would forbid the use of any GPL tool (either on App Store, or anywhere else really).

This is a shame, I'll try to find a different solution.

Thank you for your time.

I think you both verify that there is no way to let a user select a binary to run from inside a sandboxed app. The same from any binary downloaded from the internet.

That's not what I said at all. I once shipped a Mac App Store app that did exactly that. But whatever.
  • Coming back to your reply @Etresoft

    If one would like to bundle ffmpeg inside a Mac App Store app - could one use a static build (from https://ffmpeg.org/download.html#build-mac) or does it have to be built from source and codesigned? I have made a macOS App using the static build of ffmpeg- everything works well when building the App in Xcode. However uploading to AppStore always brings the "App sandbox error" (ITMS-90296).

Add a Comment
.

However uploading to AppStore always brings the "App sandbox error"

Just to reiterate, I’m only able to comment on the technical side of this.

On that front, since I last posted on this thread we’ve published a number of docs relevant to issues like this, most notably:

Share and Enjoy

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