2013-08-13

How to send Unix file descriptors between processes?

This blog post explains how to send Unix file descriptors between processes. The example shown below sends from parent to child, but the general idea works the other way round as well, and it works even between unrelated processes.

As a dependency, the processes need to have the opposite ends of a Unix domain socket already. Once they have it, they can use sendfd to send any file descriptor over the existing Unix domain socket. Do it like this in Python:

#! /usr/bin/python

"""sendfd_demo.py: How to send Unix file descriptors between processes

for Python 2.x at Tue Aug 13 16:07:29 CEST 2013
"""

import _multiprocessing
import os
import socket
import sys
import traceback

def run_in_child(f, *args):
  pid = os.fork()
  if not pid:  # Child.
    try:
      child(*args)
    except:
      traceback.print_exc()
      os._exit(1)
    finally:
      os._exit(0)
  return pid  


def child(sb):
  assert sb.recv(1) == 'X'
  # Be careful: there is no EOF handling in
  # _multiprocessing.recvfd (because it's buggy). On EOF, it
  # returns an arbitrary file descriptor number. We work it around by doing
  # a regular sendall--recv pair first, so if there is an obvious reason for
  # failure, they will fail properly.
  f = os.fdopen(_multiprocessing.recvfd(sb.fileno()))
  print repr(f.read())
  print f.tell()  # The file size.


def main(argv):
  sa, sb = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
  pid = run_in_child(child, sb)
  # We open f after creating the child process, so it could not inherit f.
  f = open('/etc/hosts')
  sa.sendall('X')
  _multiprocessing.sendfd(sa.fileno(), f.fileno())  # Send f to child.
  assert (pid, 0) == os.waitpid(pid, 0)
  # The file descriptor is shared between us and the child, so we get the file
  # position where the child has left off (i.e. at the end).
  print f.tell()
  print 'Parent done.'


if __name__ == '__main__':
  sys.exit(main(sys.argv))

It works even for unrelated processes. To try it, run the following program with a command-line argument in a terminal window (it will start the server), and in another terminal window run it several times without arguments (it will run the client). Each time the client is run, it connects to the server, and sends a file descriptor to it, which the server reads. The program:

#! /usr/bin/python2.7

"""sendfd_unrelated.py: Send Unix file descriptors between unrelated processes.

for Python 2.x at Tue Aug 13 16:26:27 CEST 2013
"""

import _multiprocessing
import errno
import os
import socket
import stat
import sys
import time

SOCKET_NAME = '/tmp/sendfd_socket'

def server():
  print 'Server.'
  ssock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  while 1:
    try:
      ssock.bind(SOCKET_NAME)
      break
    except socket.error, e:
      if e[0] != errno.EADDRINUSE:
        raise
      # TODO: Fail if the old server is still running.
      print 'Removing old socket.'
      os.unlink(SOCKET_NAME)
    
  ssock.listen(16)
  while 1:
    print 'Accepting.'
    sock, addr = ssock.accept()
    print 'Accepted.'
    assert sock.recv(1) == 'X'
    f = os.fdopen(_multiprocessing.recvfd(sock.fileno()))
    print repr(f.read())
    print f.tell()  # The file size.
    del sock, f


def client():
  print 'Client.'
  sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
  while 1:
    try:
      sock.connect(SOCKET_NAME)
      break
    except socket.error, e:
      if e[0] not in (errno.ENOENT, errno.ECONNREFUSED):
        raise
      print 'Server not listening, trying again.'
      time.sleep(1)
  print 'Connected.'
  f = open('/etc/hosts')
  sock.sendall('X')
  _multiprocessing.sendfd(sock.fileno(), f.fileno())  # Send f to server.
  assert '' == sock.recv(1)  # Wait for the server to close the connection.
  del sock
  print f.tell()


def main(argv):
  if len(argv) > 1:
    server()
  else:
    client()
  print 'Done.'


if __name__ == '__main__':
  sys.exit(main(sys.argv))

There is no system call named sendfd or recvfd. They are implemented in terms of the sendmsg and recvmsg system calls. Passing the correct arguments is tricky and a bit awkward. Here is how to do it in C or C++:

#include <string.h>
#include <sys/socket.h>

int sendfd(int unix_domain_socket_fd, int fd) {
  char dummy_char = 0;
  char buf[CMSG_SPACE(sizeof(int))];
  struct msghdr msg;
  struct iovec dummy_iov;
  struct cmsghdr *cmsg;
  memset(&msg, 0, sizeof msg);
  dummy_iov.iov_base = &dummy_char;
  dummy_iov.iov_len = 1;
  msg.msg_control = buf;
  msg.msg_controllen = sizeof(buf);
  msg.msg_iov = &dummy_iov;
  msg.msg_iovlen = 1;
  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;
  cmsg->cmsg_len = CMSG_LEN(sizeof(int));
  msg.msg_controllen = cmsg->cmsg_len;
  memcpy(CMSG_DATA(cmsg), &fd, sizeof fd);  /* int. */
  return sendmsg(unix_domain_socket_fd, &msg, 0);  /* I/O error unless 1. */
}

int recvfd(int unix_domain_socket_fd) {
  int res;
  char dummy_char;
  char buf[CMSG_SPACE(sizeof(int))];
  struct msghdr msg;
  struct iovec dummy_iov;
  struct cmsghdr *cmsg;
  memset(&msg, 0, sizeof msg);
  dummy_iov.iov_base = &dummy_char;
  dummy_iov.iov_len = 1;
  msg.msg_control = buf;
  msg.msg_controllen = sizeof(buf);
  msg.msg_iov = &dummy_iov;
  msg.msg_iovlen = 1;
  cmsg = CMSG_FIRSTHDR(&msg);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;
  cmsg->cmsg_len = CMSG_LEN(sizeof(int));
  msg.msg_controllen = cmsg->cmsg_len;
  res = recvmsg(unix_domain_socket_fd, &msg, 0);
  if (res == 0) return -2;  /* EOF. */
  if (res < 0) return -1;  /* I/O error. */
  memcpy(&res, CMSG_DATA(cmsg), sizeof res);  /* int. */
  return res;  /* OK, new file descriptor is returned. */
}

See also http://www.mca-ltd.com/resources/fdcred_1.README (download C source of Python extension here) for more information. Perl programmers can use Socket::MsgHdr.

No comments: