1 điểm bởi GN⁺ 13 ngày trước | 1 bình luận | Chia sẻ qua WhatsApp
  • Bộ đếm dấu thời gian TCP (tcp_now) của macOS sau khoảng 49,7 ngày kể từ khi khởi động sẽ bị tràn số 32-bit, khiến đồng hồ TCP nội bộ dừng lại
  • Vì vậy, các kết nối ở trạng thái TIME_WAIT không hết hạn và tiếp tục tích tụ, làm các cổng tạm thời không được giải phóng
  • Theo thời gian, do cạn kiệt cổng tạm thời, mọi kết nối TCP mới đều thất bại, chỉ các kết nối hiện có còn được duy trì
  • ICMP (ping) vẫn hoạt động bình thường, nhưng toàn bộ chức năng TCP bị tê liệt, và không thể khôi phục nếu không khởi động lại
  • Các máy chủ macOS, máy build, môi trường CI chạy dài hạn đều phơi nhiễm với vấn đề này theo chu kỳ 49 ngày 17 giờ, và cần khởi động lại định kỳ cho đến khi kernel được sửa

Bối cảnh: khái niệm cơ bản của TCP

  • Khi một kết nối TCP kết thúc, nó không biến mất ngay mà đi vào trạng thái TIME_WAIT; đây là một bước để xử lý các gói bị trễ và đảm bảo quá trình đóng kết nối đáng tin cậy
    • Nhằm ngăn các gói cũ bị hiểu nhầm là của kết nối mới, đồng thời xử lý việc truyền lại nếu ACK cuối cùng bị mất
  • Thời gian duy trì TIME_WAIT được định nghĩa là 2 × MSL (Maximum Segment Lifetime), và trên macOS được đặt khoảng 30 giây
  • MSL là khoảng thời gian tối đa mà một segment TCP có thể tồn tại trên mạng; RFC 793 định nghĩa là 2 phút, nhưng trên các hệ thống hiện đại thường được đặt ngắn hơn nhiều
  • Tràn số nguyên không dấu 32-bit là hiện tượng giá trị quay trở lại 0 khi vượt quá giá trị tối đa (4.294.967.295). Dấu thời gian TCP (tcp_now) của macOS là một bộ đếm 32-bit tăng theo mili giây kể từ lúc khởi động, nên sẽ tràn sau 49 ngày 17 giờ 2 phút 47,296 giây

Phát hiện: hiện tượng gián đoạn kết nối TCP sau 49,7 ngày

  • Các máy chủ Mac dùng để giám sát iMessage của Photon được vận hành 24/7, và vào ngày 30/3/2026, đúng 49,7 ngày sau khi khởi động, đã xuất hiện hiện tượng mọi kết nối TCP mới đều thất bại
    • Các kết nối hiện có và ICMP (ping) vẫn hoạt động bình thường, nhưng không thể tạo socket TCP mới
  • Nguyên nhân là tràn bộ đếm dấu thời gian TCP (tcp_now) trong kernel XNU; logic kiểm tra tăng đơn điệu đã chặn việc cập nhật sau khi wraparound, khiến đồng hồ TCP nội bộ dừng hẳn
  • Các kết nối TIME_WAIT không hết hạn nên cổng tạm thời không được giải phóng và tiếp tục tích tụ, cuối cùng không thể khôi phục nếu không khởi động lại
  • Sau khi khởi động lại, hiện tượng tương tự lặp lại theo chu kỳ 49,7 ngày

Thiết kế thí nghiệm: so sánh hành vi TCP trước và sau khi tràn

  • Giả thuyết: nếu cơ chế garbage collection của TIME_WAIT dừng sau khi tràn, thì mẫu tạo kết nối TCP ngắn hạn trước và sau thời điểm tràn sẽ khác nhau
    • Trước khi tràn: TIME_WAIT hết hạn bình thường sau 30 giây
    • Sau khi tràn: TIME_WAIT tồn tại vô hạn
  • Chạy một script kiểm thử gồm ba giai đoạn
    1. Giai đoạn giám sát: ghi lại số lượng TIME_WAIT mỗi 10 giây từ 35 phút trước khi tràn đến 5 phút trước khi tràn
    2. Giai đoạn bùng nổ: trong 10 phút quanh thời điểm tràn, cứ mỗi 2 giây tạo khoảng 15 kết nối TCP ngắn
    3. Giai đoạn quan sát: sau khi dừng tạo kết nối, tiếp tục theo dõi biến động của TIME_WAIT

Kết quả: TIME_WAIT bị kẹt sau khi tràn

  • Trước khi tràn, số lượng TIME_WAIT luân chuyển ổn định trong khoảng 0~200, xác nhận cơ chế thu hồi hoạt động bình thường
  • Ngay sau khi tràn, số lượng TIME_WAIT liên tục tăng và không còn hết hạn nữa
  • Với Machine B, 2.828 kết nối TIME_WAIT vẫn không có lấy một kết nối nào được thu hồi sau 84 giây, và tiếp tục tích tụ về sau
  • Machine A cũng được xác nhận thủ công là số lượng TIME_WAIT tăng đơn điệu và không thể phục hồi

Nguyên nhân gốc rễ: tràn 32-bit của tcp_now trong kernel XNU

  • tcp_now là một bộ đếm 32-bit theo mili giây được định nghĩa trong bsd/netinet/tcp_var.h, dùng để theo dõi thời gian trôi qua kể từ khi khởi động
  • Trong hàm calculate_tcp_clock(), phép tính (uint32_t)now.tv_sec * 1000 vượt quá giá trị tối đa sau 49,7 ngày và gây ra wraparound
  • Do câu lệnh điều kiện if (tmp < current_tcp_now), tại thời điểm tràn, giá trị cũ lớn hơn giá trị mới nên việc cập nhật bị chặntcp_now dừng vĩnh viễn
  • Việc kiểm tra hết hạn TIME_WAIT được thực hiện dựa trên tcp_now, nên khi đồng hồ dừng lại, điều kiện hết hạn sẽ luôn sai và không thể thu hồi

Hiệu ứng dây chuyền: lan thành sự đình trệ toàn bộ chức năng TCP

  • Sau vài phút: việc thu hồi TIME_WAIT dừng lại, gây lỗi dần trên các workload có nhiều kết nối ngắn
  • Sau vài giờ: hàng nghìn TIME_WAIT tích tụ, dẫn đến cạn kiệt cổng tạm thời
  • Sau khi cạn cổng: các kết nối TCP mới thất bại ở trạng thái SYN_SENT, chỉ các kết nối hiện có còn tồn tại
  • Tải CPU tăng vọt: kernel liên tục quét hàng đợi TIME_WAIT, làm tăng tải hệ thống
  • Kết quả là TCP tê liệt hoàn toàn, chỉ ICMP còn hoạt động bình thường
  • Cách khôi phục duy nhất là khởi động lại, sau đó bộ đếm 49,7 ngày bắt đầu lại từ đầu

Bằng chứng bổ sung và các trường hợp liên quan

  • RFC 7323 nêu rõ việc bit dấu của dấu thời gian 32-bit đơn vị 1ms sẽ wrap khoảng mỗi 24,8 ngày
    • Trường hợp của macOS là tràn toàn bộ 32-bit ở mốc 49,7 ngày, là một lỗi kernel cục bộ riêng biệt với vấn đề dấu thời gian từ xa được RFC đề cập
  • Nhiều báo cáo cùng triệu chứng đã xuất hiện trong cộng đồng Apple và các dự án mã nguồn mở
    • Không thể kết nối TCP, ping vẫn bình thường, chỉ khởi động lại mới giải quyết được, và xảy ra sau khi chạy liên tục nhiều tuần
    • Có thể thấy cùng mẫu này trong Podman issue #12495 và các nơi khác
  • Điểm chung: chỉ TCP thất bại, ICMP bình thường, cần khởi động lại, chu kỳ phát sinh theo đơn vị nhiều tuần

Phạm vi ảnh hưởng

  • Có thể xảy ra trên các hệ thống macOS chạy liên tục hơn 49 ngày 17 giờ
  • Người dùng thông thường ít bị ảnh hưởng do thường xuyên khởi động lại khi cập nhật định kỳ
  • Các môi trường rủi ro cao
    • Cụm máy chủ chạy dài hạn
    • Máy chủ build CI/CD dùng macOS
    • Workstation Mac Pro
    • Máy Mac colocated được quản trị từ xa
    • Cụm Mac mini cho build farm và hạ tầng kiểm thử

Quy trình tái hiện

  • Tính thời điểm dự kiến tràn dựa trên thời gian khởi động
  • Theo dõi số lượng TIME_WAIT trước và sau thời điểm tràn
  • Tạo nhiều kết nối TCP ngắn quanh thời điểm tràn
  • Nếu sau 2 phút số lượng TIME_WAIT không giảm, việc tái hiện lỗi được xem là thành công

Trạng thái hệ thống được quan sát sau 9,5 giờ

  • Các kết nối TIME_WAIT không có lấy một kết nối nào được thu hồi và tiếp tục tăng
  • hơn 3.000 kết nối thất bại ở trạng thái SYN_SENT tích tụ
  • Chỉ các kết nối hiện có được duy trì, không thể tạo kết nối mới
  • Tải trung bình của Machine B tăng lên 49,74, do kernel tiêu tốn quá nhiều CPU để quét hàng đợi TIME_WAIT

Kết luận

  • Chỉ một số nguyên 32-bit và điều kiện if (tmp < current_tcp_now) đã trở thành quả bom hẹn giờ khiến toàn bộ TCP ngừng hoạt động sau 49,7 ngày
  • Đây là kiểu lỗi rất khó phát hiện trong giai đoạn phát triển, kiểm thử hay code review, mà chỉ bộc lộ trong môi trường vận hành thực tế
  • Photon đã tái hiện cùng hiện tượng trên nhiều máy chủ, và xác nhận rõ ràng rằng trước khi tràn thì thu hồi bình thường, sau khi tràn thì TIME_WAIT tích tụ
  • Khi tcp_now dừng lại, đồng hồ TCP của kernel cũng dừng; hệ thống nhìn bề ngoài vẫn bình thường nhưng toàn bộ cổng TCP đều bị tiêu hao hết
  • Quản trị viên các hệ thống macOS chạy dài hạn cần nhớ mốc 49 ngày 17 giờ 2 phút 47 giây, và cần điều chỉnh chu kỳ khởi động lại hoặc khởi động lại định kỳ cho đến khi kernel được sửa
  • Photon hiện đang phát triển một biện pháp workaround để khôi phục tcp_now mà không cần khởi động lại

1 bình luận

 
Ý kiến Hacker News
  • Giờ thì cuối cùng cũng hiểu vì sao iMac của tôi thỉnh thoảng không kết nối được gì
    Tôi hoàn toàn không biết là do uptime

  • Khi đọc bài viết, tôi có cảm giác quá rõ là do AI viết, nên tự hỏi không biết họ đã thực sự liên hệ với Apple chưa
    Tất nhiên lỗi này quan trọng, nhưng tôi cảm thấy có khá nhiều cách diễn đạt cường điệu
    Có lẽ phần lớn người dùng hầu như sẽ không bị ảnh hưởng
    Nếu để Mac ở chế độ ngủ thì stack TCP sẽ được reset, nên cũng có thể tránh được vấn đề
    Dù sao Apple rồi cũng sẽ sửa thôi, nhưng hiện tại chưa đến mức phải hoảng loạn

    • Tôi nghĩ là mình cũng từng gặp vấn đề này
      Một chiếc MacBook tắt chế độ ngủ tự động đã được bật khoảng 50 ngày, và gặp hiện tượng ping thì được nhưng hoàn toàn không thể tạo kết nối TCP
      Đổi Wi‑Fi hoặc chuyển sang kết nối có dây cũng không giải quyết được, nhưng reboot xong thì mọi thứ lập tức bình thường trở lại
    • Họ chưa liên hệ với Apple, có vẻ như tác giả blog đang tự tìm cách sửa
      Có nói rằng họ đang phát triển một giải pháp thay thế tốt hơn reboot, còn trước mắt thì hãy reboot định kỳ
    • Tôi cũng để Mac Mini chạy 24/7, và thỉnh thoảng khi mạng bị treo thì chỉ cần tắt rồi bật lại adapter Wi‑Fi là xong
      Những lúc đó đúng là thời điểm tốt để reboot
    • Họ thực sự đã báo cáo với Apple, và được cho biết là đã được ghi vào hệ thống nội bộ
  • Dạo này blog do AI viết thật sự rất khó đọc
    Văn phong gượng gạo và mất quá nhiều thời gian mới đi vào trọng tâm

    • Tôi cũng vậy. Đọc bài do AI viết rất mệt và khó tập trung
    • Nếu chỉ nhìn phần AI tóm tắt thì vấn đề khá đơn giản — Mac không cho phép rollover khi đồng hồ tcp_now bị overflow
  • Tôi không đồng ý với câu “sẽ không có lập trình viên nào test trong 50 ngày”
    Thực tế chỉ cần mô phỏng kiểm thử bằng cách tăng tốc thời gian là được

    • Trong nhân Linux, để bắt những lỗi kiểu này, bộ đếm jiffies được khởi tạo ngay lúc boot với giá trị sát ngưỡng overflow
    • macOS dùng đồng hồ phần cứng nên nó dừng lại trong lúc sleep
      Trong trường hợp này, có thể sửa hàm như calculate_tcp_clock để truyền uptime làm tham số và kiểm chứng
    • Cách này cũng rất thường gặp trong kiểm thử video game
  • Lỗi này không chỉ ảnh hưởng OpenClaw mà ảnh hưởng đến mọi kết nối TCP

    • Không cần từng kết nối riêng lẻ phải tồn tại lâu
      Chỉ cần uptime của macOS vượt quá 49,7 ngày thì tất cả kết nối TCP sẽ bắt đầu bị ảnh hưởng
    • Cũng có người đùa rằng “giờ thì OpenClaw trông như thứ quan trọng nhất thế giới”
  • Nhiều thiết bị macOS của tôi đã chạy hơn 600~1000 ngày, mà các kết nối TCP vẫn hết hạn bình thường
    Phiên bản kernel lần lượt là 20.6.0 và 17.7.0
    Vì vậy có vẻ lỗi này chỉ xảy ra từ một số phiên bản nhất định trở về sau

    • Theo phân tích thì giá trị tcp_now bị kẹt ngay trước ngưỡng overflow, và do wraparound sai trong phép tính timer nên trở thành số âm khiến việc so sánh thất bại
      Trong một khoảng thời gian ngắn, các kết nối TIME_WAIT có thể tích lại, nhưng bài gốc đã phản ứng quá mức và trông như bài do LLM viết
    • Thực tế lỗi này được cho là phát sinh từ đoạn code mới được đưa vào trong macOS 26 năm ngoái
      Liên kết GitHub liên quan
  • Những vấn đề kiểu này lặp đi lặp lại trong nhiều phần mềm khác nhau
    Trước đây server Guild Wars cũng từng gặp chuyện tương tự, và để kích hoạt overflow sớm hơn, họ đã cộng một giá trị nhất định vào GetTickCount() khi test

    • Với những hệ thống phải xử lý overflow, nên chủ động gây overflow ngay sau khi khởi động để kiểm thử
  • Lỗi này gợi nhớ đến lỗi 49,7 ngày của Windows 95
    Bài viết liên quan

    • Tôi cũng đã cố nhớ xem mình từng thấy con số kỳ lạ đó ở đâu
    • Đúng kiểu một “vấn đề cũ trong hình hài mới
    • Sự cố phải reboot nguồn sau 51 ngày của Boeing 787 cũng là một ví dụ tương tự
    • Đó là lý do con số 49,7 ngày nghe quen thuộc
  • Tôi thắc mắc OpenClaw thì có liên quan gì đến lỗi này

  • Vấn đề này gợi nhớ đến lỗi 208 ngày của scheduler trong nhân Linux
    Liên kết tham khảo