Hardening consulting

Select, poll et EINTR

J'ai fais quelque modifications dans winPR pour corriger un bug dans les timers avec completion, et je suis tombé sur un soucis qui pourrait intéresser d'autres personnes que moi.

Ça concerne la gestion des EINTR, quand un appel système est interrompu par l'arrivée d'un signal, la réponse va être un -1 et errno positionné à EINTR. Par exemple quand on fait un select ou un read, la manière de se protéger de ce comportement, c'est de faire le code suivant:

#include <sys/select.h>
#include <errno.h>

void myFunction() {
    struct fd_set rset;
    int status, max_fd;

    ...


    do {
        ret = select(max_fd, &rset, NULL, NULL, NULL);
    } while (ret < 0 && errno == EINTR);
}

Bon et bien problème résolu, il suffit d'appliquer ce schéma à chaque fois qu'on a un appel système non ?

Le cas du poll

Malheureusement, ça ne va pas fonctionner dans un certain nombre de cas. Par exemple, si on utilise un POSIX timer qui va déclencher périodiquement un signal et qu'on se met en attente sur un autre file descriptor. Imaginons qu'on ai positionné un POSIX timer avec un déclenchement par SIGALARM toutes les 100 milli-secondes, et qu'on veuille attendre pendant une seconde un évènement sur un fichier fd, et que cette évènement n'arrive jamais. Le code serait le suivant et on se protège contre les EINTR:

#include <sys/poll.h>
#include <errno.h>

/* setup POSIX timer with a SIGALARM every 100 ms */

void waitOn(int fd) {
    struct pollfd set;
    int status;

    set.fd = fd;
    set.revents = 0;
    set.events = POLLIN;

    do {
        ret = poll(&set, 1, 1000);
    } while (ret < 0 && errno == EINTR);
}

Et bien plutôt que de sortir au bout d'une seconde, on va avoir une boucle infinie. En effet, toutes les 100 milli-secondes, un signal va être émis pour le POSIX timer et traité, et donc générer un retour de poll avec ret = -1 et errno = EINTR. Sauf qu'on va repartir pour un tour de boucle en attendant de nouveau 1 seconde, etc. Donc s'il ne se passe rien au niveau du fichier fd, on va rester à boucler indéfiniment plutôt que de sortir de la fonction au bout d'une seconde. On voit que le problème se situe donc dans le fait que le temps d'occurence du signal est inférieur à celui de l'attente du poll. Ou plutôt le problème se situe dans le fait qu'on ne met pas à jour le temps d'attente. Pour avoir un code correct, il faudrait avoir le temps de départ de l'attente, calculer la date d'échéance et appeler poll en ajustant le timeout à chaque tour de boucle (chose que je vous laisse en exercice ;)).

Et si on est plus select ?

Le cas de select est un peu plus subtil, en effet on aurait le code suivant:

#include <sys/select.h>
#include <errno.h>

/* setup POSIX timer with a SIGALARM every 100 ms */

void waitOn(int fd) {
    struct timeval timeout;
    struct fd_set set;
    int status;

    FD_ZERO(&set);
    FD_SET(fd, &set);

    timeout.tv_sec = 1;
    timeout.tv_usec = 0;

    do {
        ret = select(fd+1, &set, NULL, NULL, &timeout);
    } while (ret < 0 && errno == EINTR);
}

A première vue on dirait qu'on a le même bogue qu'avec poll non ? Et bien le manuel de select dit que notamment sous Linux, le champs timeout est suceptible d'être mis à jour et que timeout va contenir en sortie le temps qu'il restait à dormir avant interruption de l'appel. Donc on n'aura pas la boucle infinie car timeout va se réduire jusqu'à finir à 0, et on sortira instantanément de l'appel select avec ret = 0.

Mais si on veut faire une implémentation multi-plateforme, se baser sur ce comportement n'est pas une option et il faut, comme pour poll, recalculer le temps d'attente à chaque tour de boucle.

Conclusion

Une autre option est d'utiliser pselect ou ppoll qui désactive les signaux, mais suivant le programme, bloquer l'exécution des signaux n'est pas forcément une option viable ou souhaitable.