We have a native ARM64 application. The application is a development environment and native compiler for the language Common Lisp. CL has a foreign function interface, which allows loading of .dylib
files into CL and calling functions in them from CL. For this reason, we add certain entitlements. See below.
It is notarized and installed on macOS 14.7. When I run spctl
on it I get this:
$ spctl --assess -v /Applications/AllegroCL64.app
/Applications/AllegroCL64.app: rejected (the code is valid but does not seem to be an app)
That’s before I run it. Which is odd because the app is notarized. When I run the app, it asks for a license file and installs it into /Applications/AllegroCL64.app/Contents/Resources/
and after that, the spctl
shows this:
$ spctl --assess -v /Applications/AllegroCL64.app /Applications/AllegroCL64.app: a sealed resource is missing or invalid
I assume the mere act of copying the license (a file called devel.lic
which is a small text file) is causing this. Why does it say it “does not seem to be an app”?
This self-modification of the files in the Contents/Resources
directory is a huge feature. We allow downloading of patches, which add features and fix bugs in the product. Is this going to be a problem, going forward? I don’t remember seeing this result from spctl
before and I have a feeling it’s a new , due to tightening of security policies, etc.
All of this is quite worrying to us.
More details of the app:
$ codesign -vvvv mlisp
mlisp: valid on disk
mlisp: satisfies its Designated Requirement
$ codesign -d --entitlements - /Applications/AllegroCL64.app
Executable=/Applications/AllegroCL64.app/Contents/MacOS/AllegroCL64
[Dict]
[Key] com.apple.security.cs.allow-dyld-environment-variables
[Value]
[Bool] true
[Key] com.apple.security.cs.allow-jit
[Value]
[Bool] true
[Key] com.apple.security.cs.disable-library-validation
[Value]
[Bool] true
[Key] com.apple.security.get-task-allow
[Value]
[Bool] true
$
Other details:
- The app was built with the Command Line tools version 2395 on macOS 12.x.
There are two cases to consider here:
-
Self-modifying apps
-
Software updates
I’ll tackle each in turn.
Modifying your own app is not a supported technique. This is not a change in policy. It’s been this way since 1984. There are plenty of historical docs that are clear about this, and we also state it clearly in the Separate read-only and read/write content section of Embedding nonstandard code structures in a bundle.
The nature of unsupported things is that sometimes they work and sometimes they don’t. So the fact that something might have worked in some specific situations isn’t helpful. And even if you get something working today, you may run into problems in the future.
My goal is to help you move to a supported approach that should work now and in the future. I regularly help folks in your situation. For example, you might find this post and this post enlightening.
Regarding your licence setup, storing the licence outside of the app is the correct path forward. Exactly how you do that depends on a variety of tradeoffs. Personally I like this approach:
-
Store the licence in Application Support directory (
.applicationSupportDirectory
) in the user domain (.userDomainMask
). -
Look for the licence in the same directory in all domains (
.allDomainsMask
).
That way the site admin can move the file from their home directory to /Library/Application Support
, or the much more obscure /Network/Library/Application Support
, if they want the app to work for other users.
Note I’m referencing the FileManager.urls(for:in:)
method here.
Regarding stuff that’s not your licence, I’m going to defer that until after our discussion of software updates.
And on the subject software updates, you wrote::
How do applications patch themselves and still not run afoul of Gatekeeper, then? Are they just complete reinstalls?
Not necessarily.
Software updaters generally don’t patch a live app. Doing so is risky because, if something goes wrong, the app is left in an inconsistent state.
IMPORTANT Because software updaters don’t do that, Gatekeeper now works hard to prevent it. See the link to the WWDC talk about app bundle protection in Trusted Execution Resources.
Rather, an incremental updater will usually copy the app and apply the deltas to it. Once the new app is all done, it replaces the old app with the new. If you wanna be fancy, you can use renamex_np
to do this atomically.
The key point here is that the updater doesn’t:
-
Modify an app that’s been run.
-
Or run an app until it’s fully formed, meaning that it’ll pass Gatekeeper.
Preparing the deltas can be a bit tricky. The basic idea is:
-
Build, sign, notarise, and stapler the old.
-
Build, sign, notarise, and stapler the new.
-
Generate the delta from 1 to 2.
Finally, this incremental update mechanism could take twice the disk space but that’s not always true. If the volume supports cloning, the copy is actually a clone and thus only takes extra space as you modify things.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"