I work for Brave, a browser with ~80M users. We want to introduce a new system for automatic updates called Omaha 4 (O4). It's the same system that powers automatic updates in Chrome.
O4 runs as a separate application on users' systems. For Chrome, this works as follows: An app called GoogleUpdater.app
regularly checks for updates in the background. When a new version is found, then GoogleUpdater.app
installs it into Chrome's installation directory /Applications/Google Chrome.app
.
But consider what this means: A separate application, GoogleUpdater.app
, is able to modify Google Chrome.app
.
This is especially surprising because, for example, the built-in Terminal.app
is not able to modify Google Chrome.app
. Here's how you can check this for yourself:
- (Re-)install Chrome with its DMG installer.
- Run the following command in Terminal:
mkdir /Applications/Google\ Chrome.app/test
. This works. - Undo the command:
rm -rf /Applications/Google\ Chrome.app/test
- Start Chrome and close it again.
mkdir /Applications/Google\ Chrome.app/test
now fails with "Operation not permitted".
(These steps assume that Terminal does not have Full Disk Access and System Integrity Protection is enabled.)
In other words, once Chrome was started at least once, another application (Terminal in this case) is no longer allowed to modify it.
But at the same time, GoogleUpdater.app
is able to modify Chrome. It regularly applies updates to the browser. For each update, this process begins with an mkdir
call similarly to the one shown above.
How is this possible? What is it in macOS that lets GoogleUpdater.app
modify Chrome, but not another app such as Terminal? Note that Terminal is not sandboxed.
I've checked that it's not related to codesigning or notarization issues. In our case, the main application (Brave) and the updater (BraveUpdater) are signed and notarized with the same certificate and have equivalent requirements, entitlements and provisioning profiles as Chrome and GoogleUpdater.
The error that shows up in the Console
for the disallowed mkdir
call is:
kernel (Sandbox) System Policy: mkdir(8917) deny(1) file-write-create /Applications/Google Chrome.app/foo
(It's a similar error when BraveUpdater tries to install a new version into /Applications/Brave Browser.app
.)
The error goes away when I disable System Integrity Protection. But of course, we cannot ask users to do that.
Any help would be greatly appreciated.
The approach I recommend in situations like this is to construct a fully-formed new version of the app and then move that into place. That gets you out of the business of modifying app bundles, which has two benefits:
-
You avoid any app bundle protection entanglements.
-
If something goes wrong, the user’s original app remains intact.
-
You can check the code signature on the final app to make sure it’s what you expect.
The only drawback to this approach is that it consumes extra disk space, but that doesn’t have to be true. If a file is unchanged between the old and the new app, copy it from the old to the new using an APFS clone. That clone is a completely separate file, it just happens to share the blocks on disk as the original.
Regarding the code signature point I made above, you really want to make sure that the final app:
-
Has a valid code signature.
-
That’s covered by a notarised ticket.
To do the first point, use the code signing APIs to verify the signature of the app. To do the second point, get the cdhash of the app and verify that it matches the cdhash of the app that you submitted to notarisation.
For more about cdhashes, see TN3126 Inside Code Signing: Hashes.
To validate a code signature, use SecStaticCodeCheckValidityWithErrors
or one of its related APIs. Make sure that:
-
You check all architectures (
kSecCSCheckAllArchitectures
). -
You check nested code (
kSecCSCheckNestedCode
). -
You enable strict checking (
kSecCSStrictValidate
). -
The code is in a directory that’s not accessible to others. If you’re running as root, that’s easy to set up: Make the directory only readable by root. If you’re not running as root then take advantage of app group container protection.
-
You’re prepared to wait a while. A full check like this is slow, so make sure you factor it in to your user experience.
To get the cdhash of the app after you’ve validated it, call SecCodeCopySigningInformation
with kSecCodeInfoCdHashes
(or the older kSecCodeInfoUnique
).
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"