Parallel processing app distributed by developer won't run sandboxed

I have a macOS app that I have been distributing for free outside the app store for more than 15 years, without notarization, without sandboxing, and without hardened runtime, all with no problems.

If I understand correctly, macOS will soon be modified so that it will not launch any developer-distributed apps that are not notarized. Notarization will require both hardened runtime and sandboxing, and unhappily, my app will not run when notarized -- I have added sandboxing and hardened runtime, than gotten it notarized and tried -- and that is because it will not run when sandboxed. Thus I have two questions:

  1. Will there be some means, that I perhaps have missed, for my users to run my app as is, in un-notarized form with no sandboxing and no hardened runtime? (Assume that they are willing to click "Okay" on any macOS popups of the form "Abandon hope, all ye who enter here.") Perhaps I have missed something about the signing or distribution process ... ?

  2. If not, is there some entitlement I can obtain to allow my app to run when sandboxed? Perhaps the question is even "Should there be such an entitlement?" And to that end, I must now explain why it cannot run sandboxed:

My app is a parallel processing system: To work properly it must open multiple copies of itself -- that's right, there will be multiple instances of the app window visible on the console, distinguished by tint, title and location so the user can tell which is which, and multiple app badges in the dock, similarly distinguished. Doing so is easy -- I use the c++ "system" function to call the Unix executable that is buried within the ".app" folder, passing it a command tail whereby the launched copy can tell how to distinguish itself. I build up the text string for the call piece by piece, but the result looks rather like this:

system("<path-to-my-app>/MyApp.app/Contents/MacOS/MyApp -tail-item-1 -tail-item-2 ... &");

The app is written in mixed C++ and Objective C. The usual "Main.mm" file contains the entry point for the program, a "main()" function that does nothing but call "NSApplicationMain()", but I have added code to "main()" that runs before the call of NSApplicationMain(). That code uses C function "getopt()" to look for the extra command-tail items. If any are present, the app acts appropriately -- generally assigning non-default values to global variables that are used later in initialization.

The first instance of the app that is called -- presumably by the user mousing on an icon somewhere -- knows by the absence of extra command-tail items that it is the first one launched, and thus knows to launch multiple additional instances of itself using this mechanism. The launched instances know by the presence of extra command-tail items that they are not the first one launched, and act differently, based on the command-tail items themselves.

All this has been working fine for over a decade when the app is not sandboxed and does not have a hardened runtime.

For what it is worth, the app will run with hardened runtime, provided the option "Disable Executable Memory Protection" is checked. Furthermore, when it is also sandboxed and I open it with no extra copies of itself launched (the number to launch is a preferences option), that single app instance runs fine.

I have instrumented the code, and what seems to be happening is that the system call to launch another app returns zero -- implying it succeeded -- but has no effect: It is as if someone had special-cased "system" to do nothing, but to report success nonetheless. That is an entirely reasonable feature of a hardened runtime -- allowing arbitrary system calls would be a security disaster looking for a place to happen.

The point is that my app would not be making an arbitrary system call -- it would be trying to open one specific app -- itself -- which would be sandboxed with a hardened runtime, and notarized. That is not likely to be a huge security problem.

Incidentally, not all system calls fail this way -- I can do

system("osascript -e 'tell app \"Safari\" to activate';");

or

system( "open -a \"Safari\" <path to a help file located in MyApp's Resources>");

with impunity.

Also incidentally, using AppleScript to launch another copy of MyApp from within itself doesn't do what I want: The system notices that MyApp is already running and just makes it active instead of launching a new copy, and there is no way to pass in a command tail anyway.

I don't wish to appear to be advertising, so I won't identify my app, but a little more detail might be useful: It is a parallel program interpreter. The language implemented is the "Scheme" dialect of Lisp. Each instance running is a complete read/eval/print loop embedded in an application window where the user can read and type. The first instance of the app launched mmaps a large memory area for the Lisp system's main memory: That works kind of like a big heap in more conventional programs. It is not executable code, it contains Lisp data structures that an application instance can access. The other instances launched use the same mmapped area. The shared memory has lots of lock bits. I use low-level "lockless coding" -- hand-coded assembler with the Intel "lock" prefix or the more complicated arm64 stuff -- to keep simultaneous access by different app instances from corrupting the shared memory.

Parallel Scheme has many uses, which include debugging and monitoring of running Scheme programs, and having multiple tail-recursive "actors" (Lisp jargon) operate on the same data at the same time.

Enough said. I would like to be able to notarize this app so that users who obtained it outside the app store could understand that Apple had checked it for dangerous code. If that were possible, I might even try submitting it to the app store -- but that would be another story.

Do I have any hope of keeping this product available?

Replies

If I understand correctly, macOS will soon be modified so that it will not launch any developer-distributed apps that are not notarized.

That’s not true. Well, technically it’s a statement about the future, and I can’t talk about that, but I can say that Apple has not made any announcements along those lines.

The current situation [1] is that:

  • An app must be notarised to pass Gatekeeper.

  • There are ways for users to bypass Gatekeeper.

Having said that, I recommend that you build your app so that it passes Gatekeeper. There are two reasons for this:

  • It builds trust with your users. Speaking for myself, if I download an app and it requires me to bypass Gatekeeper, I pretty much stop there.

  • It’s not hard to imagine a future where it gets harder to bypass Gatekeeper.

my app will not run when notarized -- I have added sandboxing and hardened runtime

Hmmm, App Sandbox and the hardened runtime are very different things. App Sandbox is only required by apps distributed via the Mac App Store. If you distribute your app independently, you may enable App Sandbox but you are not required to do so.

For the moment I recommend that you avoid App Sandbox and just enable the hardened time.

Reading through your post it’s clear that you’re doing some wacky stuff. Your current approach is definitely not compatible with the sandbox, and it’s not clear whether it’d be possible to fix that. I actually think it might be, but that’s neither here nor there. The reality is that you don’t need to sandbox your app, so we don’t have to explore those options unless you change you mind about Mac App Store distribution.

For what it is worth, the app will run with hardened runtime

Cool.

provided the option "Disable Executable Memory Protection" is checked.

That’s a good start but, in the medium term, I recommend that you explore a more secure option. There are three relevant entitlements here:

  • com.apple.security.cs.allow-jit

  • com.apple.security.cs.allow-unsigned-executable-memory

  • com.apple.security.cs.disable-executable-page-protection

These are listed from most to least secure. I recommend that you explore the most secure one, com.apple.security.cs.allow-jit. For hints and that, see Resolving Hardened Runtime Incompatibilities.

However, if that’s too much trouble you should be able to get things working with com.apple.security.cs.allow-unsigned-executable-memory.

I’ve seen a lot of folks use the least secure option com.apple.security.cs.disable-executable-page-protection, but I’ve yet to see an example of where it was used for a good reason (-:

Share and Enjoy

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

[1] This includes the latest macOS 14 beta and any announcements we’ve made about future changes in this space.

Quinn, thank you for your thoughtful reply. I will explore more secure options for hardened runtime, will report here in case of difficulty, and will keep my fingers crossed about future requirements for distributing un-notarized apps outside the Mac App Store..

However ...

For the long term, I am thinking that it might be fun if I could distribute my app through the Mac App Store. I expect that would be an uphill battle, but one obvious first step is getting it to run when sandboxed. So if you have any hints or suggestions for how to do that, or for helpful documents I might usefully read, please let me know.

On the theory that a picture is worth a thousand words, I include (if the system will allow it) a link to my own web site which shows (scroll down) some screen shots of the app running, and which has other links to lots of documentation about it:

http://www.jayreynoldsfreeman.com/My/Wraith_Scheme_%2864-bit_version%29.html

You state:

... it’s clear that you’re doing some wacky stuff ...

Nice to know that we mere out-in-the-field developers can still surprise the folks at Apple. :-)

The URL in the reply immediately above isn't formatted correctly, try this one ...

http://www.jayreynoldsfreeman.com/My/Wraith_Scheme_(64-bit_version).html

... and scroll down to see images.

For the long term, I am thinking that it might be fun if I could distribute my app through the Mac App Store.

To deploy your app via the Mac App Store, it must be sandboxed. If it’s sandboxed, it can’t launch itself as a child process [1]. However, you can launch yourself using NSWorkspace, and NSWorkspace has an option, createsNewApplicationInstance, that lets you create a second instance of your app.

This does change a few things though. The launched app won’t be a child process of your main app, so you lose some control over it. Most notably, you’ll have to come up with a way to share memory between your apps. That should be possible using an app group.

Share and Enjoy

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

[1] This is due to the way sandbox inheritance works. I discuss the background to that in Resolving App Sandbox Inheritance Problems.

I thought I would report progress and success, with some details in case anyone else is having similar troubles. I hope Quinn or someone else more knowledgeable than I will correct me if my report is misleading.

I have successfully created and made available for non-App-Store distribution, a sandboxed version of my application, for which Gatekeeper observes that Apple has been able to check the app for malicious software and found none.

Huzzah ...

All the information about how to do that was available here or on other Apple sources, but there has been a lot of confusion about notarization, so it took some effort for a relative newcomer, like me, to put the pieces together, the more so since my app is a bit weird.

First, it was clear from various postings (including some of Quinn's) that one must treat the app bundle as a tree rooted at "MyApp.app", and work from the leaves to the root, signing anything that needed signing. My app had several embedded binaries, so I did this by CDing to the directories that contained them, before building, and using

codesign --force --options runtime --sign "Developer ID Application: <my name> <my team ID>" ./<embedded binary>

I set up a script to do that at build time.

I let Xcode manage signing the app, and enabled sandboxing with the entitlements I needed. So far so good.

I was a blindsided for a while by having had to do the extra signing as a separate task: I did not realize that the "Distribute App" button at the top right corner of the Xcode (14.3.1) Organizer window would walk me through the notarization process, notwithstanding that I had had to pre-sign several embedded binaries beforehand. But once I had made an archive of the app (also an Xcode task), that button did its job, and the process provided useful messages when I had not gotten everything straight, so that notarizing actually turned out to be pretty easy once I got used to it.

I also notarized a couple of disc images for distribution -- one with just the app and one with source code. To do so, I compressed the disc images via the "compress" option in the menu that pops up when you control-click on something in Finder. To notarize, I used "notarytool" from the command line, and the command that worked for me was (deep breath):

xcrun notarytool submit <path to compressed .dmg> --wait --apple-id <my id> --password <specialized app password> --team-id <team-id number>

Getting those command arguments just right was a bit of a pain -- your mileage may vary.

That done, I stapled the ticket to the disc image (not the compressed version), also from the command line, via:

xcrun stapler staple <path to disc image>

I hope all this makes sense.

I will have to experiment with NSWorkSpace to see if I can also enable hardened runtime. That will take some time, and it may turn out that hardened runtime will not work at all. I may report here further, depending on what I learn.

  • Thanks for the update!

Add a Comment