3

I'm working on a large-ish Mac app, which is split across many components in many different processes. One of those components is a Safari Extension Companion[1], which is is a kind of "App Extension" that allows a JavaScript -based extension to talk to native code (this is required for secure IPC between our components). The IPC channel is a Unix domain socket in the user's ~/Library/Application Support/<APPNAME>/ directory. For legacy reasons, the Unix socket server speaks HTTP and the client library for it uses a custom URLProtocol, in case that matters.

This works fine for all of our other components, but none of them are running in a sandbox[2]. However, the Xcode template for the extension companion created a sandbox, and while the companion loads if sandboxed, it seems to require the sandbox. I tried setting com.apple.security.app-sandbox to false in the entitlements file, or removing the entitlements file (and all references to it), both appear to prevent Safari from loading the companion at all (it still runs as its own binary if directly invoked, but Safari apparently won't touch it).

I've added exceptions to the sandbox to allow it to access the relevant directory, and just the IPC socket directly; but that doesn't seem to be sufficient. The error I get when trying to open the socket is EPERM (Operation Not Permitted), which doesn't really explain the problem. After adding the exceptions to the entitlements and fixing up the paths, things like the log file (also under the "real" ~/Library/) now get written to as expected.

This error happens on the connect call for the socket (it returns -1, indicating an error, and errno is 1 (EPERM)) so there's never actually an attempt to write to or read from the socket. This seems very odd to me, since - according to the Apple Sandbox Design Guide -

UNIX domain sockets are straightforward; they work just like any other file

with regard to sandbox exceptions.

The entitlements file (modified from the auto-generated template using the answer here:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-only</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.network.server</key>
    <true/>
    <key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
    <array>
        <string>/Library/Application Support/{APPNAME}/</string>
        <string>/Library/Application Support/{APPNAME}/ipc_socket</string>
        <string>/Library/Logs/{APPNAME}/</string>
    </array>
</dict>
</plist>

Is there a way to make a Safari Extension Companion work without sandboxing it at all? If not, how do I allow the companion to use the IPC socket anyhow?

Alternatively, is there a way to use UNIX sockets in extension companions (if the problem isn't the sandbox per se)?


*EDIT: This question has been edited as a prior problem - ENOENT (File Not Found) when calling connect - was resolved before it got any comments or answers. It was due to the sockaddr_un being too short for the full domain socket path, and was resolved by using the real path to the socket, rather than the (very long) path through the sandbox's version of Library/Application Support and the symlink planted there. However, the new problem appears to be in the same place and likely also due to sandboxing.

EDIT 2: Updated the entitlements to include network client (and server, for good measure). Although the Unix socket is not a network socket, it is a network API that is failing, so I thought it might help. Made no difference, though; still get EPERM on calling connect.


[1] Using an Extension Companion rather than a native-code Safari App Extension because the background/global logic is fairly complex but currently basically identical across all major browsers; re-writing it in native code would be a major one-time task and also an ongoing maintenance burden. I also suspect it wouldn't help with the problem I'm having.

[2] The two main ones cannot be sandboxed, as they require access to the entire file system. The app is not distributed through the App Store, so the lack of sandboxing doesn't present compliance problems. I'd like to enable the sandboxing, for security, but it isn't practical.

CBHacking
  • 1,984
  • 16
  • 20

1 Answers1

9

TL;DR: Unix sockets must be in the sandbox container or a group container!

Found the solution. Turns our Apple's documentation is wrong: Unix domain sockets cannot be accessed through com.apple.security.temporary-exception.files.home-relative-path.read-write entitlements, even though normal files can. That's what was causing the EPERM error.

The socket must be in either the sandbox's container, or in a group container that the sandbox has permission to access. Given the length limit on sockaddr_un.sun_path and the fact that our (long) bundle name is included in the sandbox-specific container path, I decided to create a group container. This will also make it easier to sandbox other components in the future. The final version of the entitlements file looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.application-groups</key>
    <array>
        <string>{REVERSED.COMPANY.DOMAIN}</string>
    </array>
    <key>com.apple.security.temporary-exception.files.home-relative-path.read-write</key>
    <array>
        <string>/Library/Application Support/{APPNAME}/</string>
        <string>/Library/Logs/{APPNAME}/</string>
    </array>
</dict>
</plist>

The socket is placed in the group container (~/Library/Group Containers/{REVERSED.COMPANY.DOMAIN}/ipc_socket). The temporary entitlements are still in there to access stuff like the logs and config directories. The network entitlements and user-selected files entitlement weren't needed, and were removed.

Community
  • 1
  • 1
CBHacking
  • 1,984
  • 16
  • 20
  • Thanks @CBHacking! You said _Given the length limit on sockaddr_un.sun_path and the fact that our (long) bundle name is included_ I'm curious is user home directory included in that length? It sounds like a potential problem if user has pretty long home directory name it won't fill allowed 104 bytes unix socket path. – Dmitry Kharitonov Dec 22 '19 at 19:26
  • Just did experiment, and yes, home directory name counts. Pretty dangerous :) – Dmitry Kharitonov Dec 22 '19 at 19:38
  • If the path is too long (because, for example, the profile name is too long), the only result *should* be that the app fails to work because it can't open the socket. This requires either validating the path length before writing to `sockaddr_un.sun_path` or using a bounded write function and checking the return value, but... you should be doing that anyhow. If the socket's full path won't fit, you just... give the user an error message, I guess. – CBHacking Dec 23 '19 at 07:14