- 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
- 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
- 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
- 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ặn và tcp_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
- Có 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
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
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ỳ
Những lúc đó đúng là thời điểm tốt để reboot
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
tcp_nowbị overflowTô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 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ứngLỗi này không chỉ ảnh hưởng OpenClaw mà ảnh hưởng đến mọi kết nối TCP
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
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
tcp_nowbị 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ạiTrong 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
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 testLỗ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 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