Pourquoi ce n’est pas un bug dans qmail?

Je lisais “Quelques reflections sur la sécurité après dix ans de Qmail 1.0” de DJB et il a énuméré cette fonction pour déplacer un descripteur de fichier:

 int fd_move (to, from)
 int à;
 int de;
 {
   if (to == from) retourne 0;
   if (fd_copy (to, from) == -1) retourne -1;
   fermer (de);
   retourne 0;
 }

Il m’est apparu que ce code ne vérifiait pas la valeur de retour de close, donc j’ai lu la page de manuel pour close (2), et il semble que cela puisse échouer avec EINTR , auquel cas le comportement approprié semble être proche encore une fois avec le même argument.

Étant donné que ce code a été écrit par quelqu’un avec beaucoup plus d’expérience que moi en C et UNIX, et est resté inchangé dans qmail pendant plus d’une décennie, je suppose qu’il manque une nuance qui rend ce code correct. Quelqu’un peut-il m’expliquer cette nuance?

Lorsqu’un descripteur de fichier est dupé, comme c’est le cas dans la fonction fd_copy ou dup2 , vous vous retrouverez avec plus d’un descripteur de fichier faisant référence à la même chose (c’est-à-dire le même struct file dans le kernel). En fermant l’un d’entre eux, il suffit de décrémenter son compte de référence. Aucune opération n’est effectuée sur l’object sous-jacent, sauf s’il s’agit de la dernière fermeture. En conséquence, des conditions telles que EINTR et EIO ne sont pas possibles.

J’ai deux réponses:

  1. Il essayait de faire un point sur la prise en compte du code commun et, souvent, de tels exemples omettaient la vérification des erreurs pour des raisons de brièveté et de clarté.
  2. close (2) peut renvoyer EINTER, mais le fait-il dans la pratique, et si oui, que feriez-vous raisonnablement? Réessayez une fois? Réessayez jusqu’au succès? Et si vous obtenez EIO? Cela pourrait vouloir dire presque n’importe quoi, alors vous n’avez vraiment aucun recours raisonnable, sauf pour vous connecter et passer à autre chose. Si vous réessayez après une EIO, vous pourriez obtenir EBADF, alors quoi? Supposons que le descripteur est fermé et continue?

Chaque appel système peut renvoyer EINTR, en particulier celui qui bloque comme read (2) en attente sur un humain lent. Ceci est un scénario plus probable et une bonne routine “get input from terminal” vérifiera cela. Cela signifie également que write (2) peut échouer, même lors de l’écriture d’un fichier journal. Essayez-vous de consigner l’erreur générée par l’enregistreur ou devez-vous simplement abandonner?

Une autre possibilité est que sa fonction ne soit utilisée que dans une application (ou une partie de celle-ci) qui a fait quelque chose pour que l’appel ne soit pas interrompu par un signal. Si vous n’allez pas faire quelque chose d’important avec les signaux, vous n’avez pas à y répondre, et il peut être judicieux de les masquer tous, plutôt que d’envelopper chaque appel système bloquant dans une tentative EINTR. Sauf bien sûr ceux qui vont vous tuer, alors SIGKILL et fréquemment SIGPIPE si vous vous en occupez en quittant, avec SIGSEGV et des erreurs fatales similaires qui ne seront en aucun cas transmises à une application utilisateur appropriée.

Quoi qu’il en soit, si tout ce dont il parle, c’est la sécurité, il est fort possible qu’il n’ait pas à réessayer. Si la fermeture échouait avec EIO, il ne serait pas en mesure de le réessayer, ce serait une défaillance permanente. Par conséquent, il n’est pas nécessaire pour l’exactitude de son programme que la close réussisse. Il se peut bien que ce ne soit pas nécessaire pour l’exactitude de son programme qu’il soit à nouveau essayé sur EINTR.

Habituellement, vous voulez que votre programme fasse le meilleur effort pour réussir, et cela implique de réessayer sur EINTR. Mais c’est une préoccupation distincte de la sécurité. Si votre programme est conçu de telle sorte que certaines fonctions défaillantes pour quelque raison que ce soit ne constituent pas une faille de sécurité, le fait qu’il ait échoué à EINTR, plutôt que pour une raison permanente, n’est pas un défaut. On sait que DJB a une opinion assez juste, donc je ne serais pas du tout surpris s’il a prouvé une raison pour laquelle il n’a pas besoin de réessayer, et ne se soucie donc pas, même si cela permettrait à son programme de réussir. vider la poignée dans certaines situations où elle échoue peut-être (comme l’envoi explicite d’un signal inoffensif avec kill par l’utilisateur à un moment crucial).

Edit: il me semble que réessayer sur EINTR pourrait potentiellement être une faille de sécurité sous certaines conditions. Il introduit un nouveau comportement à cette section de code: il peut boucler indéfiniment en réponse à un signal d’inondation, où auparavant il ferait une tentative de close puis de retour. Je ne sais pas avec certitude si cela causerait des problèmes à qmail (après tout, la close ne garantit pas le retour rapide). Mais si abandonner après une tentative rend le code plus facile à parsingr, cela pourrait être une décision judicieuse. Ou pas.

Vous pourriez penser que réessayer empêche une faille DoS, où un signal provoque une défaillance fausse. Mais réessayer permet une autre faille DoS (plus difficile), où un signal d’inondation provoque un décrochage indéfini. En termes de binary “cette application peut-elle être DoSed?”, Qui est le genre de question de sécurité absolue qui intéressait DJB quand il a écrit qmail et djbdns, cela ne fait aucune différence. Si quelque chose peut arriver une fois, cela signifie normalement que cela peut se produire plusieurs fois.

Seuls les appareils cassés retournent EINTR sans que vous ne le demandiez explicitement. La sémantique sane pour signal() active les appels système réitérables (“style BSD”). Lors de la construction d’un programme sur un système avec la sémantique sysv (signaux d’interruption), vous devez toujours remplacer les appels à signal() par des appels à bsd_signal() , que vous pouvez définir en sigaction() s’il n’existe pas.

Il convient également de noter qu’aucun système ne renverra EINTR à la réception du signal, sauf si vous avez installé des gestionnaires de signaux. Si l’action par défaut est laissée en place ou si le signal n’est pas activé, il est impossible d’interrompre les appels système.