Socket Permissions & Passing Credentials File Permissions, ucred, SCM_CREDENTIALS

 

Socket Permissions & Passing Credentials
Part 4 of 6 โ€” File Permissions, ucred, SCM_CREDENTIALS
๐Ÿ“‚ File 4 of 6
๐Ÿ” Security
๐Ÿ“– TLPI ยง57.5

Who Can Connect? Access Control on UNIX Sockets

Because UNIX domain sockets appear as files in the file system, their access is controlled by standard file permissions. This gives you a simple but powerful security mechanism: only processes with the right permissions on the socket file can connect or send data.

Beyond file permissions, UNIX domain sockets support credential passing โ€” a peer can send its PID, UID, and GID and the receiver can verify these credentials with kernel-guaranteed accuracy. The kernel fills in or verifies the credentials, so the receiver can trust them completely.

Key Terms

socket permissions umask chmod() struct ucred SCM_CREDENTIALS SO_PEERCRED sendmsg() recvmsg() cmsg ancillary data

Socket File Permissions

When you call bind() on a UNIX domain socket, the kernel creates a socket file. This file has permissions determined by your current umask. The default permissions are typically 0777 & ~umask, often resulting in 0755 or 0777.

What permissions control for sockets:

Write permission
Controls: Can this process send to the socket (connect() for stream, sendto() for datagram)?
Execute permission
On the directory containing the socket file โ€” needed to access the socket path.
Read permission
Not typically needed for sockets themselves (reads happen after connection).
/* Create a socket file accessible only by root */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/stat.h>

#define SOCK_PATH "/tmp/secure_socket"

int create_restricted_socket(void)
{
    int                sfd;
    struct sockaddr_un addr;
    mode_t             old_umask;

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (sfd == -1) {
        perror("socket");
        return -1;
    }

    remove(SOCK_PATH);

    /* Method 1: Set umask to control initial permissions */
    old_umask = umask(0177);  /* Only owner can write (0600 result) */

    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SOCK_PATH, sizeof(addr.sun_path) - 1);

    if (bind(sfd, (struct sockaddr *)&addr, SUN_LEN(&addr)) == -1) {
        perror("bind");
        umask(old_umask);
        return -1;
    }

    umask(old_umask);  /* Restore original umask */

    /* Method 2: Use chmod() after bind() for explicit control */
    /* Allow only the file owner to connect: */
    if (chmod(SOCK_PATH, S_IRUSR | S_IWUSR) == -1) {
        perror("chmod");
        return -1;
    }

    /* Allow owner and group to connect: */
    /* chmod(SOCK_PATH, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); */

    printf("Socket created at %s with restricted permissions\n", SOCK_PATH);
    return sfd;
}
โš ๏ธ /tmp directory risk: If you place a socket in /tmp, any user can create a file with the same name before your server starts (a symlink attack). For security, place sockets in a directory that only the server’s user can write to.

Getting Peer Credentials: SO_PEERCRED

After accept() on a stream socket (or on a connected datagram socket), you can call getsockopt() with SO_PEERCRED to get the PID, UID, and GID of the connected peer. The kernel fills these in โ€” they cannot be forged.

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/un.h>

/* struct ucred โ€” the credentials structure */
/* Defined in <sys/socket.h> on Linux */
struct ucred {
    pid_t pid;   /* PID of the sending process */
    uid_t uid;   /* Effective UID of the sending process */
    gid_t gid;   /* Effective GID of the sending process */
};

/* Usage: after accept() returns cfd */
int get_peer_credentials(int cfd)
{
    struct ucred cred;
    socklen_t    len = sizeof(cred);

    /* getsockopt with SO_PEERCRED fills in cred */
    if (getsockopt(cfd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == -1) {
        perror("getsockopt(SO_PEERCRED)");
        return -1;
    }

    printf("Peer PID: %ld\n", (long)cred.pid);
    printf("Peer UID: %ld\n", (long)cred.uid);
    printf("Peer GID: %ld\n", (long)cred.gid);

    /* Now you can decide: should this peer be allowed? */
    if (cred.uid != 0 && cred.uid != getuid()) {
        printf("Unauthorized peer! Closing connection.\n");
        return -1;  /* Reject this connection */
    }

    return 0;  /* Authorized */
}

Real-world use: The dbus-daemon uses SO_PEERCRED to verify which process is connecting, enabling per-process access control policies. systemd also uses it to identify which service is communicating.

Sending Credentials as Ancillary Data: SCM_CREDENTIALS

For datagram sockets (which have no connection), SO_PEERCRED is not available. Instead, the sender can explicitly attach credential information to a message using ancillary data (also called control messages) with sendmsg(). The receiver uses recvmsg() to retrieve it.

The Linux kernel verifies the credentials: a non-root process can only send its own PID/UID/GID, not someone else’s. Root can send any credentials.

Structure of a message with ancillary data:

struct msghdr
(message header)
โ†’
iov[]
(data buffers)
+
msg_control
(ancillary data)
โ†“
struct cmsghdr { cmsg_len, SOL_SOCKET, SCM_CREDENTIALS } + struct ucred { pid, uid, gid }
/* Sender: attach credentials to a datagram using sendmsg() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SV_PATH  "/tmp/cred_sv"
#define DATA     "Hello with credentials"

int send_with_credentials(int sfd)
{
    struct sockaddr_un sv_addr;
    struct msghdr      msgh;
    struct iovec       iov;
    struct ucred       cred;
    char               cmsg_buf[CMSG_SPACE(sizeof(struct ucred))];
    struct cmsghdr    *cmsgp;

    /* The data payload */
    iov.iov_base = (void *)DATA;
    iov.iov_len  = sizeof(DATA);

    /* Build our credentials */
    cred.pid = getpid();
    cred.uid = getuid();
    cred.gid = getgid();

    /* Build the control message (ancillary data) */
    memset(cmsg_buf, 0, sizeof(cmsg_buf));
    cmsgp = (struct cmsghdr *)cmsg_buf;
    cmsgp->cmsg_len   = CMSG_LEN(sizeof(struct ucred));
    cmsgp->cmsg_level = SOL_SOCKET;
    cmsgp->cmsg_type  = SCM_CREDENTIALS;
    memcpy(CMSG_DATA(cmsgp), &cred, sizeof(struct ucred));

    /* Build the server address */
    memset(&sv_addr, 0, sizeof(sv_addr));
    sv_addr.sun_family = AF_UNIX;
    strncpy(sv_addr.sun_path, SV_PATH, sizeof(sv_addr.sun_path) - 1);

    /* Build the message header */
    memset(&msgh, 0, sizeof(msgh));
    msgh.msg_name    = &sv_addr;
    msgh.msg_namelen = SUN_LEN(&sv_addr);
    msgh.msg_iov     = &iov;
    msgh.msg_iovlen  = 1;
    msgh.msg_control = cmsg_buf;
    msgh.msg_controllen = cmsgp->cmsg_len;

    /* Send the message with credentials */
    if (sendmsg(sfd, &msgh, 0) == -1) {
        perror("sendmsg");
        return -1;
    }

    printf("Sent message with PID=%ld UID=%ld GID=%ld\n",
           (long)cred.pid, (long)cred.uid, (long)cred.gid);
    return 0;
}
/* Receiver: extract credentials from received message using recvmsg() */
int recv_with_credentials(int sfd)
{
    struct msghdr  msgh;
    struct iovec   iov;
    struct ucred  *credp;
    struct cmsghdr *cmsgp;
    char           data_buf[256];
    char           cmsg_buf[CMSG_SPACE(sizeof(struct ucred))];
    ssize_t        n;

    iov.iov_base = data_buf;
    iov.iov_len  = sizeof(data_buf) - 1;

    memset(&msgh, 0, sizeof(msgh));
    msgh.msg_iov        = &iov;
    msgh.msg_iovlen     = 1;
    msgh.msg_control    = cmsg_buf;
    msgh.msg_controllen = sizeof(cmsg_buf);

    n = recvmsg(sfd, &msgh, 0);
    if (n == -1) {
        perror("recvmsg");
        return -1;
    }
    data_buf[n] = '\0';
    printf("Received data: %s\n", data_buf);

    /* Walk the control message chain looking for SCM_CREDENTIALS */
    for (cmsgp = CMSG_FIRSTHDR(&msgh);
         cmsgp != NULL;
         cmsgp = CMSG_NXTHDR(&msgh, cmsgp)) {

        if (cmsgp->cmsg_level == SOL_SOCKET &&
            cmsgp->cmsg_type  == SCM_CREDENTIALS) {

            credp = (struct ucred *)CMSG_DATA(cmsgp);
            printf("Sender PID: %ld\n", (long)credp->pid);
            printf("Sender UID: %ld\n", (long)credp->uid);
            printf("Sender GID: %ld\n", (long)credp->gid);
        }
    }
    return 0;
}

Important: To receive SCM_CREDENTIALS, the receiving socket must have the SO_PASSCRED option enabled:

int enable = 1;
setsockopt(sfd, SOL_SOCKET, SO_PASSCRED, &enable, sizeof(enable));

Understanding CMSG Macros

Working with ancillary data requires using the CMSG_* macros to properly navigate the control message buffer. These macros handle alignment issues for you.

Macro Purpose
CMSG_SPACE(len) Total bytes needed for control buffer (includes alignment padding)
CMSG_LEN(len) Value for cmsg_len field (header + data, no trailing pad)
CMSG_DATA(cmsgp) Pointer to the data portion of a control message
CMSG_FIRSTHDR(msgh) Pointer to first control message in the buffer
CMSG_NXTHDR(msgh, cmsgp) Pointer to next control message (for iteration)
/* Always allocate control buffer using CMSG_SPACE, not sizeof: */
char cmsg_buf[CMSG_SPACE(sizeof(struct ucred))];  /* CORRECT */
/* char cmsg_buf[sizeof(struct cmsghdr) + sizeof(struct ucred)]; WRONG - alignment issues */

/* Always set cmsg_len using CMSG_LEN: */
cmsgp->cmsg_len = CMSG_LEN(sizeof(struct ucred));  /* CORRECT */

๐ŸŽฏ Interview Questions โ€” Permissions & Credentials
Q1. How does a UNIX domain socket server restrict which clients can connect?

Using file system permissions on the socket file. The server can call chmod() on the socket path to restrict access: for example, chmod(path, 0600) means only the socket owner can connect. The server can also place the socket in a directory that only authorized users can access (execute permission on the directory). This is simpler and more robust than trying to authenticate inside the application.

Q2. What is SO_PEERCRED and when would you use it?

SO_PEERCRED is a Linux socket option that retrieves the PID, UID, and GID of the process on the other end of a connected UNIX domain socket. You use getsockopt(fd,
SOL_SOCKET, SO_PEERCRED, &cred, &len)
after accept(). The kernel fills in the credentials โ€” they cannot be spoofed. This is useful for servers that need to authorize clients based on who they are (e.g., “only root or the web server user can connect”). D-Bus and systemd use this extensively.

Q3. What is the difference between SO_PEERCRED and SCM_CREDENTIALS?

SO_PEERCRED is used on stream sockets after a connection is established โ€” the receiver calls getsockopt() and the kernel reports the credentials of the connected peer. SCM_CREDENTIALS is used with ancillary data on both stream and datagram sockets โ€” the sender explicitly attaches credentials to a message using sendmsg(), and the receiver extracts them from recvmsg(). Both are kernel-verified and cannot be forged by unprivileged processes.

Q4. Can a process lie about its credentials when sending SCM_CREDENTIALS?

Only a process with CAP_SETUID / CAP_SETGID capability (typically root) can send credentials that differ from its own PID/UID/GID. A regular (non-root) process that tries to send someone else’s credentials will get EPERM. The kernel validates the credentials before allowing the sendmsg() to succeed.

Q5. What is ancillary data (control messages) in sockets?

Ancillary data (also called control messages or out-of-band data for sockets) is metadata attached to a message sent via sendmsg(). It travels alongside the normal data but in a separate buffer (msg_control). Each control message has a struct cmsghdr header with cmsg_level, cmsg_type, and cmsg_len fields identifying what the data contains. For UNIX domain sockets, the two important types are SCM_CREDENTIALS (identity) and SCM_RIGHTS (file descriptor passing, covered in Chapter 61).

Leave a Reply

Your email address will not be published. Required fields are marked *