技術 約6分で読めます

macOSの全マシンに潜む49.7日バグ、tcp_nowオーバーフローで新規TCP接続が死ぬ

いけさん目次

Photon社が2026年4月9日に公開した記事で、macOSのTCPネットワークスタックに潜む深刻なバグが明らかになった。
連続稼働49日17時間2分47秒、約49.7日で新規のTCP接続が一切確立できなくなる。

既存の接続やpingは通る。マシンは一見正常に動いている。
だが新しいSSH接続もWebリクエストもAPIコールも、すべてタイムアウトする。
唯一の復旧手段は再起動。

tcp_nowとは何か

macOSのXNUカーネルには tcp_now という変数がある。
システム起動からの経過ミリ秒を追跡し、TCPスタックの内部クロックとして機能する。

問題は、この変数が32ビット符号なし整数(uint32_t)として宣言されていること。

uint32_t tcp_now;  // 0 〜 4,294,967,295

保持できる最大値は4,294,967,295。
ミリ秒に換算するとこうなる。

4,294,967,295 ms
= 4,294,967.295 秒
= 71,582.788 分
= 1,193.046 時間
= 49日 17時間 2分 47.296秒

この時間が経過すると、カウンタはゼロに巻き戻る。
いわゆる32ビット整数オーバーフローだ。

なぜTCP接続が死ぬのか

tcp_now はTCPのTIME_WAIT状態の管理に使われている。

TIME_WAITは、TCPコネクションを閉じた後に一定時間そのスロットを保持する仕組み。
閉じたばかりの接続のパケットが遅延して届いた場合に、新しい接続と混同しないための安全策だ。

通常の流れでは、TIME_WAITの有効期限が切れるとスロットが解放され、新しい接続に再利用される。
だが tcp_now がオーバーフローしてゼロに戻ると、有効期限の判定ロジックが壊れる。

// 簡略化したイメージ
if (tcp_now - conn->start_time > TIMEOUT) {
    expire_connection(conn);  // スロット解放
}
// tcp_now がゼロに戻ると、この条件が成立しなくなる

TIME_WAIT接続がいつまでも解放されず、エフェメラルポート(一時ポート)が枯渇していく。

graph TD
    A["起動から49.7日経過"] --> B["tcp_now が<br/>0にラップアラウンド"]
    B --> C["TIME_WAITの<br/>有効期限判定が停止"]
    C --> D["閉じた接続の<br/>スロットが解放されない"]
    D --> E["エフェメラルポート<br/>65,536個が徐々に枯渇"]
    E --> F["新規TCP接続が<br/>すべて失敗"]
    F --> G["既存接続とICMPは<br/>正常に動作し続ける"]
    G --> H["監視システムでは<br/>稼働中と表示"]

システムが持つエフェメラルポートは約65,536個。
ビジーなサーバーなら毎時数千のTCP接続を処理するため、オーバーフロー後数分で枯渇する。
軽い使用のマシンでも、時間の問題で同じ結末を迎える。

検出が極めて困難

このバグの厄介さは、マシンが「壊れたように見えない」こと。

項目動作
ping正常に応答する
既存のSSHセッション維持される
既存のDB接続維持される
CPU/メモリ使用率正常
システムログ異常なエントリなし
新規TCP接続タイムアウト

ICMP(ping)はTCPとは別の経路で処理されるため影響を受けない。
既存のTCP接続は新しいポートを必要としないため生き続ける。
死ぬのは「新しいTCP接続の確立」だけ。

監視システムが既存接続のヘルスチェックしか見ていなければ、ダッシュボード上は「稼働中」のまま。
管理者は原因不明のまま再起動で対処し、49.7日後にまた同じ現象が再発する。

RFC 7323は25年前からこの問題を予見していた

RFC 7323「TCP Extensions for High Performance」は、TCPタイムスタンプのオーバーフロー処理を明確に定義している。

タイムスタンプカウンタがラップアラウンドする状況に備え、モジュラー算術(剰余演算)による比較を規定している。
つまり、カウンタがゼロに戻っても正しく前後関係を判定できるロジックだ。

// RFC 7323準拠のオーバーフロー安全な比較
// a が b より「後」かどうかを判定
static inline bool tcp_time_after(uint32_t a, uint32_t b) {
    return (int32_t)(a - b) > 0;
}
// 符号付きキャストにより、ラップアラウンドしても正しく判定される

AppleのXNUカーネルは、このRFC 7323準拠のロジックを実装していない。
単純な整数比較(a > b)を使っているため、tcp_now がゼロに戻った瞬間に判定が崩壊する。

XNUカーネルのソースコードはGitHubで公開されており、この問題は理論上検証可能な状態にあった。
にもかかわらず、長年見過ごされてきた。

Photonのmac miniフリートで発覚

Photon社はmac miniのフリートを運用し、毎時数千件のネットワークトランザクションを処理していた。

ある時期から、マシンが順番に同じ不可解な挙動を見せ始めた。
新しい接続が拒否されるが、すべての診断指標は正常。
管理者は原因不明のまま再起動で凌いでいた。

突破口になったのは、問題が発生したマシンの稼働時間がすべて同じ期間にクラスタリングしていたこと。
別のマシンでモニタリングスクリプトを仕込み、同じ稼働時間の閾値に近づけて検証した。
正確に49.7日を超えた時点で、新規接続が停止し始めた。

XNUカーネルのソースコードを調査し、tcp_now の32ビット整数オーバーフローが根本原因と特定された。

Windows 95/98にもあった49.7日バグ

1999年、Windows 95/98にも同じ49.7日問題が存在した。
32ビットのシステムタイマーがオーバーフローし、ブルースクリーンでクラッシュするバグだ。

項目Windows 95/98(1999年)macOS XNU(2026年)
原因32ビットタイマーのオーバーフローtcp_now(32ビットuint32_t)のオーバーフロー
発症タイミング約49.7日49日17時間2分47秒
症状ブルースクリーン新規TCP接続の静かな失敗
検出しやすさ即座にわかる極めて気づきにくい
RFC準拠N/ARFC 7323(未実装)

Windows 95/98のほうがある意味マシだった。
ブルースクリーンなら即座に異常がわかり、再起動して復旧できる。
macOSでは、マシンは表面上正常に動き続けながら、新しいネットワーク通信だけが死ぬ。
低トラフィック環境では数時間気づかない可能性がある。

同様の事例は他にもある。
Boeing 787では51日の連続稼働で電力管理システムが停止する問題が報告されており、
Linuxカーネルでも208日でスケジューラにバグが発生する事例があった。
32ビット整数のオーバーフローは、繰り返し現れる古典的な時限爆弾だ。

影響範囲と回避策

Photonの報告によれば、macOS Catalina(10.15)以降が影響を受ける。

ただし、Hacker Newsでのコミュニティ報告は混在している。
600日以上の稼働で問題なしという報告もあれば、55日で大量のTIME_WAIT蓄積を確認した報告もある。

この差はネットワークトラフィック量に依存する。
ポートプールの枯渇速度はTCP接続の頻度に比例するため、
軽い使用のマシンではオーバーフロー後もしばらく動作する場合がある。
ただし tcp_now のクロック凍結自体は確実に発生しており、枯渇は時間の問題。

スリープも関係している可能性がある。
ノートPCは頻繁にスリープに入るため、カーネルの実稼働時間が49.7日に達しにくい。
24時間稼働のデスクトップやサーバーがハイリスク。

現時点の対策

唯一の確認済み回避策は、49.7日に達する前に再起動すること。

常時稼働のMac(ホームサーバー、ビルドマシン、CI環境、メディアサーバーなど)を運用している場合、
30〜45日周期でのスケジュール再起動が推奨される。

恒久的な修正にはAppleがXNUカーネルを更新する必要がある。
技術的には複雑ではない。

  1. tcp_now を64ビットカウンタに変更する
  2. またはRFC 7323で規定されるモジュラー算術アプローチを実装する

標準仕様は何年も前から存在している。
だが2026年4月時点で、Appleからの公式コメントやパッチのタイムラインは出ていない。


macOSをサーバー用途で使うなら、7週間に一度の再起動をカレンダーに入れておくのが現実解。
25年前にWindowsで踏んだ地雷を、2026年のmacOSでもう一度踏むことになるとは。