- Le compteur d’horodatage TCP (
tcp_now) de macOS subit un débordement 32 bits environ 49,7 jours après le démarrage, ce qui fige l’horloge TCP interne - En conséquence, les connexions en état TIME_WAIT n’expirent plus et s’accumulent, empêchant la libération des ports éphémères
- Avec le temps, l’épuisement des ports éphémères fait échouer toutes les nouvelles connexions TCP, tandis que les connexions existantes restent maintenues
- ICMP (
ping) continue de fonctionner normalement, mais l’ensemble des fonctions TCP est paralysé, sans possibilité de récupération autrement qu’avec un redémarrage - Les serveurs macOS, machines de build et environnements CI fonctionnant sur de longues durées sont exposés à ce problème selon un cycle de 49 jours et 17 heures, ce qui impose des redémarrages périodiques tant qu’un correctif du noyau n’est pas disponible
Contexte : notions de base sur TCP
- Une connexion TCP ne disparaît pas immédiatement à sa fermeture : elle passe en état TIME_WAIT, une étape destinée à gérer les paquets retardés et à garantir une fermeture fiable
- Cela évite qu’anciens paquets soient interprétés à tort comme appartenant à une nouvelle connexion, et permet de gérer la retransmission en cas de perte du dernier ACK
- La durée de TIME_WAIT est définie comme 2 × MSL (Maximum Segment Lifetime) ; sur macOS, elle est réglée à environ 30 secondes
- Le MSL est la durée maximale pendant laquelle un segment TCP peut survivre sur le réseau ; la RFC 793 la fixe à 2 minutes, mais les systèmes modernes utilisent généralement des valeurs bien plus courtes
- Le débordement d’un entier non signé 32 bits correspond au retour à 0 après dépassement de la valeur maximale (4 294 967 295). Sur macOS, l’horodatage TCP (
tcp_now) est un compteur 32 bits incrémenté en millisecondes depuis le démarrage, et le débordement survient après 49 jours 17 heures 2 minutes 47,296 secondes
Découverte : interruption des connexions TCP après 49,7 jours
- Les serveurs Mac de Photon dédiés à la surveillance d’iMessage fonctionnaient 24/7, et le 30 mars 2026, exactement 49,7 jours après le démarrage, toutes les nouvelles connexions TCP ont commencé à échouer
- Les connexions existantes et ICMP (
ping) continuaient de fonctionner, mais il devenait impossible d’ouvrir de nouveaux sockets TCP
- Les connexions existantes et ICMP (
- La cause est un débordement du compteur d’horodatage TCP (
tcp_now) dans le noyau XNU : une logique de validation de croissance monotone bloque la mise à jour après le wraparound, ce qui fige l’horloge TCP interne - Les connexions TIME_WAIT n’expirent donc plus, les ports éphémères ne sont jamais libérés et s’accumulent, rendant toute récupération impossible sans redémarrage
- Après redémarrage, le même phénomène se reproduit à nouveau tous les 49,7 jours
Conception de l’expérience : comparer le comportement TCP avant et après le débordement
- Hypothèse : si le garbage collection de TIME_WAIT s’arrête après le débordement, le schéma de création de connexions TCP courtes doit différer avant et après celui-ci
- Avant le débordement : expiration normale de TIME_WAIT après 30 secondes
- Après le débordement : persistance indéfinie de TIME_WAIT
- Exécution d’un script de test en trois phases
- Phase de surveillance : enregistrement du nombre de TIME_WAIT toutes les 10 secondes, de 35 minutes avant le débordement à 5 minutes avant
- Phase d’explosion : création d’environ 15 connexions TCP courtes toutes les 2 secondes pendant 10 minutes autour du débordement
- Phase d’observation : surveillance de l’évolution de TIME_WAIT après arrêt de la génération de connexions
Résultats : stagnation de TIME_WAIT après le débordement
- Avant le débordement, le nombre de connexions TIME_WAIT oscillait de façon stable entre 0 et 200, confirmant un recyclage normal
- Juste après le débordement, le nombre de connexions TIME_WAIT augmente continuellement et n’expire plus
- Sur la machine B, 2 828 connexions TIME_WAIT n’avaient toujours pas été recyclées 84 secondes plus tard, et le cumul a continué ensuite
- Sur la machine A également, les vérifications manuelles ont montré une augmentation monotone du nombre de TIME_WAIT, sans récupération possible
Cause racine : débordement 32 bits de tcp_now dans le noyau XNU
tcp_nowest un compteur 32 bits en millisecondes défini dansbsd/netinet/tcp_var.h, utilisé pour suivre le temps écoulé depuis le démarrage- Dans la fonction
calculate_tcp_clock(), l’opération(uint32_t)now.tv_sec * 1000dépasse la valeur maximale après 49,7 jours, provoquant un wraparound - À cause de la condition
if (tmp < current_tcp_now), la valeur existante devient supérieure à la nouvelle après débordement, ce qui bloque la mise à jour et fige définitivementtcp_now - Comme l’expiration de TIME_WAIT est évaluée par rapport à
tcp_now, si l’horloge s’arrête, la condition d’expiration devient toujours fausse, empêchant tout recyclage
Effet en chaîne : propagation jusqu’à l’arrêt complet de TCP
- Après quelques minutes : l’arrêt du recyclage de TIME_WAIT provoque des problèmes progressifs sur les charges avec beaucoup de connexions courtes
- Après quelques heures : des milliers de TIME_WAIT s’accumulent, entraînant un épuisement des ports éphémères
- Une fois les ports épuisés : les nouvelles connexions TCP échouent en état SYN_SENT, et seules les connexions existantes restent actives
- Hausse brutale de la charge CPU : le noyau continue à scanner la file TIME_WAIT, augmentant la charge
- Au final, paralysie complète de TCP, tandis qu’ICMP continue de fonctionner normalement
- La seule méthode de récupération est le redémarrage, après quoi le compteur de 49,7 jours repart de zéro
Éléments supplémentaires et cas similaires
- La RFC 7323 précise qu’avec des horodatages 32 bits à 1 ms, l’enroulement du bit de signe se produit environ tous les 24,8 jours
- Dans le cas de macOS, il s’agit d’un débordement complet sur 32 bits (49,7 jours), soit un défaut local du noyau distinct des problèmes d’horodatage distant abordés par la RFC
- De nombreux cas identiques ont été signalés dans la communauté Apple et dans des projets open source
- Impossible d’établir des connexions TCP,
pingreste normal, seul un redémarrage résout le problème, après plusieurs semaines de fonctionnement - Le même schéma apparaît notamment dans la Podman issue #12495
- Impossible d’établir des connexions TCP,
- Points communs : seul TCP échoue, ICMP fonctionne, redémarrage nécessaire, cycle de survenue de plusieurs semaines
Périmètre d’impact
- Le problème peut survenir sur les systèmes macOS fonctionnant en continu plus de 49 jours et 17 heures
- Les utilisateurs classiques sont peu touchés, car les mises à jour périodiques entraînent généralement des redémarrages
- Environnements à haut risque
- Flottes de serveurs tournant sur de longues durées
- Serveurs de build CI/CD basés sur macOS
- Stations de travail Mac Pro
- Mac en colocation administrés à distance
- Clusters de Mac mini utilisés pour des fermes de build ou des infrastructures de test
Procédure de reproduction
- Calculer l’instant estimé du débordement à partir de l’heure de démarrage
- Surveiller le nombre de TIME_WAIT avant et après le débordement
- Générer un grand nombre de connexions TCP courtes au moment du débordement
- Si le nombre de TIME_WAIT ne diminue pas après 2 minutes, la reproduction du bug est réussie
État du système observé après 9,5 heures
- Aucune connexion TIME_WAIT n’a été recyclée, et leur nombre a continué à augmenter
- Plus de 3 000 connexions en échec à l’état SYN_SENT se sont accumulées
- Seules les connexions existantes étaient maintenues, toute nouvelle connexion étant impossible
- La charge moyenne de la machine B est montée jusqu’à 49,74, le noyau consommant excessivement du CPU pour scanner la file TIME_WAIT
Conclusion
- Un simple entier 32 bits et la condition
if (tmp < current_tcp_now)agissent comme une bombe à retardement capable d’arrêter complètement TCP après 49,7 jours - Il s’agit d’un type de défaut difficile à détecter en phase de développement, de test ou de revue de code, et qui ne se révèle qu’en conditions réelles d’exploitation
- Photon a reproduit le même phénomène sur plusieurs serveurs, en confirmant clairement un recyclage normal avant le débordement, puis une accumulation de TIME_WAIT après celui-ci
- Lorsque
tcp_nowse fige, l’horloge TCP du noyau s’arrête ; le système semble fonctionner en apparence, mais tous les ports TCP finissent par être épuisés - Les administrateurs de systèmes macOS à longue durée de fonctionnement doivent retenir l’échéance de 49 jours 17 heures 2 minutes 47 secondes ; des redémarrages périodiques sont nécessaires tant qu’un ajustement du noyau ou un correctif n’est pas disponible
- Photon développe actuellement une solution de contournement permettant de restaurer
tcp_nowsans redémarrage
Aucun commentaire pour le moment.