1 points par GN⁺ 21 일 전 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • 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
  • 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
    1. Phase de surveillance : enregistrement du nombre de TIME_WAIT toutes les 10 secondes, de 35 minutes avant le débordement à 5 minutes avant
    2. Phase d’explosion : création d’environ 15 connexions TCP courtes toutes les 2 secondes pendant 10 minutes autour du débordement
    3. 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_now est un compteur 32 bits en millisecondes défini dans bsd/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 * 1000 dé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éfinitivement tcp_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, ping reste 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
  • 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_now se 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_now sans redémarrage

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.