Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interblocage des pompes de redirection — ce que la lecture du code a établi

Date : 2026-08-13. Statut : enquête ouverte, cause non établie.

Le fait

Le 2026-08-12, la suite complète d’air-sshd lancée en root sur carbon s’est bloquée 2 h 22 min sur forward_demux_blocking::a_forward_joins_a_session_already_under_way. Deux fils, chacun dans un ppoll sans échéance, un port d’écoute toujours tenu. Arrêt manuel.

#0  air_sys_syscall::poll::raw_syscall_ppoll (fds=…, nfds=2, timeout=0, …)
#0  air_sys_syscall::poll::raw_syscall_ppoll (fds=…, nfds=3, timeout=0, …)

timeout=0 est le pointeur nul de ppoll : attendre pour toujours. Les deux comptes de descripteurs correspondent au harnais de test (client + pidfd de session) et à la boucle du monitor (pré-auth + médiation + pidfd de session).

Ni le socket client, ni le canal de médiation, ni le pidfd n’étaient lisibles : le processus de session était donc vivant, et personne n’avait rien à dire.

Jamais reproduit en isolation — seulement en suite complète. C’est la signature d’une course sensible à l’ordonnancement, et c’est probablement ce qui se cache derrière les rouges intermittents de cette famille en CI (un job expiré à 60 minutes sans une ligne).

Ce que la lecture a ÉLIMINÉ

Trois hypothèses plausibles, écartées avec leur raison. Les consigner évite qu’on les reparcoure.

1. La demande de SIGHUP perdue en silence — NON

SessionDriver::hang_up fait let _ = requester.send_session_control(SessionControl::Hangup), et ignore donc l’échec. Cela ressemble à un trou.

Ce n’en est pas un : ForwardRequester::send empoisonne le requester dès qu’une émission échoue. Un Hangup perdu implique un canal condamné, donc une connexion qui se termine de toute façon. La justification écrite (« un geste de confort perdu n’est pas une raison de refuser une session ») tient.

2. SIGHUP ignoré par héritage à travers l’execve — NON

Le dépôt sait que SIG_IGN survit à l’exec : c’est écrit dans sigpipe.rs, et c’est pourquoi le shell de session retrouve SIG_DFL sur SIGPIPE avant son execve. Si SIGHUP était masqué quelque part sans être restauré, le shell l’ignorerait et ne mourrait jamais.

Vérifié : air-sshd ne touche à aucune disposition sauf SIGPIPE, et la restaure. Ni signalfd, ni sigprocmask, ni masque hérité.

3. Le monitor cesse de lire pendant qu’il doit une annonce — NON

Quand la mort est constatée mais que l’annonce n’a pas pu être écrite, la boucle du monitor passe en awaiting_writable. Si elle n’attendait alors que la place d’écrire, un worker en train de remplir sa moitié bloquerait les deux — interblocage classique de tampons pleins.

Vérifié : forward_events vaut IN | OUT dans ce mode. Le monitor continue de lire.

Ce que la lecture a ÉTABLI

Le test attend davantage que ce que le pilote promet.

session_blocking.rs énonce son contrat sans ambiguïté :

Un CHANNEL_CLOSE ne fait plus que signaler et marquer le canal en fermeture ; la finalisation vient quand la mort est constatée. Si elle ne vient jamais, le canal reste semi-fermé et les redirections, elles, continuent d’être servies.

C’est un choix délibéré de la V2.6-3 C.5c-fix : un processus qui piège SIGHUP ne doit pas figer les redirections qui coexistent. Du point de vue du démon, l’état observé est conforme — session semi-fermée, forwarding servi.

Mais collect_session_end attend le CHANNEL_CLOSE de la session sans condition. Il encode donc une hypothèse plus forte que le contrat : ce shell-ci meurt sur SIGHUP. Quand elle est fausse — ou seulement lente —, le test n’échoue pas : il pend.

Cela déplace la question. Le rouge intermittent de la CI n’est peut-être pas un défaut du démon, mais un test qui attend une garantie que personne n’a donnée.

Ce qui reste à trancher

La lecture ne peut pas dire lequel des deux s’est produit :

  • le shell n’est pas mort — le SIGHUP n’a pas été délivré, ou pas au bon processus ;
  • il est mort sans que la mort soit constatée — le pidfd n’était pas surveillé au bon moment, ou l’annonce s’est perdue entre monitor et worker.

Les deux produisent exactement l’état observé, et aucun n’est distinguable a posteriori d’une pile figée.

#706 tranche à la prochaine occurrence : l’attente est désormais bornée à 60 s et le test échoue en nommant ce qu’il attendait et si une session était encore suivie par pidfd. C’est la différence entre un job expiré sans une ligne et un rouge qui dit où regarder.

Complété le 2026-08-13serve_session_control attendait « un message de médiation servi » et non « le raccrochage a eu effet ». Il vérifiait donc quelque chose de plus faible que ce dont le test a besoin : servi le mauvais message, l’assertion passait quand même.

Le canal de médiation étant une file, tout message doublant le Hangup l’aurait laissé non servi pour toujours — plus personne n’appelant serve_once ensuite —, produisant exactement le blocage observé. Cette hypothèse n’est pas démontrée pour ce test : la lecture montre que tous les envois du worker vers le monitor sont des paires demande/réponse, déclenchées par une action du client et déjà servies au moment du raccrochage. Il n’y a donc, ici, pas de doubleur identifié.

La fonction attend désormais la mort du processus — seule preuve que le SIGHUP est parti et a porté. Ce n’est pas présenté comme une correction de l’interblocage : c’est le déplacement du point d’échec là où la chaîne casse. Les deux branches restées indistinguables ci-dessus le deviennent :

  • l’échec tombe dans serve_session_controlle processus n’est pas mort ;
  • il tombe plus loin, dans l’attente bornée de #706 ⇒ il est mort sans que la mort soit constatée.

Si l’on veut provoquer plutôt qu’attendre

Le seul périmètre où le blocage s’est jamais montré est la suite complète en root. Deux passes ont coûté plus de deux heures pour une occurrence. Le rendement est faible ; attendre que #706 parle est probablement plus économique.