![]()
每一臺 Mac,其實都有一個“隱藏的過期時間”。
當系統連續運行 49 天 17 小時 2 分 47 秒 后,Apple XNU kernel 中一個 32 位無符號整數溢出會導致內部的 TCP 時間戳時鐘被凍結。一旦這個時鐘停止,處于 TIME_WAIT 狀態的連接將永遠不會過期,臨時端口會被慢慢耗盡,最終系統將無法再建立任何新的 TCP 連接。此時,ICMP(例如 ping)仍然可以正常工作,但除此之外,一切網絡通信都會“失效”。大多數人唯一知道的解決辦法,就是重啟機器。
那么這其中究竟發生了什么?
近日,AI Agent 公司 Photon 在監控 iMessage 服務的機器集群時發現這個問題。隨后,其在兩臺機器上現場復現了這個 bug,并最終將根因追溯到 XNU 內核源碼中的一處比較邏輯,也為我們解釋了 Bug 的來龍去脈。
來源:https://photon.codes/blog/we-found-a-ticking-time-bomb-in-macos-tcp-networking
作者 | photon 責編 | 蘇宓
出品 | CSDN(ID:CSDNnews)
![]()
背景:你需要了解的幾個概念
在深入這個 bug 之前,先簡單了解幾個關鍵概念。
什么是 TIME_WAIT?
當一個 TCP 連接關閉時,它并不會立刻消失。
發起關閉的一方會進入一個叫做 TIME_WAIT 的狀態。在這個狀態下,連接實際上已經“死亡”——不會再有數據傳輸——但操作系統仍然會把它保留一段時間。
為什么要這么做?主要有兩個原因:
1. 處理延遲到達的數據包。互聯網并不保證數據包按順序到達。某個舊連接中的數據包,可能仍在網絡中輾轉。如果操作系統立刻復用同一個源端口和目標地址來建立新連接,這些“遲到”的數據包就可能被誤認為屬于新連接,從而導致數據混亂。
2. 確保連接可靠關閉。TCP 的四次揮手以主動關閉方發送的最后一個 ACK 結束。如果這個 ACK 丟失,對端會重新發送 FIN 包。TIME_WAIT 的存在,就是為了讓系統在這段時間內還能正確處理這種重傳。
TIME_WAIT 的持續時間被定義為 2 × MSL(最大報文生存時間)。當這個時間過去之后,操作系統才會真正釋放該連接占用的資源——包括它所使用的臨時端口。
什么是 MSL?
MSL(Maximum Segment Lifetime,最大報文生存時間)指的是一個 TCP 報文在網絡中被丟棄前,理論上能夠存活的最長時間。
1981 年發布的 TCP 原始規范 RFC 793 將 MSL 設定為 2 分鐘,因此 TIME_WAIT 的時長就是 4 分鐘。
但在實際系統中,現代操作系統通常使用更短的時間:
Linux:MSL 為 30 秒,對應 TIME_WAIT 為 60 秒
macOS / XNU kernel:MSL 為 15 秒,對應 TIME_WAIT 為 30 秒
Windows:默認 MSL 為 120 秒,對應 TIME_WAIT 為 240 秒
在 macOS 上,一個關閉的 TCP 連接只會在 TIME_WAIT 狀態停留 30 秒就會被清理掉。這個速度其實很快——前提是清理機制本身沒有出問題。
什么是 32 位無符號整數回繞(wraparound)?
在 C 語言中,一個 uint32_t 能表示的范圍是 0 到 4,294,967,295(232 ? 1)。當你嘗試存儲一個超過這個最大值的數字時,它不會報錯,而是“回繞”到 0,就像里程表從 999,999 翻回 000,000 一樣。
這并不是崩潰,也不是異常,而是 C 語言對無符號整數的定義行為。真正的風險在于:如果代碼默認這個計數器只會遞增,而沒有考慮回繞的情況,就可能埋下隱患。
這類問題其實并不罕見,比如:
Windows 95 / Windows 98 的 49.7 天崩潰問題:內核的 32 位毫秒計數器溢出后,相關組件沒有正確處理回繞,導致系統卡死。
2038 年問題:使用 32 位有符號整數記錄時間(自 1970 年起的秒數)的 Unix 系統,會在 2038 年 1 月 19 日發生溢出。
GPS 周數翻轉:GPS 使用 10 位周計數器,每 1024 周(約 19.7 年)回繞一次,可能導致部分設備顯示錯誤日期。
吃豆人 256 關的“死機畫面”:8 位整數溢出使游戲在 255 關之后變得無法繼續。
我們在 macOS 中發現的這個 bug,正屬于同一類問題。XNU 內核使用一個 uint32_t 來記錄 TCP 時間戳,單位是毫秒,自系統啟動開始計數。232 毫秒正好是 49 天 17 小時 2 分 47.296 秒。一旦超過這個時間,計數器就會回繞歸零。
接下來會發生什么,正是這篇文章要講的重點。
![]()
發現過程:一個我們從未察覺的“計時炸彈”
在 Photon,我們運行著一批 Mac 機器,用來監控 iMessage 服務的健康狀況。這些機器上跑著多個 iMessage 服務實例,并由一個中央控制器持續發送 ping/pong 消息來測量往返延遲。這些機器是 24/7 持續運行的,只有在絕對必要時才會重啟。
2026 年 3 月 30 日——也就是距離上一次統一重啟正好 49.7 天——集群中的幾臺機器,開始悄無聲息地無法建立新的 TCP 連接。
奇怪的是,ping 依然正常,已有連接也沒有斷開,但凡是需要創建新 TCP socket 的操作,全部失敗。
這個現象非常典型:XNU kernel 的 TCP 時間戳計數器發生了回繞,而內部的單調性保護阻止它在溢出后繼續更新。結果就是——TCP 內部時鐘被凍結了。
接下來就是連鎖反應:TIME_WAIT 連接不再過期,臨時端口不斷堆積,卻無法被回收。
最終,系統徹底無法再建立新的連接。唯一的恢復方式就是重啟——但這不過是重新開始下一輪 49.7 天的倒計時。
在重啟這些出問題的機器以恢復服務后,我們檢查了整個集群,發現還有幾臺機器也接近同樣的臨界點——它們將在 4 月 1 日達到 49.7 天的運行時間。
于是我們決定做一個“現場實驗”。
兩臺機器(Machine A 和 Machine B)的啟動時間如下:
Machine B: { sec = 1770762608 } Tue Feb 10 14:30:08 2026兩臺機器當時都已經運行了 49 天 16 小時。精確的溢出時間如下:
Machine B:2026-04-01 08:32:55 PDT(剩余約 36 分鐘)也就是說,我們大約還有半小時來準備實驗——時間剛剛好。
![]()
實驗設計:跨越溢出窗口批量制造 TCP 連接
我們的假設很直接:如果這個 49.7 天的溢出真的會破壞 TIME_WAIT 的回收機制,那么在溢出前后制造一批短連接,應該能看到明顯的行為差異:
溢出前:TIME_WAIT 連接會在約 30 秒后正常過期
溢出后:TIME_WAIT 連接將“永久滯留”
為此,我們寫了一個測試腳本,分為三個階段:
首先是監控階段(溢出前 35 分鐘到溢出前 5 分鐘):每 10 秒記錄一次 TIME_WAIT 連接數量,不主動創建連接。
然后是沖擊階段(溢出前 5 分鐘到溢出后 5 分鐘):每 2 秒發起約 15 個短生命周期 TCP 連接,請求公共端點(例如 8.8.8.8:443、1.1.1.1:443 等),完成 TLS 握手后立即關閉。
最后是觀察階段:停止創建新連接,繼續監控 TIME_WAIT 的數量變化。
這個腳本在 07:58 被部署到兩臺機器上,并同時啟動。
![]()
實驗結果
溢出前:TIME_WAIT 正常回收
在監控階段,兩臺機器的 TIME_WAIT 表現完全健康:
[08:27:28] PHASE=wait | remain=306s | TIME_WAIT=0 | ESTABLISHED=41系統自身的后臺連接會產生少量 TIME_WAIT(0–13),并且會在幾秒內過期。這是完全正常的行為。
沖擊階段:溢出前的動態平衡
08:27:38,腳本開始創建連接。不到 30 秒,TIME_WAIT 從 0 上升到約 200,并隨后進入平臺期:
[08:31:37] PHASE=blast | remain=57s | TIME_WAIT=192腳本每 2 秒創建約 15 個連接(約每分鐘 450 個),而每個 TIME_WAIT 只會存活 30 秒就被回收。大約 30 秒后,系統進入動態平衡:TIME_WAIT 穩定在約 200 左右(理論值為 7.5 次/秒 × 30 秒 = 225;略低是因為部分連接失敗)。創建與回收保持完美平衡,這就是溢出前的健康狀態。
溢出瞬間
[08:32:39] PHASE=blast | remain=-5s | TIME_WAIT=428腳本使用墻上時間(date +%s)來估算溢出倒計時,而內核的 microuptime() 是單調時鐘。經過 49.7 天,兩者會產生幾十秒的偏差。從完整日志來看,TIME_WAIT 實際開始單調上升是在 remain≈28 秒(約 08:32:06)時——這才是回收機制真正停止的時間點。
連接依然以相同速率被創建,但沒有任何一個被回收。
溢出后:TIME_WAIT 只增不減
Machine A 的腳本在溢出后約 50 秒停止,Machine B 則繼續運行了 5 分鐘。兩臺機器的監控都持續到手動終止。
Machine B 的關鍵數據(腳本在 08:37:55 停止創建連接):
![]()
這就是決定性證據。在 macOS 中,TIME_WAIT 超時時間是 2 × MSL = 30 秒。腳本停止 84 秒后,全部 2,828 個 TIME_WAIT 連接本應已經歸零。但現實是,沒有任何一個被回收——數量甚至還在增加,因為系統自身的正常連接也開始不斷堆積。
Machine A(腳本已停止,08:50 手動檢查):
![]()
持續單調增長,沒有任何恢復跡象。
對比:溢出前 vs 溢出后
觀測數據:399, 412, 428, 443, 458, 473, 487, 502 ……(單調增長)![]()
根本原因:XNU 內核中 tcp_now 的 32 位溢出
接下來解釋為什么會發生這個問題,逐行分析 Apple 內核源碼。
漏洞分類
這是 TCP 子系統中的 32 位無符號整數計時器溢出錯誤,具體來說是 TCP 時間戳計數器的溢出。受影響的計數器 tcp_now 是內核的內部 TCP 時鐘。一旦它停止計數,TCP 棧中所有依賴它的定時器都會失效。
tcp_now:注定會溢出的計數器
在 XNU 內核(Apple 開源項目 apple-oss-distributions/xnu)中,tcp_now 定義在 bsd/netinet/tcp_var.h:
#define TCP_RETRANSHZ 1000 /* TCP 時間戳的精度,1 毫秒 */這是一個 32 位無符號整數,以毫秒為單位遞增,跟蹤自開機以來的時間。每當 TCP 子系統需要獲取當前時間戳時,會調用 calculate_tcp_clock()(基于 XNU 內核源碼分析):
}關鍵在于這一行:(uint32_t)now.tv_sec * 1000。當系統運行了 4,294,967 秒(約 49.7 天)后,這個乘法結果超過了 uint32_t 的最大值 4,294,967,295。強制轉換為 uint32_t 會導致無符號整數回繞——數值從接近最大值直接跳回接近零。
為什么 tcp_now 在溢出后會凍結
漏洞出現在這一段保護邏輯中:
}設計意圖很簡單:“tcp_now 必須只向前移動。”在正常情況下,這段邏輯工作正常。但在溢出瞬間:
比較:4,294,960,000 < 5,000? → false!舊值 tmp(接近最大)大于 new 值 current_tcp_now(回繞到接近零),cmpxchg 永遠不會執行。結果,tcp_now 鎖定在溢出前的值,再也不會更新。
內核的 TCP 時鐘徹底停止。
TIME_WAIT 過期檢查失效機制
當 TCP 連接進入 TIME_WAIT 狀態時,內核會記錄一個絕對過期時間。在`bsd/netinet/tcp_timer.c`文件中,`add_to_time_wait_locked()`函數實現了該邏輯:
}此處延遲時長計算公式為:延遲 = 2 × TCPTV_MSL = 2 × 15000 = 30000毫秒。
內核的垃圾回收函數`tcp_gc()`會周期性掃描 TIME_WAIT 隊列:
}`TSTMP_GEQ`宏定義在`bsd/netinet/tcp_seq.h`文件中:
#define TSTMP_GEQ(a, b) ((int)((a)-(b)) >= 0)這是一種標準的有符號模運算比較方式,專門用于處理序列號回繞問題。正常情況下(`tcp_now`持續遞增),當`tcp_now`大于等于過期時間時,該宏會返回 true,連接會被內核清理。
但當`tcp_now`被凍結時:
= -30000 >= 0 ? → false!計算結果一直是 false,連接永遠不會被回收。
完整因果鏈
Application-layer timeouts, services become unreachable![]()
連鎖反應:從時鐘凍結到 TCP 完全失效
這個漏洞的致命之處在于靜默失效:不會觸發內核恐慌、無錯誤日志、無崩潰報告。系統表面看起來完全正常,直到 TCP 服務徹底癱瘓。
故障演進過程:
1. 溢出后數分鐘后:TIME_WAIT 連接停止過期。如果業務僅創建少量短連接,數小時內都無法察覺異常;
2. 溢出后數小時:TIME_WAIT 連接堆積至數千個,系統臨時端口(macOS 默認范圍為 49152~65535,共 16384 個)開始耗盡;
3. 端口耗盡:新的出站連接無法綁定本地端口,卡在 SYN_SENT 狀態并失敗。已建立的長連接(ESTABLISHED)不受影響,因為已占用端口;
4. 系統負載飆升:內核持續消耗 CPU 資源掃描龐大且永不縮減的 TIME_WAIT 隊列,應用不斷重試失敗連接,進一步加重負載;
5. TCP 徹底失效:僅 ICMP 協議(ping 命令)可用,因為它不依賴 TCP 端口和 TCP 定時器子系統。
唯一恢復方案:重啟系統 → 重置 tcp_now 為 0,重新開始 49.7 天的倒計時。
![]()
佐證依據
RFC 7323 與時間戳回繞
RFC 7323(高性能 TCP 擴展)第 5.4 節(時間戳時鐘)中提到,以 1 毫秒為精度的 32 位時間戳,大約在 24.8 天(231 ms)后會發生符號位回繞。第 5.5 節(過期時間戳)要求 PAWS 實現必須在連接空閑超過 24 天后,將緩存的時間戳置為無效。
我們觀測到的溢出周期是 49.7 天,也就是完整的無符號數回繞周期 232 ms,正好是 RFC 中符號位回繞周期的兩倍。RFC 討論的是傳輸過程中對端 TCP 時間戳選項的回繞,而不是本地內核自身的定時器變量,后者屬于 XNU 實現上的缺陷。
社區中一致的故障現象報告
蘋果社區論壇和開源項目中,有多份報告描述的癥狀與該漏洞完全吻合:
蘋果社區帖子 :macOS Catalina 下“無法建立新的 TCP 連接”。新連接進入 SYN_SENT 后立即關閉,已有連接不受影響,只有重啟才能恢復。
蘋果社區帖子 :“Mac Pro TCP/IP 停止工作”。TCP 完全失效,但 ping(ICMP)仍然正常。
Podman 問題 :在 macOS 12 上“podman 虛擬機在運行一段時間后網絡連接卡住”。運行數周后,運行在 macOS 上的虛擬機出現 TCP 出站失敗,但 ICMP 仍然可用。
這些報告的共同特征:TCP 失效但 ICMP 正常,只有重啟才能解決,并且發生在連續運行數周之后。這與 tcp_now 溢出的預測癥狀完全一致。ICMP 不使用 TCP 定時器子系統,因此不受影響。
![]()
影響范圍:哪些設備會受影響?
任何同時滿足以下兩個條件的 macOS 系統:
連續運行時間超過 49 天 17 小時且未重啟
存在任何 TCP 網絡活動(幾乎所有聯網的 Mac)
大多數普通 Mac 會因為系統更新在 49 天內重啟,因此普通用戶很少觸發此問題。但以下場景屬于高風險:
長期運行的服務器集群(例如我們的 iMessage 監控系統)
macOS CI/CD 構建服務器(Jenkins、GitHub Actions 自托管運行器)
Mac Pro 工作站(長期渲染、編譯或仿真任務)
托管機房中的 Mac(遠程管理,很少重啟)
用作構建集群或測試環境的 Mac mini 集群
![]()
復現 Bug 方法
想在你自己的 macOS 設備上驗證這個漏洞?只需四步。
步驟 1:計算溢出時間
echo "Time until overflow: $((remain/3600))h $((remain%3600/60))m $((remain%60))s"步驟 2:在溢出前后監控 TIME_WAIT 數量
done步驟 3:在溢出窗口內生成連接
done步驟 4:觀察現象
停止生成連接,等待 2 分鐘。如果 TIME_WAIT 數量沒有下降,說明漏洞已復現。
![]()
9.5 小時后:親眼見證系統癱瘓
溢出后我們沒有重啟,而是讓兩臺機器繼續運行,觀察漏洞自然惡化的全過程。
溢出后 9.5 小時的系統狀態(PDT 18:02)
Load: 49.74TIME_WAIT 累積趨勢
![]()
沒有任何一個 TIME_WAIT 連接被回收。數量只增不減。
SYN_SENT 堆積:新建連接大量失敗
溢出 9.5 小時后,兩臺機器都積累了 3000 以上的 SYN_SENT 連接,這是 TCP 端口耗盡的典型表現:
出站連接卡在三次握手的第一步,無法申請到端口
臨時端口被永不釋放的 TIME_WAIT 占用
只剩下 37–38 個 ESTABLISHED 連接,已有的長連接仍然正常,但新建連接幾乎無法建立
機器 B 的系統負載飆升到 49.74,因為內核在不斷掃描不斷膨脹的 TIME_WAIT 隊列,消耗大量 CPU
這與我們預測的惡化過程完全一致:
→ Only recovery: reboot![]()
結論
一個 32 位整數。一段看似無害的 if (tmp < current_tcp_now) 保護機制。49.7 天的等待。就足以埋下一顆定時炸彈。
這類漏洞非常隱蔽,因為它躲過了所有防御環節。它不會在開發測試中被發現,誰會做連續 50 天的測試?它不會在代碼審查中被標記,邏輯看起來完全合理。它甚至可能在生產環境中被誤診為網絡問題或硬件故障。只有當你剛好盯著一臺運行了 49 天的機器,并且剛好知道 232 毫秒等于 49.7 天時,整個謎題才會被解開。
我們在多臺服務器上復現了該問題,證據確鑿:溢出前,TIME_WAIT 正常過期(0–13 個);溢出后,TIME_WAIT 永遠不回收(累積到數千個)。tcp_now 被凍結,內核的 TCP 時鐘停止。其他一切看起來都正常,直到端口耗盡。
如果你管理長期運行的 macOS 設備,請記住這個時間:
49 天 17 小時 2 分鐘 47 秒。
我們正在開發比重啟更好的修復方案,一個不需要完整重啟、專門解決 tcp_now 凍結的臨時修復方案。在此之前,請在時鐘溢出前安排重啟。
【活動分享】"48 小時,與 50+ 位大廠技術決策者,共探 AI 落地真路徑。"由 CSDN&奇點智能研究院聯合舉辦的「全球機器學習技術大會」正式升級為「奇點智能技術大會」。2026 奇點智能技術大會將于 4 月 17-18 日在上海環球港凱悅酒店正式召開,大會聚焦大模型技術演進、智能體系統工程、OpenClaw 生態實踐及 AI 行業落地等十二大專題板塊,特邀來自BAT、京東、微軟、小紅書、美團等頭部企業的 50+ 位技術決策者分享實戰案例。旨在幫助技術管理者與一線 AI 落地人員規避選型風險、降低試錯成本、獲取可復用的工程方法論,真正實現 AI 技術的規模化落地與商業價值轉化。這不僅是一場技術的盛宴,更是決策者把握 2026 AI 拐點的戰略機會。
特別聲明:以上內容(如有圖片或視頻亦包括在內)為自媒體平臺“網易號”用戶上傳并發布,本平臺僅提供信息存儲服務。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.