App Sandbox and the loading of libraries written at runtime

We're interested in adopting App Sandbox in an app distributed outside of the Mac App Store. However, we're hitting a bit of a roadblock and it doesn't seem like either of the techniques described in that post can be used in a reasonable way.

For background, this is a third-party launcher for a cross-platform Java game that, among other things, makes it easier for users to mod the game. Users generally download mods as .jar files and place them in a certain directory. In some cases, these mods contain native dynamic libraries (e.g. a .dylib) as part of their code. In general, the .dylib is extracted from the contents of the .jar to some temporary location, loaded, and then deleted once the game closes (the exact details, like the actual temporary location, depends on the mod).

App Sandbox greatly interests us in this case because it can limit the damage that a compromised mod could do, and in my testing the functionality of most mods still works with it enabled. However, sandboxed apps quarantine every file they write to by default. Unfortunately, most mods are created by individual developers who don't notarize their libraries (their mods are generally cross-platform, and they're likely just using third-party code that they bundle with the mod but don't sign or notarize). [1] This means that a mod that loads a dynamic library as described above triggers Gatekeeper as described in the documentation if the app is sandboxed, but does not if the sandbox is disabled.

Even worse, a user often can't bypass the warning even if they trust the mod because the extracted library is usually a temporary file, and generally is deleted after the failure (which usually causes the game to crash and thus close). By the time they try to approve the code in System Settings, the file is gone (and even if they could approve it, this approval wouldn't stick next time they launch the game).

In theory it would work to use an unsandboxed XPC service to remove the quarantine and let the libraries through. However, this is easier said than done. We don't control the mods' code or how they go about loading whatever code they need, which limits what we can do.

[1] And in some cases, people like to play old versions of the game with old mods, and the versions they're using might've been released before notarization was even a thing.


The closest thing I can think of to a solution is injecting code into the Java process that runs code to call out to the XPC service to remove the quarantine before a library loads (e.g. before any calls to dlopen using dyld interposition). A prototype I have... works... but this seems really flimsy, I've read that interposition isn't meant to be used in non-dev tools, and if there's a better solution I'd certainly prefer that over this.

Other things we've tried have significant downsides:

  • com.apple.security.files.user-selected.executable requires user selection in a file picker, and seems to be more blunt than just allowing libraries/plugins which might lead to a sandbox escape [2]
  • Adding the app to the "Developer Tools" section in System Settings > Privacy & Security allows the libraries to load automatically, but requires users to add the app manually and also sounds like it would make a sandbox escape very easy [2]

Oh, and I also submitted an enhancement request for an entitlement/similar that would allow these libraries to load (FB13795828) but it was returned as "no plans to address" (which honestly wasn't that surprising).

[2] My understanding is that if a sandboxed process loads libraries, the library code would still be confined by the sandbox because it's still running in the sandboxed process. But if a sandboxed process can write and open a non-quarantined app, that app would not be within the confines of the sandbox. So basically we want to somehow allow the libraries to load but not allow standalone executables to run outside the sandbox.


In general the game and almost all popular mods I've tested work with App Sandbox enabled, except for this Gatekeeper snag. It would be a shame to completely abandon App Sandbox for this reason if everything else can be made to work.

This situation seems not super common, but documentation does say

When your sandboxed app launches for the first time, macOS creates a sandbox container on the file system (in ~/Library/Containers) and associates it with your app. Your app has full read and write access to its sandbox container, and can run programs located there as well.

which leaves me wondering whether the Gatekeeper prompt is even intended behavior since the libraries are in the sandbox container and written by the app. (By the way, my testing of the claim that apps can run programs in their sandbox container didn't seem to confirm what the documentation said, even without quarantine - FB15963761). Though, given the other documentation page I linked above which more directly references Gatekeeper and quarantined plug-ins, I doubt this is a bug.

I suppose the final question is, is this just a situation where App Sandbox won't work (at least in any supported way)? Or is there perhaps some technique we're missing?

Wow, that’s quite a corner you’ve painted yourself in to (or maybe been painted in to :-).

Though, given the other documentation page I linked above which more directly references Gatekeeper and quarantined plug-ins, I doubt this is a bug.

You are correct. The key point here is that there are two layers of security:

  • App Sandbox
  • Gatekeeper

That quote is about the first. The App Sandbox generally prevents your app from running programs outside of its static sandbox [1]. Your app’s container is within its static sandbox, and the doc is trying to say that executing code from there is OK.

However, that’s completely orthogonal to the restrictions being imposed by Gatekeeper.

I've read that interposition isn't meant to be used in non-dev tools,

Correct. But if you had to use it then that’s unlikely to be your biggest compatibility headache. That is, your overall system is gonna be quite brittle, and I suspect that interposition wouldn’t be the first thing to break O-:


Anyway, let’s see if we can avoid that. And to start, I’d like to clarify an aspect of this. Am I right in assuming that each mod’s entry point is in Java? So at some point the mod transitions from Java to native code? How does that transition happen? And could you hook that?

I’m thinking that the mod must use some sort of JNI-y thing in order to get to native code, and there must be explicit support for that in your Java VM. If you could tweak that support then that’d provide an equivalent to your dlopen interpose, without the interpose.

This would, of course, fall down if a mod’s native code unpacks more native code, so I’m curious if that happens in practice?

Share and Enjoy

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

[1] In contrast to your dynamic sandbox. I define those terms in On File System Permissions.

Wow, that’s quite a corner you’ve painted yourself in to (or maybe been painted in to :-).

Haha, yeah 😅 more so the former, luckily(?) - the joys of trying to pick up a several year old open source issue (definitely seeing why no one has done it yet)

Am I right in assuming that each mod’s entry point is in Java? So at some point the mod transitions from Java to native code? How does that transition happen? And could you hook that?

Yes. In general Java's System.load function is used to load it with JNI. (Well, more specifically, the mod probably is using some Java library it depends on that calls System.load rather than the developer directly using it, but from our perspective that should basically be the same thing.) And on hooking it...

If you could tweak that support then that’d provide an equivalent to your dlopen interpose, without the interpose.

Hmm, maybe. The two ways I can think of that we could hook at the Java level is either distributing modified JVM runtimes or maybe using Java agents/instrumentation (which I'm woefully unfamiliar with).

Though we do allow users to select their own JVM (sometimes different versions of the game need different JVM versions, and it can sometimes be useful to select specific versions to workaround certain bugs in certain cases), which would mean the second option is likely the better option.

This would, of course, fall down if a mod’s native code unpacks more native code, so I’m curious if that happens in practice?

I don't believe this really happens (I'm making an assumption based on the fact that most mods I've extracted only contain one library at maximum, so I don't think we'll see any "native loads more native" situations), so I think we can ignore this case. Though, in the rare case it does happen, we would intend to distribute both sandboxed and unsandboxed versions and allow users who really want to use these mods (or simply need to use mods that are fundamentally incompatible with the sandbox) to use the unsandboxed version.

Though, one question I have is that if I simply move the same implementation I have working (call to XPC service which checks that file is actually a library then removes quarantine, then proceed with library load) to move from a dlopen interposition to some Java instrumentation of System.load, wouldn't your comment about the overall system being brittle

That is, your overall system is gonna be quite brittle, and I suspect that interposition wouldn’t be the first thing to break O-:

still apply? Or are you just speaking in general, that if I had to do this hack, there's probably a lot of other, more fragile hacks in place too?

App Sandbox and the loading of libraries written at runtime
 
 
Q