1 คะแนน โดย GN⁺ 21 일 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ตัวนับ 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 ใหม่ได้
  • สาเหตุคือ การ 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 ขั้นตอน
    1. ขั้นมอนิเตอร์: บันทึกจำนวน TIME_WAIT ทุก 10 วินาที ตั้งแต่ 35 นาทีก่อน overflow จนถึง 5 นาทีก่อน overflow
    2. ขั้นเร่งโหลด: สร้างการเชื่อมต่อ TCP สั้น ๆ ประมาณ 15 รายการทุก 2 วินาที เป็นเวลา 10 นาทีคร่อมช่วงก่อนและหลัง overflow
    3. ขั้นสังเกตผล: หยุดสร้างการเชื่อมต่อ แล้วมอนิเตอร์การเปลี่ยนแปลงของ 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 ล้มเหลวเท่านั้น, 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 ความคิดเห็น

 
GN⁺ 21 일 전
ความเห็นจาก Hacker News
  • ตอนนี้เพิ่งเข้าใจว่าทำไม iMac ของฉันบางครั้งถึงเชื่อมต่ออะไรไม่ได้เลย
    ไม่เคยรู้มาก่อนเลยว่าต้นเหตุมาจาก uptime

  • พออ่านบทความแล้วรู้สึกแรงมากว่ามันเหมือนเขียนโดย AI เลยสงสัยว่าได้ติดต่อ Apple จริงหรือเปล่า
    แน่นอนว่าบั๊กนี้สำคัญ แต่ก็รู้สึกว่ามี การใช้ถ้อยคำเกินจริง เยอะ
    ผู้ใช้ส่วนใหญ่น่าจะได้รับผลกระทบไม่มาก
    ถ้าปล่อย Mac เข้าสู่โหมดพัก เครื่องอาจหลีกเลี่ยงปัญหานี้ได้เพราะ TCP stack จะถูกรีเซ็ต
    สุดท้าย Apple ก็คงจะแก้ แต่ตอนนี้ยัง ไม่ใช่เรื่องที่ต้องตื่นตระหนก

    • ฉันก็น่าจะเคยเจอปัญหานี้เหมือนกัน
      MacBook ที่ปิดการพักเครื่องอัตโนมัติไว้เปิดมาประมาณ 50 วัน แล้วเกิดอาการ ping ได้แต่เชื่อมต่อ TCP ไม่ได้เลย
      เปลี่ยน Wi‑Fi หรือเสียบสายก็ไม่หาย แต่พอรีบูตก็กลับมาปกติทันที
    • ไม่ได้ติดต่อ Apple และดูเหมือนว่าคนเขียนบล็อกกำลังพยายามแก้เอง
      เขาบอกว่ากำลังพัฒนา วิธีแก้ชั่วคราวทางเลือก ที่ดีกว่าการรีบูต และระหว่างนี้ให้รีบูตเป็นระยะ
    • ฉันก็เปิด Mac Mini ไว้ตลอด 24 ชั่วโมง และเวลาบางครั้งเครือข่ายค้าง แค่ปิดแล้วเปิด Wi‑Fi adapter ใหม่ก็หาย
      ตอนนั้นแหละคือ จังหวะที่เหมาะจะรีบูต
    • มีการรายงานให้ Apple แล้วจริง ๆ และบอกว่าถูกลงทะเบียนในระบบภายในแล้ว
  • เดี๋ยวนี้บทความบล็อกที่เขียนโดย AI อ่านยากมาก
    สำนวน ไม่เป็นธรรมชาติและใช้เวลานานเกินไปกว่าจะเข้าประเด็น

    • ฉันก็เหมือนกัน บทความที่ AI เขียนอ่านแล้วเหนื่อยและโฟกัสไม่ได้
    • ถ้าดูเฉพาะสรุปจาก AI ก็ง่ายมาก — ปัญหาคือ Mac ไม่ยอมให้เกิด rollover ตอน clock tcp_now overflow
  • ฉันไม่เห็นด้วยกับคำพูดที่ว่า “ไม่มีนักพัฒนาคนไหนทดสอบนาน 50 วัน”
    ที่จริงแค่ทำ simulation test โดยเร่งเวลา ก็ได้

    • ใน Linux kernel มีการจับปัญหาแบบนี้โดยตั้งค่าเริ่มต้นของ jiffies counter ตอนบูตให้ไปอยู่ใกล้ค่าที่จะ overflow
    • macOS ใช้นาฬิกาฮาร์ดแวร์ จึงหยุดเดินระหว่าง sleep
      ในกรณีแบบนี้ ถ้าแก้ฟังก์ชันอย่าง calculate_tcp_clock ให้ รับ uptime เป็นอาร์กิวเมนต์ ก็จะตรวจสอบได้
    • วิธีแบบนี้ยังใช้กันบ่อยในการ ทดสอบวิดีโอเกม ด้วย
  • บั๊กนี้ไม่ได้กระทบแค่ OpenClaw แต่กระทบ การเชื่อมต่อ TCP ทั้งหมด

    • ไม่จำเป็นที่แต่ละการเชื่อมต่อจะต้องเปิดค้างไว้นาน
      ถ้า uptime ของ macOS เกิน 49.7 วัน การเชื่อมต่อ TCP ทั้งหมดก็จะเริ่มได้รับผลกระทบ
    • ยังมีมุกด้วยว่า “ตอนนี้ OpenClaw ดูเหมือนเป็นสิ่งที่สำคัญที่สุดในโลกไปแล้ว”
  • อุปกรณ์ macOS หลายเครื่องของฉันเปิดทิ้งไว้เกิน 600~1000 วัน และการเชื่อมต่อ TCP ก็ยังหมดอายุได้ตามปกติ
    เวอร์ชันเคอร์เนลคือ 20.6.0 กับ 17.7.0 ตามลำดับ
    ดังนั้นบั๊กนี้น่าจะ เกิดเฉพาะหลังบางเวอร์ชันเท่านั้น

    • จากการวิเคราะห์พบว่าค่า tcp_now ไปหยุดอยู่ก่อน overflow และเกิด wraparound ในการคำนวณ timer ผิดพลาด จนกลายเป็นค่าติดลบ ทำให้การเปรียบเทียบล้มเหลว
      ช่วงเวลาสั้น ๆ อาจทำให้การเชื่อมต่อ TIME_WAIT สะสมได้ แต่ต้นฉบับตอบสนองเกินไปและ ดูเหมือนบทความที่ LLM เขียน
    • จริง ๆ แล้วบั๊กนี้เกิดจากโค้ดที่เพิ่งถูกเพิ่มเข้ามาใหม่ใน macOS 26 เมื่อปีที่แล้ว
      ลิงก์ GitHub ที่เกี่ยวข้อง
  • ปัญหาแบบนี้เกิดซ้ำในซอฟต์แวร์หลายประเภท
    เมื่อก่อนก็เคยมีกรณีคล้ายกันใน เซิร์ฟเวอร์ Guild Wars โดยใช้การบวกค่าหนึ่งเข้าไปใน GetTickCount() เพื่อเร่งให้เกิด overflow ระหว่างทดสอบ

    • ระบบที่ต้องรองรับ overflow ควร ทดสอบด้วยการบังคับให้ overflow ทันทีหลังเริ่มทำงาน
  • บั๊กนี้ทำให้นึกถึง บั๊ก 49.7 วันของ Windows 95
    บทความที่เกี่ยวข้อง

    • ฉันก็กำลังพยายามนึกอยู่ว่าเคยเห็นตัวเลขมหัศจรรย์นี้มาจากที่ไหน
    • พูดตามตรง มันเหมือนเป็น “ปัญหาเก่าที่กลับมาใหม่
    • ปัญหารีบูตไฟเลี้ยงทุก 51 วันของ Boeing 787 ก็เป็นกรณีคล้ายกัน
    • เพราะแบบนี้เอง ตัวเลข 49.7 วันเลยรู้สึกคุ้น ๆ
  • สงสัยว่า OpenClaw เกี่ยวอะไรกับบั๊กนี้

  • ปัญหานี้ทำให้นึกถึง บั๊ก 208 วันของ Linux kernel scheduler
    ลิงก์อ้างอิง