Bom hẹn giờ 49 ngày ẩn trong macOS — toàn bộ mạng TCP ngừng hoạt động vì một lỗi kernel
(photon.codes)Đã phát hiện một lỗi trong kernel XNU của macOS khiến mạng TCP tê liệt hoàn toàn sau khi hệ thống chạy liên tục đúng 49 ngày 17 giờ 2 phút 47 giây. Nguyên nhân là tràn số nguyên 32-bit của bộ đếm dấu thời gian TCP nội bộ trong kernel (tcp_now). ping vẫn phản hồi, nhưng không thể thiết lập bất kỳ kết nối TCP mới nào, và hiện tại cách khắc phục duy nhất là khởi động lại.
Quá trình phát hiện
Photon đang vận hành 24/7 một đội máy chủ Mac để giám sát trạng thái dịch vụ iMessage. Vào ngày 30/03/2026, đúng thời điểm đã trôi qua 49,7 ngày kể từ lần khởi động lại cuối cùng, nhiều máy bắt đầu âm thầm từ chối các kết nối TCP mới. ping vẫn bình thường, các kết nối hiện có cũng được giữ nguyên, nhưng mọi nỗ lực mở socket mới đều thất bại.
Sau khi khởi động lại để khôi phục dịch vụ, nhóm đã chọn hai máy (A, B) sẽ chạm đến cùng ngưỡng này trong vài ngày tới và thiết kế một thí nghiệm trực tiếp.
Nguyên lý kỹ thuật của lỗi
Bộ đếm gây ra vấn đề tcp_now
Trong kernel XNU, tcp_now là số nguyên không dấu 32-bit đếm thời gian đã trôi qua tính từ lúc khởi động theo đơn vị mili giây. Giá trị lớn nhất mà 32-bit có thể biểu diễn là 4.294.967.295ms — quy đổi ra chính xác là 49 ngày 17 giờ 2 phút 47 giây.
Vì sao bộ đếm lại “đóng băng”?
Mã cập nhật tcp_now có một cơ chế bảo vệ đơn giản để “đồng hồ không chạy lùi”:
if (tmp < current_tcp_now) {
os_atomic_cmpxchg(&tcp_now, tmp, current_tcp_now, ...);
}
Tại thời điểm tràn, current_tcp_now mới tính toán sẽ quay về gần 0, trong khi tmp hiện có lại ở gần giá trị tối đa. Điều kiện tmp < current_tcp_now trở thành false vĩnh viễn, khiến tcp_now bị kẹt tại giá trị đó mãi mãi. Đồng hồ TCP của kernel đã dừng lại.
Vì sao TIME_WAIT không bao giờ hết hạn
Khi một kết nối TCP đóng lại, kernel sẽ ghi thời điểm hết hạn là tcp_now + 30 giây. Trình thu gom rác sẽ quét định kỳ, và nếu tcp_now >= thời điểm hết hạn thì kết nối sẽ được giải phóng. Nhưng nếu tcp_now bị đóng băng, điều kiện này sẽ không bao giờ đúng, nên các kết nối TIME_WAIT sẽ không bao giờ được thu hồi.
Kết quả thí nghiệm
Nhóm đã quan sát số lượng TIME_WAIT trong khi tạo nhiều kết nối TCP ngắn hạn mỗi giây, trong 5 phút trước và sau thời điểm tràn.
| Giai đoạn | Trạng thái |
|---|---|
| Trước khi tràn | TIME_WAIT ổn định ở khoảng ~200 (hết hạn bình thường mỗi 30 giây) |
| Ngay sau khi tràn | Việc hết hạn dừng lại và TIME_WAIT bắt đầu tăng đơn điệu |
| 84 giây sau khi ngừng tạo kết nối | TIME_WAIT lẽ ra phải về 0 nhưng lại tăng thêm (2.828 → 2.837) |
| 9,5 giờ sau khi tràn | Machine A: 4.888, Machine B: 8.217 — không thu hồi được dù chỉ một kết nối |
Sau 9,5 giờ, số kết nối ở trạng thái SYN_SENT cũng đã tích tụ vượt quá 3.000, và load average của Machine B tăng vọt lên 49,74.
Môi trường bị ảnh hưởng
Máy Mac của người dùng phổ thông thường ít bị ảnh hưởng hơn vì các bản cập nhật hệ điều hành khiến máy hay được khởi động lại trước mốc 49 ngày. Nhưng các môi trường sau thuộc nhóm rủi ro cao:
- Đội máy chủ chạy liên tục trong thời gian dài
- Máy chủ build CI/CD chạy macOS (Jenkins, GitHub Actions self-hosted runner)
- Workstation Mac Pro (chạy render hoặc biên dịch trong thời gian dài)
- Mac đặt tại colocation và được quản trị từ xa
- Build farm và hạ tầng kiểm thử dùng Mac mini
Ứng phó hiện tại và sắp tới
Hiện tại nhóm đang phát triển một biện pháp workaround có thể trực tiếp sửa tcp_now bị đóng băng mà không cần khởi động lại. Trước khi có giải pháp đó, biện pháp tạm thời chỉ có một:
Hãy lên lịch khởi động lại trước mốc 49 ngày 17 giờ 2 phút 47 giây.
Những lỗi lịch sử tương tự
Lỗi này thuộc về một dòng bug tràn số nguyên đã tồn tại từ lâu: lỗi crash sau 49,7 ngày của Windows 95/98, vấn đề năm 2038 (Y2K38), rollover số tuần của GPS, và màn kill screen ở stage 256 của Pac-Man đều thuộc cùng một họ lỗi.
Nguồn gốc bài viết: Photon Blog, 2026.04.07
9 bình luận
Dạo này macOS cũng làm đủ 49 ngày rồi nhỉ
kkkkkkkk
zzzzz
Nghe nói là vấn đề về thời gian nên tôi lại nhớ đến Y2K.. 🤖..
Con người lặp lại cùng một sai lầm.
Vậy là thật sự phải khởi động lại trước 49 ngày.
Thực ra với thời gian thì tuyệt đối không nên so sánh bằng
<..if ((int32_t)(tmp - current_tcp_now) < 0) {
os_atomic_cmpxchg(&tcp_now, tmp, current_tcp_now, ...);
}
Phải làm như thế này để xem hiệu của hai giá trị... con người lúc nào cũng lặp lại cùng một sai lầm.
Nhìn mấy thứ như thế này thì có lẽ đến năm 2038 thật sự sẽ hỗn loạn mất.
Trời ơi, cái này đúng là vô lý thật....
Vậy thì các instance Mac trên AWS hay GitHub làm sao mà từ trước đến giờ vẫn không gặp vấn đề vậy...?