- ตัวนับ TCP timestamp (
tcp_now) ของ macOS จะเกิด 32-bit overflow หลังบูตไปประมาณ 49.7 วัน ทำให้ นาฬิกา TCP ภายในหยุดเดิน - ส่งผลให้ การเชื่อมต่อในสถานะ TIME_WAIT ไม่หมดอายุและสะสมเพิ่มขึ้น ทำให้พอร์ตชั่วคราวไม่ถูกคืนกลับมาใช้งาน
- เมื่อเวลาผ่านไป จะเกิด พอร์ตชั่วคราวหมด จนทำให้การเชื่อมต่อ TCP ใหม่ล้มเหลวทั้งหมด และคงเหลือเพียงการเชื่อมต่อเดิม
- ICMP (
ping) ยังทำงานปกติ แต่ ฟังก์ชัน TCP ทั้งหมดเป็นอัมพาต และไม่สามารถกู้คืนได้นอกจากรีบูต - เซิร์ฟเวอร์ macOS, เครื่อง build และสภาพแวดล้อม CI ที่รันยาวต่อเนื่อง มีความเสี่ยงเจอปัญหานี้ทุก ๆ 49 วัน 17 ชั่วโมง และจนกว่าจะมีการแก้ไขเคอร์เนลก็จำเป็นต้อง รีบูตเป็นระยะ
พื้นหลัง: แนวคิดพื้นฐานของ TCP
- เมื่อการเชื่อมต่อ TCP ปิดลง จะไม่หายไปทันที แต่จะเข้าสู่สถานะ TIME_WAIT ซึ่งเป็นขั้นตอนสำหรับจัดการแพ็กเก็ตที่มาช้าและปิดการเชื่อมต่ออย่างน่าเชื่อถือ
- เพื่อป้องกันไม่ให้แพ็กเก็ตเก่าถูกตีความผิดว่าเป็นการเชื่อมต่อใหม่ และเพื่อรองรับการส่งซ้ำเมื่อ ACK สุดท้ายสูญหาย
- ระยะเวลาของ TIME_WAIT ถูกกำหนดเป็น 2 × MSL (Maximum Segment Lifetime) และใน macOS ตั้งไว้ที่ประมาณ 30 วินาที
- MSL คือเวลาสูงสุดที่ TCP segment สามารถคงอยู่ในเครือข่ายได้ โดย RFC 793 กำหนดไว้ที่ 2 นาที แต่ในระบบสมัยใหม่มักตั้งให้สั้นกว่านั้นมาก
- 32-bit unsigned integer overflow คือปรากฏการณ์ที่เมื่อค่ามากกว่าค่าสูงสุด (4,294,967,295) แล้วจะย้อนกลับไปเป็น 0 โดย TCP timestamp (
tcp_now) ของ macOS เป็นตัวนับแบบ 32 บิตที่เพิ่มขึ้นทุกมิลลิวินาทีหลังบูต และจะเกิด overflow หลังผ่านไป 49 วัน 17 ชั่วโมง 2 นาที 47.296 วินาที
การค้นพบ: การเชื่อมต่อ TCP หยุดทำงานหลัง 49.7 วัน
- เซิร์ฟเวอร์ Mac สำหรับมอนิเตอร์ iMessage ของ Photon ทำงานแบบ 24/7 และในวันที่ 30 มีนาคม 2026 พบว่าเมื่อ ผ่านไปครบ 49.7 วันหลังบูตพอดี การเชื่อมต่อ TCP ใหม่ทั้งหมดเริ่มล้มเหลว
- การเชื่อมต่อเดิมและ ICMP (
ping) ยังทำงานปกติ แต่ไม่สามารถสร้าง TCP socket ใหม่ได้
- การเชื่อมต่อเดิมและ ICMP (
- สาเหตุคือ การ overflow ของตัวนับ TCP timestamp (
tcp_now) ในเคอร์เนล XNU โดยตรรกะที่ตรวจสอบว่าค่าต้องเพิ่มขึ้นแบบ monotonic ไปบล็อกการอัปเดตหลังเกิด wraparound ทำให้ นาฬิกา TCP ภายในหยุดนิ่ง - การเชื่อมต่อ TIME_WAIT จึงไม่หมดอายุ ทำให้ พอร์ตชั่วคราวไม่ถูกคืนและสะสมเพิ่มขึ้น จนไม่สามารถกู้คืนได้นอกจากรีบูต
- หลังรีบูต ปรากฏการณ์เดิมจะเกิดซ้ำทุก ๆ 49.7 วัน
การออกแบบการทดลอง: เปรียบเทียบพฤติกรรม TCP ก่อนและหลัง overflow
- สมมติฐาน: หาก garbage collection ของ TIME_WAIT หยุดทำงานหลัง overflow รูปแบบการสร้างการเชื่อมต่อ TCP ระยะสั้นก่อนและหลัง overflow จะต้องแตกต่างกัน
- ก่อน overflow: TIME_WAIT หมดอายุตามปกติหลัง 30 วินาที
- หลัง overflow: TIME_WAIT ค้างอยู่ไม่สิ้นสุด
- รันสคริปต์ทดสอบที่ประกอบด้วย 3 ขั้นตอน
- ขั้นมอนิเตอร์: บันทึกจำนวน TIME_WAIT ทุก 10 วินาที ตั้งแต่ 35 นาทีก่อน overflow จนถึง 5 นาทีก่อน overflow
- ขั้นเร่งโหลด: สร้างการเชื่อมต่อ TCP สั้น ๆ ประมาณ 15 รายการทุก 2 วินาที เป็นเวลา 10 นาทีคร่อมช่วงก่อนและหลัง overflow
- ขั้นสังเกตผล: หยุดสร้างการเชื่อมต่อ แล้วมอนิเตอร์การเปลี่ยนแปลงของ TIME_WAIT
ผลลัพธ์: TIME_WAIT ค้างหลัง overflow
- ก่อน overflow จำนวน TIME_WAIT หมุนเวียนคงที่อยู่ระหว่าง 0~200 และยืนยันได้ว่ามี การเก็บคืนตามปกติ
- ทันทีหลัง overflow จำนวน TIME_WAIT เพิ่มขึ้นต่อเนื่อง และไม่หมดอายุอีกต่อไป
- ในกรณีของ Machine B มีการเชื่อมต่อ TIME_WAIT จำนวน 2,828 รายการที่ผ่านไป 84 วินาทีแล้วยังไม่ถูกเก็บคืนแม้แต่รายการเดียว และหลังจากนั้นก็ยังสะสมเพิ่มต่อเนื่อง
- Machine A ก็เช่นกัน จากการตรวจสอบด้วยมือพบว่าจำนวน TIME_WAIT เพิ่มขึ้นทางเดียวและอยู่ในสภาพกู้คืนไม่ได้
รากสาเหตุ: 32-bit overflow ของ tcp_now ในเคอร์เนล XNU
tcp_nowคือ ตัวนับแบบ 32 บิตระดับมิลลิวินาที ที่นิยามไว้ในbsd/netinet/tcp_var.hเพื่อใช้ติดตามเวลาที่ผ่านไปหลังบูต- ในฟังก์ชัน
calculate_tcp_clock()การคำนวณ(uint32_t)now.tv_sec * 1000จะเกินค่าสูงสุดหลัง 49.7 วันและเกิด wraparound - เนื่องจากมีเงื่อนไข
if (tmp < current_tcp_now)เมื่อเกิด overflow ค่าก่อนหน้าจะมากกว่าค่าใหม่ ทำให้ การอัปเดตถูกบล็อก และtcp_nowหยุดค้างถาวร - การตรวจสอบการหมดอายุของ TIME_WAIT อ้างอิง
tcp_nowดังนั้นเมื่อนาฬิกาหยุด เงื่อนไขหมดอายุจะ เป็นเท็จเสมอ และไม่สามารถเก็บคืนได้
ผลกระทบต่อเนื่อง: ลุกลามจนฟังก์ชัน TCP ทั้งระบบหยุดทำงาน
- ภายในไม่กี่นาที: การเก็บคืน TIME_WAIT หยุดลง และ workload ที่มีการเชื่อมต่อสั้นจำนวนมากเริ่มมีปัญหาแบบค่อยเป็นค่อยไป
- ภายในไม่กี่ชั่วโมง: TIME_WAIT สะสมเป็นหลักพัน และเกิด พอร์ตชั่วคราวหมด
- หลังพอร์ตหมด: การเชื่อมต่อ TCP ใหม่ล้มเหลวในสถานะ SYN_SENT และเหลือเพียงการเชื่อมต่อเดิม
- โหลด CPU พุ่งสูง: เคอร์เนลสแกนคิว TIME_WAIT ต่อเนื่องทำให้โหลดเพิ่มขึ้น
- ท้ายที่สุด TCP ใช้งานไม่ได้ทั้งหมด ขณะที่ ICMP ยังทำงานปกติ
- วิธีฟื้นฟูเพียงอย่างเดียวคือ รีบูต และหลังจากนั้นตัวนับ 49.7 วันจะเริ่มใหม่อีกครั้ง
หลักฐานเพิ่มเติมและกรณีที่เกี่ยวข้อง
- RFC 7323 ระบุว่า timestamp แบบ 32 บิตที่ละเอียด 1ms จะเกิดการ wrap ของบิตเครื่องหมายประมาณทุก 24.8 วัน
- สำหรับ macOS กรณีนี้เป็นการ overflow ของ 32 บิตเต็มช่วง (49.7 วัน) ซึ่งเป็น ข้อบกพร่องในเคอร์เนลฝั่งโลคัล ที่แยกจากปัญหา timestamp ระยะไกลที่ RFC กล่าวถึง
- มีรายงานอาการเดียวกันจำนวนมากในชุมชน Apple และโปรเจกต์โอเพนซอร์ส
- เชื่อมต่อ TCP ไม่ได้,
pingปกติ, แก้ได้ด้วยรีบูตเท่านั้น, เกิดหลังรันต่อเนื่องหลายสัปดาห์ - พบรูปแบบเดียวกันใน Podman issue #12495 เป็นต้น
- เชื่อมต่อ TCP ไม่ได้,
- จุดร่วมคือ TCP ล้มเหลวเท่านั้น, ICMP ปกติ, ต้องรีบูต, และ เกิดเป็นคาบระดับหลายสัปดาห์
ขอบเขตผลกระทบ
- อาจเกิดได้กับ ระบบ macOS ที่ทำงานต่อเนื่องเกิน 49 วัน 17 ชั่วโมง
- ผู้ใช้ทั่วไปได้รับผลกระทบน้อย เพราะมักรีบูตจากการอัปเดตเป็นระยะ
- สภาพแวดล้อมความเสี่ยงสูง
- ฟลีตเซิร์ฟเวอร์ที่รันระยะยาว
- เซิร์ฟเวอร์ build แบบ CI/CD บน macOS
- เวิร์กสเตชัน Mac Pro
- Mac แบบ colocated ที่บริหารจากระยะไกล
- คลัสเตอร์ Mac mini สำหรับ build farm และโครงสร้างพื้นฐานทดสอบ
ขั้นตอนการทำซ้ำ
- คำนวณเวลาที่คาดว่าจะเกิด overflow จากเวลาเริ่มบูต
- มอนิเตอร์จำนวน TIME_WAIT ก่อนและหลัง overflow
- สร้างการเชื่อมต่อ TCP สั้น ๆ จำนวนมากในช่วงเวลาที่เกิด overflow
- หากผ่านไป 2 นาทีแล้วจำนวน TIME_WAIT ไม่ลดลง แปลว่าสามารถทำซ้ำบั๊กได้สำเร็จ
สถานะของระบบที่สังเกตหลังผ่านไป 9.5 ชั่วโมง
- การเชื่อมต่อ TIME_WAIT ไม่ถูกเก็บคืนแม้แต่รายการเดียวและยังเพิ่มขึ้นต่อเนื่อง
- การเชื่อมต่อที่ล้มเหลวในสถานะ SYN_SENT สะสม มากกว่า 3,000 รายการ
- รักษาได้เฉพาะการเชื่อมต่อเดิม และไม่สามารถสร้างการเชื่อมต่อใหม่
- ค่าโหลดเฉลี่ยของ Machine B สูงถึง 49.74 โดยเคอร์เนลใช้ CPU มากเกินไปกับการสแกนคิว TIME_WAIT
บทสรุป
- จำนวนเต็ม 32 บิตเพียงตัวเดียวและเงื่อนไข
if (tmp < current_tcp_now)กลายเป็น ระเบิดเวลาที่หยุด TCP ทั้งระบบหลัง 49.7 วัน - เป็นข้อบกพร่องประเภทที่ตรวจพบได้ยากในขั้นตอนพัฒนา ทดสอบ และ code review และมักเผยตัวเฉพาะในสภาพแวดล้อม production จริง
- Photon ทำซ้ำปรากฏการณ์เดียวกันได้บนหลายเซิร์ฟเวอร์ และยืนยันอย่างชัดเจนว่า ก่อน overflow มีการเก็บคืนตามปกติ แต่หลังจากนั้น TIME_WAIT จะสะสม
- เมื่อ
tcp_nowหยุด เคอร์เนลก็สูญเสียนาฬิกา TCP ภายใน ทำให้ระบบดูเหมือนยังปกติภายนอก แต่พอร์ต TCP ถูกใช้จนหมดทั้งหมด - ผู้ดูแลระบบ macOS ที่รันต่อเนื่องระยะยาวควรจำเวลา 49 วัน 17 ชั่วโมง 2 นาที 47 วินาที ไว้ให้ดี และ จำเป็นต้องปรับรอบรีบูตหรือรีบูตเป็นระยะจนกว่าจะมีการแก้ไขเคอร์เนล
- ขณะนี้ Photon กำลังพัฒนา วิธีแก้ชั่วคราวเพื่อกู้
tcp_nowโดยไม่ต้องรีบูต
1 ความคิดเห็น
ความเห็นจาก Hacker News
ตอนนี้เพิ่งเข้าใจว่าทำไม iMac ของฉันบางครั้งถึงเชื่อมต่ออะไรไม่ได้เลย
ไม่เคยรู้มาก่อนเลยว่าต้นเหตุมาจาก uptime
พออ่านบทความแล้วรู้สึกแรงมากว่ามันเหมือนเขียนโดย AI เลยสงสัยว่าได้ติดต่อ Apple จริงหรือเปล่า
แน่นอนว่าบั๊กนี้สำคัญ แต่ก็รู้สึกว่ามี การใช้ถ้อยคำเกินจริง เยอะ
ผู้ใช้ส่วนใหญ่น่าจะได้รับผลกระทบไม่มาก
ถ้าปล่อย Mac เข้าสู่โหมดพัก เครื่องอาจหลีกเลี่ยงปัญหานี้ได้เพราะ TCP stack จะถูกรีเซ็ต
สุดท้าย Apple ก็คงจะแก้ แต่ตอนนี้ยัง ไม่ใช่เรื่องที่ต้องตื่นตระหนก
MacBook ที่ปิดการพักเครื่องอัตโนมัติไว้เปิดมาประมาณ 50 วัน แล้วเกิดอาการ ping ได้แต่เชื่อมต่อ TCP ไม่ได้เลย
เปลี่ยน Wi‑Fi หรือเสียบสายก็ไม่หาย แต่พอรีบูตก็กลับมาปกติทันที
เขาบอกว่ากำลังพัฒนา วิธีแก้ชั่วคราวทางเลือก ที่ดีกว่าการรีบูต และระหว่างนี้ให้รีบูตเป็นระยะ
ตอนนั้นแหละคือ จังหวะที่เหมาะจะรีบูต
เดี๋ยวนี้บทความบล็อกที่เขียนโดย AI อ่านยากมาก
สำนวน ไม่เป็นธรรมชาติและใช้เวลานานเกินไปกว่าจะเข้าประเด็น
tcp_nowoverflowฉันไม่เห็นด้วยกับคำพูดที่ว่า “ไม่มีนักพัฒนาคนไหนทดสอบนาน 50 วัน”
ที่จริงแค่ทำ simulation test โดยเร่งเวลา ก็ได้
ในกรณีแบบนี้ ถ้าแก้ฟังก์ชันอย่าง
calculate_tcp_clockให้ รับ uptime เป็นอาร์กิวเมนต์ ก็จะตรวจสอบได้บั๊กนี้ไม่ได้กระทบแค่ OpenClaw แต่กระทบ การเชื่อมต่อ TCP ทั้งหมด
ถ้า uptime ของ macOS เกิน 49.7 วัน การเชื่อมต่อ TCP ทั้งหมดก็จะเริ่มได้รับผลกระทบ
อุปกรณ์ macOS หลายเครื่องของฉันเปิดทิ้งไว้เกิน 600~1000 วัน และการเชื่อมต่อ TCP ก็ยังหมดอายุได้ตามปกติ
เวอร์ชันเคอร์เนลคือ 20.6.0 กับ 17.7.0 ตามลำดับ
ดังนั้นบั๊กนี้น่าจะ เกิดเฉพาะหลังบางเวอร์ชันเท่านั้น
tcp_nowไปหยุดอยู่ก่อน overflow และเกิด wraparound ในการคำนวณ timer ผิดพลาด จนกลายเป็นค่าติดลบ ทำให้การเปรียบเทียบล้มเหลวช่วงเวลาสั้น ๆ อาจทำให้การเชื่อมต่อ TIME_WAIT สะสมได้ แต่ต้นฉบับตอบสนองเกินไปและ ดูเหมือนบทความที่ LLM เขียน
ลิงก์ GitHub ที่เกี่ยวข้อง
ปัญหาแบบนี้เกิดซ้ำในซอฟต์แวร์หลายประเภท
เมื่อก่อนก็เคยมีกรณีคล้ายกันใน เซิร์ฟเวอร์ Guild Wars โดยใช้การบวกค่าหนึ่งเข้าไปใน
GetTickCount()เพื่อเร่งให้เกิด overflow ระหว่างทดสอบบั๊กนี้ทำให้นึกถึง บั๊ก 49.7 วันของ Windows 95
บทความที่เกี่ยวข้อง
สงสัยว่า OpenClaw เกี่ยวอะไรกับบั๊กนี้
ปัญหานี้ทำให้นึกถึง บั๊ก 208 วันของ Linux kernel scheduler
ลิงก์อ้างอิง