Skip to main content

Cyber AI România

Duminică, 20 septembrie 2026

pidfd_open() și pidfd_send_signal(): cum gestionezi procese fără PID reuse races și fără semnale trimise procesului greșit

Modelul clasic de administrare a proceselor Linux are o problemă subtilă: PID-ul este doar un număr reutilizabil. Procesul 1842 poate termina execuția, iar kernelul poate atribui ulterior PID 1842 unui program complet diferit. Dacă un supervisor verifică PID-ul și trimite semnalul câteva momente mai târziu, poate lovi procesul greșit.

PID file descriptors — pidfd — rezolvă această problemă oferind un file descriptor care reprezintă o instanță concretă de proces. pidfd_send_signal() poate apoi semnaliza acea referință, nu un PID care trebuie rezolvat din nou. Linux a introdus pidfd_send_signal() în 5.1 și pidfd_open() în 5.3.

Cum apare concret PID reuse race

Imaginează-ți acest flux:

1. citești PID 1842
2. verifici /proc/1842/cmdline
3. procesul 1842 moare
4. PID 1842 este reutilizat
5. rulezi kill(1842, SIGTERM)
6. semnalul ajunge la noul proces

kill() lucrează cu identificatorul numeric disponibil la momentul apelului. În schimb, un pidfd este o referință stabilă către procesul pentru care a fost creat. Dacă acel proces dispare, pidfd_send_signal() nu mută referința către următorul proprietar al aceluiași PID; operația va eșua cu ESRCH.

Deschide un proces existent cu pidfd_open()

La nivel de C:

#define _GNU_SOURCE
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>

static int pidfd_open_sys(pid_t pid)
{
    return syscall(SYS_pidfd_open, pid, 0);
}

int main(void)
{
    pid_t pid = 1842;

    int pidfd = pidfd_open_sys(pid);
    if (pidfd == -1) {
        perror("pidfd_open");
        return 1;
    }

    printf("pidfd=%d\n", pidfd);
    close(pidfd);
}

pidfd_open() întoarce un file descriptor cu close-on-exec activat. În kernelurile moderne poate primi și PIDFD_NONBLOCK; începând cu Linux 6.9 există PIDFD_THREAD, care permite referirea la un thread individual, nu doar la thread-group leader. Man-pages 6.18 precizează că glibc nu oferă încă un wrapper direct pentru pidfd_open(), astfel încât exemplul folosește syscall().

Trimite semnalul prin referința stabilă

Adaugă wrapper-ul:

#include <signal.h>

static int pidfd_send_signal_sys(int pidfd, int sig)
{
    return syscall(
        SYS_pidfd_send_signal,
        pidfd,
        sig,
        NULL,
        0
    );
}

Apoi:

if (pidfd_send_signal_sys(pidfd, SIGTERM) == -1) {
    perror("pidfd_send_signal");
}

În acest punct nu mai transmiți 1842 către kernel. Trimiți file descriptor-ul asociat instanței originale a procesului. Sunt aplicate în continuare regulile normale de permisiuni pentru semnale; pidfd nu oferă privilegii suplimentare.

Folosește poll sau epoll în loc să verifici periodic PID-ul

Un pidfd este și pollable. Poți aștepta terminarea procesului:

#include <poll.h>

struct pollfd pfd = {
    .fd = pidfd,
    .events = POLLIN
};

if (poll(&pfd, 1, -1) == -1) {
    perror("poll");
}

Când procesul vizat termină și devine zombie, pidfd-ul devine readable prin POLLIN/EPOLLIN. După ce task-ul este reaped apare și starea de hangup. Astfel un supervisor poate integra procesele în același event loop folosit pentru socket-uri, pipe-uri și timerfd-uri, fără bucle de tip kill(pid, 0) executate periodic.

Atenție: pidfd_open nu repară un PID deja expirat

Există o limită importantă. Dacă citești PID 1842 dintr-un fișier vechi, procesul original a murit deja și PID-ul aparține acum altcuiva, atunci:

pidfd_open(1842, 0)

va deschide procesul actual cu PID 1842. pidfd elimină race-ul după obținerea referinței, nu dovedește identitatea istorică a unui PID primit dintr-o sursă nesigură.

Pentru un proces copil creat chiar de supervisor, mecanismul mai robust este clone()/clone3() cu CLONE_PIDFD, prin care kernelul întoarce pidfd-ul odată cu crearea procesului. Documentația recomandă această abordare inclusiv pentru cazurile în care un copil ar putea fi reaped înainte ca pidfd_open() să fie executat.

Un flux corect pentru un process supervisor

În loc de:

fork → salvează PID → verifică PID → kill(PID)

modelul modern devine:

creează/descoperă procesul
        ↓
obține pidfd
        ↓
poll/epoll pe pidfd
        ↓
pidfd_send_signal(pidfd, SIGTERM)
        ↓
așteaptă terminarea
        ↓
close(pidfd)

Pidfd-urile se integrează și cu waitid(), pidfd_getfd(), setns() și process_madvise(), ceea ce le transformă într-o interfață mai sigură pentru managementul proceselor decât un identificator numeric utilizat separat.

Pentru servicii, runtime-uri de containere, orchestratoare și daemon-uri care gestionează multe procese scurte, diferența este importantă: PID-ul spune „procesul care are acum numărul X”; pidfd spune „acest proces concret”. Tocmai această proprietate elimină clasa de bug-uri în care verificarea identității și acțiunea asupra procesului nu mai sunt atomice.

Surse folosite

Linux man-pages — pidfd_open(2), comportament, PIDFD_NONBLOCK, PIDFD_THREAD și folosirea cu poll/epoll.

Linux man-pages — pidfd_send_signal(2), evitarea PID reuse races, permisiuni și ESRCH.

Linux man-pages — signal(7), diferența dintre kill() și semnalizarea prin pidfd.

Facebook
X
WhatsApp

Te-ar putea interesa si: