XPC restricted to processes with the same code signing, but sandboxed

TL;DR How to make this question's answer work within the sandbox? Is there a good way to make calling SecCodeCopyGuestWithAttributes possible from within a sandbox?



We are developing a macOS system extension (more precisely a DNSProxy Network Extension) that is providing an NSXPCListener which is communicating with its host app via XPC, just as in the SimpleFirewall code example from WWDC 2019.

Both the host app and the system extension are sandboxed and share an app group, and this is a requirement as we want to be accepted into the mac app store.

As we believe it is good practice to keep potential attack surfaces small, we would like to make sure that only our host app can connect to the system extension via XPC.

There are numerous posts here on what exactly to do, most helpful is this one which inspired this question's title. The crucial step for this question is to get the connecting process' code object through a call to SecCodeCopyGuestWithAttributes by process id, which is provided in NSXPCListenerDelegate.listener(_, shouldAcceptNewConnection). From within the sandbox this returns an OSStatus of 100001, which is some general "Operation not permitted" and in the console I can see an error logged by kernel where the sandbox denies read access to the .app file of the connecting app:
Code Block
error 14:12:13.946693+0200 kernel Sandbox: <bundleidentifier>(33545) deny(1) file-read-data /Users/<name>/Library/Developer/Xcode/DerivedData/<projectworkspace>/Build/Products/Debug/<appname>.app

If I disable the sandbox, or add a temporary exception entitlement to allow read access to the Xcode/DerivedData folder then I can retrieve the code object, so I conclude that indeed the sandbox blocking the read of the file is the problem.

The obvious fix to me seems to be to add a
Code Block
com.apple.security.temporary-exception.files.absolute-path.read-only
entitlement for /Applications/<appname>.app/, but this a) might be frowned upon in the app review and b) seems somewhat brittle. For example, it would prevent the app from working from inside the user's application folder, and needs a separate entitlement to work from the Xcode debugger.

Since making sure that your inter process communication is only open to a few trusted apps seems like a fairly standard precaution, I would like to ask whether there is a better option, or a recommended way to secure your XPC against unwanted connections from within a sandbox?



P.S. 1: This fairly recent question is very similar, however it remains without a satisfying conclusion and I hope to provide a better solution for the benefit of future investigators already inside this question.

P.S. 2: The fact that NSXPCListener is running inside a system extension rather than any other "normal" app in the same app group is likely not relevant, just note that the system extension is running as root while the host app is running as the normal user. This might for example make this bug (r. 63976204) relevant as we are targeting Catalina (macOS 10.15), but we are already using the entitlement necessary for a workaround for unrelated reasons.

P.S. 3: For the purposes of this question I am ignoring issues with using the process identifier, like the ones linked here
Answered by Systems Engineer in 675085022

I would like to ask whether there is a better option, or a recommended way to secure your XPC against unwanted connections from within a sandbox?

I think what you are trying to do (checking the signature of the incoming connection) does sound reasonable. I do have reservations about deploying an app that relies on the Container app and the Network System Extension communicating back and forth. The reason for this is because both processes run under a different user and so the lifetime of each process is vastly different. However, it does look like you are already aware of this judging by P.S.2.

If I disable the sandbox, or add a temporary exception entitlement to allow read access to the Xcode/DerivedData folder then I can retrieve the code object, so I conclude that indeed the sandbox blocking the read of the file is the problem.

This does provide some evidence on what may be the issue here. As you pointed out by the deny(1) file-read-data error, this does seem to indicate you are running into Sandbox issue. To try and resolve this I would try to do the following:

1) Make sure that you are using the Sandbox without a temporary entitlement for a path outside of the container: temporary-exception.files.absolute-path.read-only.

2) Try building your Network System Extension and deploying it to common place such as /Applications when you are testing. This means building with either a Development Signing Identity or a Developer ID Signing Identity.

3) If you are having issues opening the XPC connection to the Mach Service make sure you have enabled the mach-lookup.global-name entitlement on the side opening the connection to the listener. Basically, this is a sanity check to ensure that you can at least stand up the connection without the Sandbox getting in the way.

Code Block
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>com.the.marchservice.you.are.connectingto/string>
</array>



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
Accepted Answer

I would like to ask whether there is a better option, or a recommended way to secure your XPC against unwanted connections from within a sandbox?

I think what you are trying to do (checking the signature of the incoming connection) does sound reasonable. I do have reservations about deploying an app that relies on the Container app and the Network System Extension communicating back and forth. The reason for this is because both processes run under a different user and so the lifetime of each process is vastly different. However, it does look like you are already aware of this judging by P.S.2.

If I disable the sandbox, or add a temporary exception entitlement to allow read access to the Xcode/DerivedData folder then I can retrieve the code object, so I conclude that indeed the sandbox blocking the read of the file is the problem.

This does provide some evidence on what may be the issue here. As you pointed out by the deny(1) file-read-data error, this does seem to indicate you are running into Sandbox issue. To try and resolve this I would try to do the following:

1) Make sure that you are using the Sandbox without a temporary entitlement for a path outside of the container: temporary-exception.files.absolute-path.read-only.

2) Try building your Network System Extension and deploying it to common place such as /Applications when you are testing. This means building with either a Development Signing Identity or a Developer ID Signing Identity.

3) If you are having issues opening the XPC connection to the Mach Service make sure you have enabled the mach-lookup.global-name entitlement on the side opening the connection to the listener. Basically, this is a sanity check to ensure that you can at least stand up the connection without the Sandbox getting in the way.

Code Block
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>com.the.marchservice.you.are.connectingto/string>
</array>



Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com

The obvious fix to me seems to be to add a
com.apple.security.temporary-exception.files.absolute-path.read-only
entitlement for /Applications/<appname>.app/, but this a) might be
frowned upon in the app review and b) seems somewhat brittle.

With regards point a), you don’t actually need this temporary exception because the sandbox doesn’t restrict access to /Applications.

With regards point b), that’s valid but, given that your distribution target is the Mac App Store I don’t think it’s a serious concern in practice. The App Store infrastructure on macOS installs the app in /Applications and it’s very unlikely that the user is going to move it. And, if they do, you can detect that and ask them to move it back.

So, yeah, it’s a pain during development but so that’s just the way things are.

Since making sure that your inter process communication is only open
to a few trusted apps seems like a fairly standard precaution

Absolutely! As you’ve seen in the thread you referenced we already have bugs on file requesting a better solution for this. I don’t have any concrete info to share about that, but my standard advice applies: Recheck your assumptions every time we seed a new release of the OS (although in this case that means a new major OS release, because a feature like this is unlikely to ship in a software update).

Share and Enjoy

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

Thank you for your quick and helpful responses, as always!
I tried the steps you outlined, and indeed the sandbox no longer gets in the way once I run from /Applications!

Some further info, for the benefit of future readers:
  • It is not enough to drag the app over from the products folder inside xcode (which will only create a link), but you have to actually move or copy the .app file.

  • At least on Catalina it is still necessary to include a com.apple.security.temporary-exception.files.absolute-path.read-write entitlement for /private/var/db/mds/, or else even SecCodeCopySelf fails, which I believe is because of the bug linked above.

I do have reservations about deploying an app that relies on the Container app and
the Network System Extension communicating back and forth.

I’ll provide some further information that would hopefully alleviate your concerns:
There are basically two kinds of messages we send:
  1. User-provided configuration information that the container app sends configuration information to the system extension, mostly about how to talk to our remote backend. This will only happen rarely, potentially only once when the first user starts and “activates” the product for the first time. This is what should ideally be protected from random apps messing with the configuration.

  2. Status information sent from the system extension to the container app 

The latter is
2a) about whether dns traffic is currently being intercepted, as this depends not only on the DNSProxy being active, but also on the system extension having a connection to our remote backend, and
2b) an event indicating that a flow has just been blocked. We use this to spawn a notification to inform the user. The system extension can’t spawn these notifications itself since, as you say, it is not running as the user.

In particular, we are not sending back and forth data about every individual flow, nor are we making decisions about whether to allow flows inside the container app. If no container app is connected via XPC, the system extension will still continue to work properly and do its job, it will simply be less visible to the user.
I think this is very similar to the SimpleFirewall example from WWDC linked above.

I tried the steps you outlined, and indeed the sandbox no longer gets in the way once I run from /Applications!

Great news!

Regarding:

In particular, we are not sending back and forth data about every individual flow, nor are we making decisions about whether to allow flows inside the container app. If no container app is connected via XPC, the system extension will still continue to work properly and do its job, it will simply be less visible to the user.

Okay. Yeah, my general concern with these workflows is that System Extension would have to rely in any sort of way on the container app, but that does not seem like the case here so that is good.


Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
XPC restricted to processes with the same code signing, but sandboxed
 
 
Q