💾 Archived View for it.omarpolo.com › articoli › passare-file-descriptor-tra-processi.gmi captured on 2022-06-11 at 20:51:21. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2021-11-30)

-=-=-=-=-=-=-

Passare file descriptor tra processi UNIX

Un modo di scrivere demoni, o comunque programmi possibilmente complessi, è quella di separare logicamente i compiti del programma in multipli processi, e farli “parlare” attraverso i vari metodi di IPC (“inter-process communication”, comunicazione tra processi) che UNIX offre.

Una volta che è stata attuata tale separazione, diventa più facile applicare altre tecniche per rendere la vita più difficile ad un attaccante: si può far girare i singoli processi come utenti diversi, applicare sandbox diverse, ecc.

I molti casi è necessario permettere ad un processo di acquisire una risorsa che altrimenti non potrebbe ottenere, come aprire un file od eseguire un altro programma per leggerne l’output. Ad esempio, ed è la motivazione dietro la scrittura di questo articolo, un demone di rete potrebbe non poter eseguire script CGI mentre si trova in una sandbox, ma potrebbe richiedere l’esecuzione di tale script a un altro processo al di fuori della sandbox e farsi passare un file descriptor relativo allo standard output dello script eseguito.

Mentre stavo implementando questa funzionalità sul mio server gemini non ho trovato molte informazioni su questa tecnica, quindi ho pensato di scrivere qualcosa a riguardo.

gmid

L’idea di base è quella di passare un file descriptor come dati ancillari usando sendmsg(2) attraverso un socket locale di dominio UNIX.

Come prima cosa bisogna creare una coppia di socket collegati usando socketpair(2):

#include <sys/socket.h>

#include <err.h>
#include <unistd.h>

int
main(void)
{
        int p[2];

        if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, p) == -1)
                err(1, "socketpair");

        switch (fork()) {
        case -1:
                err(1, "fork");

        case 0:                 /* figlio */
                close(p[0]);
                child_main(p[1]);
                _exit(1);

        default:                /* padre */
                close(p[1]);
                parent_main(p[0]);
                _exit(1);
        }
}

A differenza di una pipe dove un lato è sola scrittura e l’altro sola lettura, socketpair ritorna una coppia di socket che sono collegati: è possibile scrivere e leggere da entrambi gli estremi.

Attraverso quella coppia di socket è possibile passarsi dati normalmente, attraverso le solite read(2) e write(2), è possibile passarli a select(2) o poll(2) e via dicendo, ma è anche possibile usarli per lo scambio di file descriptor.

Il file descriptor viene mandato e ricevuto come “dati ancillari” usando sendmsg(2), e quindi serve preparare una struct msghdr in modo adeguato. La seguente funzione invia il file descriptor d attraverso il socket fd.

#include <sys/socket.h>

#include <err.h>
#include <string.h>

int
send_fd(int fd, int d)
{
        struct msghdr msg;
        union {
                struct cmsghdr hdr;
                unsigned char buf[CMSG_SPACE(sizeof(int))];
        } cmsgbuf;
        struct cmsghdr *cmsg;
        struct iovec vec;
        int result = 1;
        ssize_t n;

        memset(&msg, 0, sizeof(msg));

        if (d >= 0) {
                msg.msg_control = &cmsgbuf.buf;
                msg.msg_controllen = sizeof(cmsgbuf.buf);
                cmsg = CMSG_FIRSTHDR(&msg);
                cmsg->cmsg_len = CMSG_LEN(sizeof(int));
                cmsg->cmsg_level = SOL_SOCKET;
                cmsg->cmsg_type = SCM_RIGHTS;
                *(int*)CMSG_DATA(cmsg) = d;             /* 1 */
        } else
                result = 0;

        vec.iov_base = &result;                         /* 2 */
        vec.iov_len = sizeof(int);                      /* 3 */
        msg.msg_iov = &vec;
        msg.msg_iovlen = 1;

        if ((n = sendmsg(fd, &msg, 0)) == -1 || n != sizeof(int))
                err(1, "sendmsg");
        return 1;
}

L’union serve per far sì che la struct hdr abbia una grandezza appropriata.

Dato che il file descriptor viene passato come dati ancillari al messaggio, dobbiamo innanzitutto avere dei dati da inviare. In questo caso ho deciso di inviare un intero, che vale zero oppure uno, per indicare se “in allegato” c’è un file descriptor valido.

Nel caso si voglia inviare qualcos’altro bisognerà aggiornare la linea indicata da 3. e passare un puntatore ai dati desiderati in 2.

Il punto 1., per quanto strano possa sembrare, è necessario per allegare il file descriptor d al messaggio. La struct cmsghdr ha una sorta di “flexible argument” alla fine, ovvero dopo l’ultimo elemento della struct ci sono una sequenza di lunghezza variabile di dati.

La ricezione di un file descriptor è simile:

int
recv_fd(int fd)
{
        struct msghdr msg;
        union {
                struct cmsghdr hdr;
                char buf[CMSG_SPACE(sizeof(int))];
        } cmsgbuf;
        struct cmsghdr *cmsg;
        struct iovec vec;
        ssize_t n;
        int result;                                     /* 1 */

        memset(&msg, 0, sizeof(msg));
        vec.iov_base = &result;                         /* 2 */
        vec.iov_len = sizeof(int);                      /* 3 */
        msg.msg_iov = &vec;
        msg.msg_iovlen = 1;
        msg.msg_control = &cmsgbuf.buf;
        msg.msg_controllen = sizeof(cmsgbuf.buf);

        if ((n = recvmsg(fd, &msg, 0)) != sizeof(int))
                err(1, "recvmsg");

        if (result) {
                cmsg = CMSG_FIRSTHDR(&msg);
                if (cmsg == NULL || cmsg->cmsg_type != SCM_RIGHTS)
                        return -1;
                return (*(int *)CMSG_DATA(cmsg));       /* 4 */
        } else
                return -1;
}

In 1. sfruttiamo lo stesso trucco della union per garantirci spazio a sufficienza per i dati che vogliamo ricevere. Anche in questo caso, dato che deve essere speculare al precedente, mi aspetto di ricevere un intero.

Se si desidera inviare altri dati, bisognerà aggiornare 1., 2., e 3. di conseguenza. Il punto 4. invece non andrà cambiato, in quanto serve per leggere il file descriptor allegato, non i dati in transito.

Un esempio di send_fd e recv_fd in uso nell’ambito di un demone multi-processo è gmid, oppure vari demoni presenti nel sistema base su OpenBSD. I sorgenti di syslogd su OpenBSD, e in particolare il file privsep_fdpass.c, mi sono stati incredibilmente utili nella stesura di questo articolo e, più in generale, nel comprendere questa tecnica.

È importante sottolineare che è possibile inviare file descriptor solo attraverso socket di tipo AF_UNIX!

Infine, questa tecnica torna particolarmente utile anche nel caso alcuni processi siano all’interno di una sandbox, come quella di capsicum(4) su FreeBSD. Capsicum impedisce a un processo di eseguire diverse chiamate a sistema, tra cui la open(2), mentre permette a un processo di ottenere nuovi file descriptor attraverso sendmsg(2). Su OpenBSD, nel caso in cui il processo abbia una pledge, bisogna assicurarsi di avere la pledge “sendfd” nel processo che invia e “recvfd” in quello che riceve.

$BlogIt: passare-file-descriptor-tra-processi.gmi,v 1.1 2021/10/20 07:41:40 op Exp $