0

I'm trying to understand the low-level mechanism of IPC using Mach messages between a launch daemon (running as root) and another process, running in a user-content.

Assuming the following data structs:

struct MACH_MSG_BASE
{
    mach_msg_header_t hdr;
    mach_msg_body_t body;
};


struct MACH_MSG_UINT32
{
    MACH_MSG_BASE base;
    
    unsigned int val;      //Sending this value as a test
};

So I'm running the following code in the daemon:

//Server:

//No error checks for brevity
mach_port_t port = MACH_PORT_NULL;
mach_port_t task = mach_task_self();

mach_port_allocate(task,
                   MACH_PORT_RIGHT_RECEIVE,
                   &port);

mach_port_insert_right(task,
                       port,
                       port,
                       MACH_MSG_TYPE_MAKE_SEND);

MACH_MSG_UINT32 msg = {};
msg.base.hdr.msgh_local_port = port;
msg.base.hdr.msgh_size = sizeof(msg.base);
        
mach_msg(&msg.base.hdr,
         MACH_RCV_MSG,
         0,
         sizeof(msg),
         port,
         MACH_MSG_TIMEOUT_NONE,
         MACH_PORT_NULL);

When I run the code above, it goes into a waiting mode at the mach_msg call, as I would expect.

But then, the first issue - how do you get the port number of the daemon from another process? I'm assuming using task_for_pid, as such:

//Client(s):

//No error checks for brevity
mach_port_t port = MACH_PORT_NULL;
mach_port_t task;
task_for_pid(mach_task_self(), server_pid, &task);  //I guess we get server_pid by daemon process name?

mach_port_allocate(task,
                   MACH_PORT_RIGHT_RECEIVE,
                   &port);

MACH_MSG_UINT32 msg = {};
    
msg.base.hdr.msgh_remote_port = _port;
msg.base.hdr.msgh_local_port = MACH_PORT_NULL;
msg.base.hdr.msgh_bits = MACH_MSGH_BITS_REMOTE(MACH_MSG_TYPE_MAKE_SEND);
msg.base.hdr.msgh_size = sizeof(msg.base);
    
msg.val = 0x12345678;
    
mach_msg(&msg.base.hdr,
         MACH_SEND_MSG,
         sizeof(msg),
         0,
         MACH_PORT_NULL,
         MACH_MSG_TIMEOUT_NONE,
         MACH_PORT_NULL);

But when I run the code above, the mach_msg returns 0x10000003 or MACH_SEND_INVALID_DEST.

What am I doing wrong?

c00000fd
  • 20,994
  • 29
  • 177
  • 400

1 Answers1

2

task_for_pid almost never works these days, Apple has significantly restricted it for security reasons.

The way to find a Mach server is for it to register a port name, and for clients to look up the port in either the privileged or user's namespace. For a launch daemon or agent, you specify the ports it offers in the MachServices section of the launchd plist:

<key>MachServices</key>
<dict>
    <key>com.example.mydaemon.MyMachService1</key>
    <true/>
    <key>com.example.mydaemon.MyMachService2</key>
    <true/>
</dict>

The client would then look up these ports to obtain a send right using the bootstrap API:

kern_return_t kr = bootstrap_look_up(bootstrap_port, "com.example.mydaemon.MyMachService1", &service_port);

All of that said, working with low level Mach ports is extremely cumbersome. I strongly recommend using XPC instead unless you have some legacy or system level Mach service you need to interface with. With XPC you still register the MachServices in the launchd plist, the code side is a lot easier to work with though. As you're using C++, your starting point would be the xpc_connection_create_mach_service() function.

pmdj
  • 22,018
  • 3
  • 52
  • 103
  • Thanks. I need to try that. Unfortunately there are so few C++ examples on how to use XPC just for the IPC between a launch daemon and a launch agent. The same is unfortunately true for the Mach messages. So if you can link any I would appreciate it. – c00000fd Jan 23 '23 at 11:25
  • I'm also curious about the launchd plist - why did you use two entries: com.example.mydaemon.MyMachService1 and com.example.mydaemon.MyMachService2? – c00000fd Jan 23 '23 at 11:26
  • Yeah, this is why I gave you the clue about `xpc_connection_create_mach_service`, I expect it might be easier to find examples using that as a keyword. I provided two names of Mach services to give you an example of how you use more than one. You can implement any number including 1, 2 was an arbitrary choice. – pmdj Jan 23 '23 at 12:38
  • Note that offering an XPC service on a privileged daemon opens up a potential security vulnerability; by default, ANYONE is allowed to connect and send your daemon messages, so if that could cause trouble, the way to guard against it is usually to enforce a code signing entitlement check: verify if the incoming connection is coming from a process with an executable signed by you. (Which presumably itself has some kind of hardened interface to stop the user performing destructive operations without an admin password for example.) – pmdj Jan 23 '23 at 12:42
  • Yes, it would work ... Unless someone injects their (malicious) code into my signed user process. (But that's another question. At this point I need to make the basic IPC work.) – c00000fd Jan 23 '23 at 13:06
  • Question: from the server side, how do I associate my port with, say, `com.example.mydaemon.MyMachService1`? – c00000fd Jan 23 '23 at 13:07
  • Code injection is prevented by using the so-called "hardened runtime" which once again is something you can check is active. Both sides of the connection use [`xpc_connection_create_mach_service`](https://developer.apple.com/documentation/xpc/1448783-xpc_connection_create_mach_servi?language=objc), the listener (server) needs to set the `XPC_CONNECTION_MACH_SERVICE_LISTENER` flag and set up an event handler for accepting incoming connections. The (reverse DNS) mach port name is specified in the `name` parameter, again on both sides, and on the listener side must match one of those in the plist – pmdj Jan 23 '23 at 14:25