next up previous
Next: Using chroot jails Up: New Solutions Previous: The fork approach

Unix socket magic

Here's another technique that helps you split an application into a privileged and a non-privileged part. In the fork example, we used a pipe for communication between the front-end and the backend. One drawback with this is that pipes are one-way only, so you'd need two of them if the back end is to talk to the front-end as well. Another problem with the fork approach in general is that the two processes cannot exchange resources such as open file descriptors easily. Imagine an application where the user selects one out of several serial ports, and wants to open it. You can design the application so that the front-end asks the back-end to open the serial device, but how do you transfer the open file to the front-end process?

This is where Unix domain sockets come in. This family of sockets is for local communication only, i.e. you cannot use them across a network. They do have some properties that make them unique; one of them is what's commonly called file descriptor passing. One process can attach a set of file descriptors to a message it sends through the socket, and if the peer is willing to receive them, these open files are attached to the recipient's file set.

Here's an outline of using a back end for opening serial ports:

int     backend_fd;

void init_backend(void)
{
        char    buffer[1024];
        int     pair[2], fd, n;
        pid_t   pid;

        if (socketpair(PF_UNIX, SOCK_DGRAM, 0, pair) < 0)
                fatal("socketpair failed");
        if ((pid = fork()) < 0)
                fatal("fork failed");
        if (pid != 0) {
                if (setuid(getuid()) < 0)
                        fatal("unable to drop privs");
                backend_fd = pair[0];
                close(pair[1]);
                return;
        }

        /* We're the backend */
        close(pair[0]);
        fd = pair[1];

        while ((n = recv(fd, buffer, sizeof(buffer)-1, 0)) > 0) {
                char            control[sizeof(struct cmsg)+10];
                struct msghdr   msg;
                struct cmsghdr  *cmsg;
                struct iovec    iov;
                int             tty;

                buffer[n] = '\0';
                if (strchr(buffer, '/') != NULL
                 || strncmp(buffer, "ttyS", 4))
                        reply(fd, "bad tty path");
                else
                if ((tty = lock_and_open_port(buffer)) < 0)
                        reply(fd, "tty already locked");
                else {
                        /* Response data */
                        iov.iov_base = "OK";
                        iov.iov_len  = 2;

                        /* compose the message */
                        memset(&msg, 0, sizeof(msg));
                        msg.msg_iov = &iov;
                        msg.msg_iovlen = 1;
                        msg.msg_control = control;
                        msg.msg_controllen = sizeof(control);

                        /* attach open tty fd */
                        cmsg = CMSG_FIRSTHDR(&msg);
                        cmsg->cmsg_level = SOL_SOCKET;
                        cmsg->cmsg_type = SCM_RIGHTS;
                        cmsg->cmsg_len = CMSG_LEN(sizeof(tty));
                        *(int *)CMSG_DATA(cmsg) = tty;

                        msg.msg_controllen = cmsg->cmsg_len;

                        if (sendmsg(fd, &msg, 0) < 0)
                                fatal("sendmsg failed");
                }
        }
        exit(0);
}

int open_tty(const char *name)
{
        char            data[1024], control[1024];
        struct msghdr   msg;
        struct cmsghdr  *cmsg;
        struct iovec    iov;

        if (write(backend_fd, name, strlen(name)) < 0)
                fatal("write failed");

        memset(&msg, 0, sizeof(msg));
        iov.iov_base   = data;
        iov.iov_len    = sizeof(data)-1;
        msg.msg_iov    = &iov;
        msg.msg_iovlen = 1;
        msg.msg_control = control;
        msg.msg_controllen = sizeof(control);

        if (recvmsg(backend_fd, &msg, 0) < 0)
                fatal("recvmsg failed");

        data[iov.iov_len] = '\0';
        if (strcmp(data, "OK")) {
                printf("failed to open %s: %s\n", name, data);
                return -1;
        }

        /* Loop over all control messages */
        cmsg = CMSG_FIRSTHDR(&msg);
        while (cmsg != NULL) {
                if (cmsg->cmsg_level == SOL_SOCKET
                 && cmsg->cmsg_type  == SCM_RIGHTS)
                        return *(int *) CMSG_DATA(cmsg);
                cmsg = CMSG_NXTHDR(&msg, cmsg);
        }

        printf("failed to open %s: bug!\n");
        return -1;
}

int
main(int argc, char **argv)
{
        init_backend();

        /* parse arguments, set up GUI etc  */

        /* now open tty */
        fd = open_tty("ttyS0");
}

The init_backend function, which is called as the very first thing in main(), creates a Unix socket pair and forks a worker process that continues with root privilege, while the main parent process that does all the GUI stuff drops all privilege before returning to main().

When the frontend comes around to opening a serial port, it sends the name of the device to the backend via the Unix socket. The backend performs some sanity checks on the name to make sure this is indeed a serial port and not /etc/shadow it's about to open, and tries to create a lock file. If any of this fails, it simply returns an error message to the front-end. If all this succeeds however, it sends the string OK as response, and sends the file descriptor of the open tty along.

This code, as well as the code that receives the open tty descriptor in open_tty looks quite complicated, and it really is. The reason for this is that the only way to do file descriptor passing is by using sendmsg and recvmsg, and a control message (struct cmsghdr) attached to it. I will not go into greater details here concerning these details; if this is all greek to you (as it was to me when I first tried to understand it), refer to the manual pages for sendmsg and recvmsg, as well as cmsg(3).

There's a second control message Unix sockets offer which is quite interesting in a security context. On Linux and BSD, Unix sockets allow you to obtain the Unix credentials of the process that sent you a particular message. These credentials come along as a control message at level SOL_SOCKET, type SCM_CREDENTIALS. They're passed as a struct containing uid, gid, and pid of the sender process.

This is quite useful for applications that are supposed to serve local users only, and need to know the identity of the user connecting to them.

The most convenient way to use this feature is for the server to set the SO_PASSCRED socket option. It can then retrieve the user credentials every time the client sends a message:

int
svc_make(const char *pathname)
{
        struct sockaddr_un sun;
        int             on = 1;

        /* create server socket */
        fd = socket(PF_UNIX, SOCK_STREAM, 0);

        /* bind it */
        memset(&sun, 0, sizeof(sun));
        sun.sun_family = AF_UNIX;
        strcpy(sun.sun_path, pathname);
        bind(fd, (struct sockaddr *) &sun, sizeof(sun));
        listen(fd, 10);

        /* turn on credentials passing */
        setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));
        return fd;
}

int
svc_recv(int fd, char *buffer, size_t size, struct ucred *cred)
{
        char            control[1024];
        struct msghdr   msg;
        struct cmsghdr  *cmsg;
        struct iovec    iov;
        int             result;

        memset(&msg, 0, sizeof(msg));
        iov.iov_base = buffer;
        iov.iov_len = size;
        msg.msg_iov = &iov;
        msg.msg_iovlen = 1;
        msg.msg_control = control;
        msg.msg_controllen = sizeof(control);

        if (recvmsg(fd, &msg, 0) < 0)
                return -1;

        result = -1;
        cmsg = CMSG_FIRSTHDR(&msg);
        while (cmsg != NULL) {
                if (cmsg->cmsg_level == SOL_SOCKET
                 && cmsg->cmsg_type  == SCM_CREDENTIALS) {
                        memcpy(cred, CMSG_DATA(cmsg), sizeof(*cred));
                        result = iov.iov_len;
                } else
                if (cmsg->cmsg_level == SOL_SOCKET
                 && cmsg->cmsg_type  == SCM_RIGHTS) {
                        dispose_fds((int *) CMSG_DATA(cmsg),
                                (cmsg->cmsg_len - CMSG_LEN(0))/sizeof(int));
                }
                cmsg = CMSG_NXTHDR(&msg, cmsg);
        }

        return result;
}

The svc_make routine creates a Unix stream socket, and sets its SO_PASSCRED option. The svc_recv function receives data from the client, and in addition extracts the sender's credentials. This gives the server the uid and gid of the process that sent the message. This information is reliable (well, it's as trustworthy as the integrity of your kernel).

There are two things to watch out for, however. The first is that the identity of the sender may change from message to message. If you don't want to deal with this kind of multiple personality disorder, you should at least verify that the uid/gid of the client remains the same, and balk if not.

The second issue is that a process receiving any control messages via Unix sockets can also be sent socket descriptors by a client. If you're not prepared to handle file descriptors passed by the client, an attacker can flood your server with open file descriptors until it reaches its resource limit; after that, it will be unable to accept new connections. This is why the sample code above calls the dispose_fds function when receiving file descriptors from the client.11.7


next up previous
Next: Using chroot jails Up: New Solutions Previous: The fork approach
Olaf Kirch 2002-01-16