Subject: kern/32842: SCM_RIGHTS can leak file descriptor resources
To: None <kern-bug-people@netbsd.org, gnats-admin@netbsd.org,>
From: Christian Biere <christianbiere@gmx.de>
List: netbsd-bugs
Date: 02/15/2006 08:20:00
>Number:         32842
>Category:       kern
>Synopsis:       SCM_RIGHTS can leak file descriptor resources
>Confidential:   no
>Severity:       serious
>Priority:       medium
>Responsible:    kern-bug-people
>State:          open
>Class:          sw-bug
>Submitter-Id:   net
>Arrival-Date:   Wed Feb 15 08:20:00 +0000 2006
>Originator:     Christian Biere
>Release:        NetBSD 3.99.15
>Environment:
System: NetBSD cyclonus 3.99.15 NetBSD 3.99.15 (STARSCREAM) #1: Sun Jan 22 18:59:43 CET 2006 bin@cyclonus:/o/NetBSD/obj/sys/arch/i386/compile/STARSCREAM i386
Architecture: i386
Machine: i386
>Description:
When passing a file descriptor of a socket using SCM_RIGHTS over a
unix domain socket (i.e., AF_LOCAL, SOCK_DGRAM) to a non-existing
socket sendmsg() fails with errno = ENOENT. Even though the sent file
descriptor is unconditionally closed after sendmsg(), the associated
socket is never released, not even after terminating the sending
process and removing its unix domain socket. If this happens
repeatedly, the system limit for file descriptors gets hit. If the
system limit is sufficiently large this may also consume all available
UDP/TCP port numbers.  The non-released socket is not accounted
against the process limit.

>How-To-Repeat:

$ cat > grab.c <<EOF && cc -W -Wall -Wformat=2 -o grab grab.c && ./grab
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <getopt.h>
#include <fcntl.h>
#include <assert.h>

#define ARRAY_LEN(x) (sizeof (x)[0] / sizeof (x))

static int
set_socket_address(struct sockaddr_un *sun, const char *path)
{
  static const struct sockaddr_un zero_sun;

  assert(sun);
  assert(path);
  
  *sun = zero_sun;
  if (strlen(path) >= sizeof sun->sun_path) {
    fprintf(stderr, "sockpath is too long\n");
    return -1;
  }
  strncpy(sun->sun_path, path, sizeof sun->sun_path);
  sun->sun_len = SUN_LEN(sun);
  return 0;
}

static int
clear_old_socket(const char *path)
{
  struct stat sb;

  if (0 == lstat(path, &sb)) {
    if (S_IFSOCK != (sb.st_mode & S_IFMT)) {
      fprintf(stderr, "existing \"%s\" is not a socket\n", path);
      return -1;
    }
    if (getuid() != sb.st_uid) {
      fprintf(stderr, "existing \"%s\" has different owner\n", path);
      return -1;
    }
    if (0 != unlink(path)) {
      perror("unlink()");
      /* Ignore */
    }
  } else if (ENOENT != errno)  {
    perror("lstat()");
    return -1;
  }

  return 0;
}

static int
create_new_socket(const char *path)
{
  int fd;

  fd = socket(PF_LOCAL, SOCK_DGRAM, 0);
  if (-1 == fd) {
    perror("socket(PF_LOCAL, SOCK_DGRAM, 0)");
    return -1;
  }

  {
    int enable = 1;
    
    if (0 != setsockopt(fd, 0, LOCAL_CREDS, &enable, sizeof enable)) {
      perror("setsockopt(fd, 0, LOCAL_CREDS, enable, sizeof enable)");
      return -1;
    }
  }

  {
    struct sockaddr_un sun;

    if (0 != set_socket_address(&sun, path))
      return -1;

    if (0 != bind(fd, (void *) &sun, sizeof sun)) {
      perror("bind()");
      return -1;
    }
  }

  return fd;
}

static int
send_msg(const int fd, const char * const dst_path,
  const struct msghdr * const msg_ptr)
{
  struct msghdr msg;
  struct sockaddr_un sun;

  assert(-1 != fd);
  assert(dst_path);
  assert(msg_ptr);
  
  if (set_socket_address(&sun, dst_path))
    return -1;

  msg = *msg_ptr;
  msg.msg_name = &sun;
  msg.msg_namelen = sizeof sun;
  
  if ((ssize_t) -1 == sendmsg(fd, &msg, 0)) {
    perror("sendmsg()");
    return -1;
  }

  return 0;
}

static int
send_descriptors(const int fd, const char * const dst_path,
  const int * const fd_array, const size_t num_fds)
{
  static const struct cmsghdr zero_cmsg;
  static const struct msghdr zero_msg;
  static struct iovec iov[1];
  struct msghdr msg;
  struct cmsghdr *cmsg;
  size_t data_size;

  assert(-1 != fd);
  assert(dst_path);
  assert(fd_array);

  data_size = num_fds * sizeof fd_array[0];

  cmsg = malloc(CMSG_SPACE(data_size));
  if (!cmsg) {
    perror("malloc()");
    return -1;
  }

  *cmsg = zero_cmsg;
  cmsg->cmsg_len = CMSG_LEN(data_size);
  cmsg->cmsg_level = SOL_SOCKET;
  cmsg->cmsg_type = SCM_RIGHTS;

  memcpy((char *) cmsg + CMSG_LEN(0), fd_array, data_size);

  msg = zero_msg;
  msg.msg_iov = iov;
  msg.msg_iovlen = ARRAY_LEN(iov);
  msg.msg_control = cmsg;
  msg.msg_controllen = CMSG_LEN(data_size);

  return send_msg(fd, dst_path, &msg);
}

static int
send_socket(const int master_fd, const char * const dst_path, in_port_t port)
{
  static const struct sockaddr_in zero_sin;
  struct sockaddr_in sin;
  int fd;

  sin = zero_sin;
  sin.sin_family = AF_INET;
  sin.sin_addr.s_addr = htonl(INADDR_ANY);
  sin.sin_port = htons(port);

  fd = socket(AF_INET, SOCK_DGRAM, 0);
  if (-1 == fd) {
    perror("socket(AF_INET, SOCK_DGRAM, 0)");
    return -1;
  }

  if (0 != bind(fd, (void *) &sin, sizeof sin)) {
    perror("handle_request: bind()");
    close(fd);
    return -1;
  }

  send_descriptors(master_fd, dst_path, &fd, 1);
  close(fd);

  return 0;
}

static int
run_server(const char *path)
{
  int fd;
  unsigned port;

  if (0 != clear_old_socket(path))
    return -1;
  
  fd = create_new_socket(path);
  if (-1 == fd)
    return -1;
 
  for (port = 1024; port < 65536; port++)
    send_socket(fd, "no such socket", port);

  if (0 != clear_old_socket(path))
    return -1;

  return 0;
}

static const char server_path[] = "/tmp/server";

int
main(void)
{
  if (0 != run_server(server_path))
    exit(EXIT_FAILURE);

  return 0;
}

/* vi: set ai et ts=2 sts=2 sw=2 cindent: */
EOF

>Fix: