Unsandboxed app can't modify other app

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:

  1. (Re-)install Chrome with its DMG installer.
  2. Run the following command in Terminal: mkdir /Applications/Google\ Chrome.app/test. This works.
  3. Undo the command: rm -rf /Applications/Google\ Chrome.app/test
  4. Start Chrome and close it again.
  5. 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.

Answered by DTS Engineer in 833148022

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"

I think that the system doesn't consider an app an actual app until it's launched for the first time. That first launch triggers the verification process and integrates the app with launch services. If you modify the app before then, it would invalidate the signature anyway.

See the WWDC 2022 reference in this post.

It sounds like apps that use the same developer signature can update each other. They can also allow a specific bundle ID to update via an Info.plist entry.

I can't say for sure which method Google is using. I didn't dig into it that much. I despise those auto-updaters. I only keep Chrome around because it makes it easy to change one specific request field for testing multi-language websites. I keep the auto-updater disabled at all times. If I used Brave, I would do the same.

I also dislike those auto-updaters, but additionally, I wouldn't trust a Brave auto-updater. The history of things Brave has done puts me off letting them auto-install such things.

(For example, adding their own affiliate links to urls, crypto stuff...)

If I were to ever install Brave again, I would also disable the auto-updater.

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"

Unsandboxed app can't modify other app
 
 
Q