fun with SCM_RIGHTS
One feature of unix domain sockets I always wanted to try is the ability to pass file descriptors to another process. The Linux unix(7) man page makes this sound like a simple procedure but as always the devil is in the details.
In the application I have in mind I want to use this feature to redirect the standard input, standard output and standard error of one process into a virtual terminal of screen. The redirected process will later be init and has to have PID 1. Therefore I can't simply start it in screen itself. The way I imagine this is starting screen in the background with a terminal_server in one virtual terminal. My init process then connects to this server receives the file descriptors for standard input, standard output and standard output and redirects its own input, output and error there. It then exec()s the real init. This will at one point make it possible to interact with the boot process from an SSH connection on servers that have no serial console.
To make the following examples more readable I leave out the error handling. I also use C99 to declare variables where I need them to make the examples more readable. Both client and server need the following includes:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h>
terminal_server.c
First the server opens a new unix domain socket(2), assigns a name to it using bind(2) and listen(2)s on it:
int server_socket = socket(PF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un server_address = { AF_UNIX, "\0init_console" };
bind(server_socket, (struct sockaddr *) &server_address, sizeof server_address);
listen(server_socket, 1);
As you can see the path name starts with a zero byte. This denotes an abstract domain socket that is independent of the file system.
Next the server waits for the client to connect:
struct sockaddr_un client_address;
socklen_t client_address_length = sizeof client_address;
int client_connection = accept(server_socket,
(struct sockaddr *) &client_address,
&client_address_length);
close(server_socket);
The accept(2) call blocks until the client connects. When the client is connected the server socket can be closed.
Now that there is a connection in the server to the client, the message containing the file descriptors needs to be prepared. This message will then be send with the sendmsg(2) system call:
sendmsg(client_connection, &message, 0);
Preparing the message is the most difficult part of the whole process. First we need the file descriptors in an array and a buffer:
int file_descriptors[3] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO };
char buffer[CMSG_SPACE(sizeof file_descriptors)];
This buffer needs to be stored in in a new msghdr structure:
struct msghdr message = {
.msg_control = buffer,
.msg_controllen = sizeof buffer,
};
Then the data can be set describing the operation we want to perform:
struct cmsghdr *cmessage = CMSG_FIRSTHDR(&message); cmessage->cmsg_level = SOL_SOCKET; cmessage->cmsg_type = SCM_RIGHTS; cmessage->cmsg_len = CMSG_LEN(sizeof file_descriptors); message.msg_controllen = cmessage->cmsg_len;
Copy the file descriptor data into the buffer:
memcpy(CMSG_DATA(cmessage), file_descriptors, sizeof file_descriptors);
After performing this preparations the sendmsg(2) should work as expected. Unfortunately it doesn't. We only sent so-called ancillary data and actual data and this doesn't seem to work on Linux.
Therefore we add one byte of phony data:
char ping = 23;
struct iovec ping_vec = {
.iov_base = &ping,
.iov_len = sizeof ping,
};
message.msg_iov = &ping_vec,
message.msg_iovlen = 1,
Would've also been possible to send the number of file descriptors we want to transfer but this value is a constant here.
After the message has been successfully sent the server has to do nothing else to do. It can close the client connection and its standard input, output and error file descriptors:
close(client_connection); close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO);
To prevent the screen terminal from closing the server needs to enter a sleeping state. This can be done with the pause(2) system call:
pause();
terminal_client.c
The client is much easier than the server. The first step is to connect to the server:
struct sockaddr_un server_address = { AF_UNIX, "\0init_console" };
int client_socket = socket(PF_UNIX, SOCK_STREAM, 0);
connect(client_socket, (struct sockaddr *) &server_address, sizeof server_address);
Then the buffer for the message that will be received needs to be set up. We already know that we need to receive one phony byte so we can express this more concise:
int file_descriptors[3];
char buffer[CMSG_SPACE(sizeof file_descriptors)];
char ping;
struct iovec ping_vec = {
.iov_base = &ping,
.iov_len = sizeof ping,
};
struct msghdr message = {
.msg_control = buffer,
.msg_controllen = sizeof buffer,
.msg_iov = &ping_vec,
.msg_iovlen = 1,
};
Then we can wait for the message:
recvmsg(client_socket, &message, 0) < 0);
recvmsg(2) blocks until a message with the specified length is received. Without the phony data byte this call won't return even if sendmsg(2) at the server side succeeds.
The connection is no longer needed and can be closed:
close(client_socket);
At this point the client already has received the file descriptors. To find out their numbers the message needs to be parsed. Next the message needs to be parsed:
struct cmsghdr *cmessage = CMSG_FIRSTHDR(&message); memcpy(file_descriptors, CMSG_DATA(cmessage), sizeof file_descriptors);
Now the file descriptors can be redirected:
dup2(file_descriptors[0], STDIN_FILENO); close(file_descriptors[0]); dup2(file_descriptors[1], STDOUT_FILENO); close(file_descriptors[1]); dup2(file_descriptors[2], STDERR_FILENO); close(file_descriptors[2]);
The dup2(2) system call redirects one file descriptor to another. It's very important not to accidentally swap the arguments as this can lead to undesired effects.
Now that everything is set up the client can be replaced by the program that should be run in the terminal of the server. For testing purposes a simple shell is enough:
execl("/bin/sh", "/bin/sh", NULL)
If you made it this far I hope you found this post interesting and informative. If you want me to put both programs up in a form that can be readily compiled drop me a line via XMPP or mail.