7 คะแนน โดย GN⁺ 2025-07-25 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • ความปลอดภัยของหน่วยความจำ และ ความปลอดภัยของเธรด ไม่ใช่แนวคิดที่แยกออกจากกันได้ และหากไม่มีความปลอดภัยของเธรด ก็ไม่อาจบรรลุความปลอดภัยของหน่วยความจำอย่างแท้จริง
  • ในกรณีของ ภาษาที่ไม่ปลอดภัยต่อเธรด อย่าง Go ปัญหาเธรดเพียงอย่างเดียวก็อาจทำให้ความปลอดภัยของหน่วยความจำพังได้
  • บางภาษา เช่น Java ใช้ โมเดลหน่วยความจำสำหรับภาวะพร้อมกัน เพื่อทำให้แม้แต่ data race ก็ยังเป็นพฤติกรรมที่นิยามไว้ ช่วยคงความปลอดภัยในระดับภาษา
  • Go มีความเปราะบางต่อ data race และมีกรณีจริงที่เกิดการละเมิดความปลอดภัยของหน่วยความจำ
  • คุณสมบัติที่ควรถูกให้ความสำคัญจริง ๆ คือ การไม่มี Undefined Behavior (พฤติกรรมที่ไม่ถูกนิยาม)

หากไม่มีความปลอดภัยของเธรด ก็ไม่อาจรับประกันความปลอดภัยของหน่วยความจำได้

ความสับสนของแนวคิด: ความปลอดภัยของหน่วยความจำ vs ความปลอดภัยของเธรด

  • ช่วงหลังมานี้ ความปลอดภัยของหน่วยความจำ ได้รับความสนใจมากขึ้น แต่ยังไม่มีคำนิยามที่ชัดเจนว่าแท้จริงแล้วหมายถึงอะไร
  • โดยดั้งเดิม ความปลอดภัยของหน่วยความจำหมายถึงภาษาที่ป้องกันการเข้าถึงหน่วยความจำแบบ use-after-free หรือ out-of-bounds
  • ขณะที่ ความปลอดภัยของเธรด หมายถึงโปรแกรมที่ไม่มีบั๊กด้านภาวะพร้อมกัน และสองแนวคิดนี้มักถูกมองว่าเป็นคนละเรื่อง
  • ผู้เขียนโต้แย้งว่าการแบ่งเช่นนี้แทบไม่มีประโยชน์ในทางปฏิบัติ และย้ำว่าสิ่งที่เราต้องการจริง ๆ คือ การไม่มี Undefined Behavior (UB)

การละเมิดความปลอดภัยของหน่วยความจำจาก data race: ตัวอย่างของ Go

  • มีการยกตัวอย่าง ภาษา Go เพื่อแสดงปัญหาของการแยกความปลอดภัยของหน่วยความจำออกจากความปลอดภัยของเธรด
  • แม้ Go จะถูกจัดเป็นภาษาที่ปลอดภัยด้านหน่วยความจำ แต่ในโปรแกรมลักษณะต่อไปนี้ เพียงแค่ data race ก็อาจทำให้เกิด ข้อผิดพลาดของหน่วยความจำ ได้
globalVar를 반복적으로 다른 타입 값(Int, Ptr)으로 변경하면서 동시에 별도 고루틴에서 이를 읽어 메서드를 호출
  • เมื่อสองเธรดซ้อนทับกันและอัปเดตพอยน์เตอร์ภายในสองตัวของ globalVar (data, vtable) แยกกัน หากมีการอ่านระหว่างทางจะเกิดสถานะผสมและนำไปสู่การเข้าถึงหน่วยความจำที่ผิดพลาด
  • ผลลัพธ์คือพยายามอ้างอิงที่อยู่ที่ไม่ถูกต้อง (เช่น 0x2a; เลขฐานสิบหกของ 42) จนโปรแกรมจบลงด้วยข้อผิดพลาด
  • ปรากฏการณ์นี้เกิดขึ้นได้ใน interface, slice และโครงสร้างคล้ายกันของ Go เช่นกัน เพราะ ไม่ได้อัปเดตหลายฟิลด์แบบ atomic

วิธีจัดการภาวะพร้อมกันของภาษาอื่นและความปลอดภัยของหน่วยความจำ

  • ภาษาอื่นอย่าง Java ก็อาจมี data race ได้เช่นกัน แต่ใช้ โมเดลหน่วยความจำสำหรับภาวะพร้อมกันที่นิยามไว้ เพื่อรับประกันว่าโปรแกรมจะไม่ทำให้ตัวภาษาเองพัง
    • ตัวอย่าง: Java ออกแบบโมเดลหน่วยความจำอย่างพิถีพิถันเพื่อไม่ให้เกิดความล้มเหลวระดับรันไทม์ (เช่น การเกิด segmentation fault โดยตรง) แม้อยู่ในสภาพแวดล้อมหลายเธรด
  • โดยมากภาษาโปรแกรมจะควบคุมปัญหาภาวะพร้อมกันด้วยหนึ่งในสองแนวทางต่อไปนี้
    • นิยามโมเดลหน่วยความจำ ให้โปรแกรมแบบพร้อมกันทั้งหมดมีพฤติกรรมที่สอดคล้องกันเสมอ (แลกกับข้อจำกัดด้านการ optimize ของคอมไพเลอร์และภาระการนำไปใช้งานที่สูงขึ้น)
      • Java, C#, OCaml, JavaScript, WebAssembly เป็นต้น
    • ใช้ ระบบชนิดข้อมูลที่เข้มแข็ง เพื่อห้าม data race ส่วนใหญ่ และจัดการข้อยกเว้นจำนวนน้อยให้ปลอดภัย (เช่น Rust, strict concurrency ของ Swift)
  • Go ไม่ได้เลือกตามสองทางนี้
    • รับประกันความปลอดภัยของหน่วยความจำเฉพาะเมื่อไม่มี data race
    • แม้จะมีเครื่องมือตรวจจับ data race แต่ในโปรแกรมจริงก็มีข้อจำกัดในการพิสูจน์ทุกสถานการณ์ผ่านการทดสอบ
    • ทั้งงานวิจัยและประสบการณ์ภาคสนามรายงานกรณีละเมิดความปลอดภัยของหน่วยความจำจริงอยู่จำนวนมาก

โมเดลหน่วยความจำของ Go และปัญหาด้านเอกสารประกอบ

  • เอกสารทางการของ Go memory model ระบุว่า race ส่วนใหญ่มีผลลัพธ์ที่จำกัด แต่ไม่ได้อธิบายอย่างชัดเจนว่าบาง data race อาจให้ผลลัพธ์ได้ไม่จำกัด
  • แม้จะมีคำกล่าวว่า Go คล้ายกับ Java/JavaScript แต่สองภาษานั้นลงทุนมากกว่ามากในการรับรองความปลอดภัยด้านภาวะพร้อมกันเมื่อเทียบกับ Go
  • มีเพียงบางส่วนย่อยของเอกสารเท่านั้นที่กล่าวอย่างจำกัดว่าบาง data race อาจก่อให้เกิด พฤติกรรมที่ไม่ถูกนิยามโดยสมบูรณ์

บทสรุป: การไม่มี Undefined Behavior (UB) คือเป้าหมายที่แท้จริง

  • ในทางปฏิบัติ คุณสมบัติที่ผู้ใช้ต้องการจริง ๆ คือ โปรแกรมไม่ทำให้ตัวภาษาเองพัง (ไม่มี UB)
  • ช่องโหว่ด้านความปลอดภัยหลายรูปแบบที่เกิดจากการละเมิดความปลอดภัยของหน่วยความจำ ล้วนเกิดขึ้นเพราะ UB เกิดขึ้นจริง
  • ทันทีที่ UB เกิดขึ้น พฤติกรรมทั้งหมดหลังจากนั้นจะคาดเดาไม่ได้ และผู้โจมตีก็สามารถใช้ประโยชน์จากจุดนี้ได้
  • ความแตกต่างเชิงแก่นระหว่างภาษาที่ “ปลอดภัย” กับ “ไม่ปลอดภัย” อยู่ที่ความเป็นไปได้ของการเกิด UB
  • แทนที่จะแยกย่อยเป็นความปลอดภัยของหน่วยความจำ ความปลอดภัยของเธรด หรือความปลอดภัยของชนิดข้อมูล สิ่งสำคัญคือ เกิด UB หรือไม่
  • ในความเป็นจริง ความปลอดภัยมีลักษณะเป็นสเปกตรัม และ Go แม้จะปลอดภัยกว่า C แต่ก็ไม่ได้รับประกันความปลอดภัยอย่างสมบูรณ์
  • การจะ “พิสูจน์” ความปลอดภัยจริงของ Go ด้วยข้อมูลเชิงประจักษ์นั้นทำได้ยากมาก และสิ่งสำคัญคือการเข้าใจผลลัพธ์ที่ไม่ตรงสัญชาตญาณจากทางเลือกที่แต่ละภาษาตัดสินใจไว้

1 ความคิดเห็น

 
GN⁺ 2025-07-25
ความเห็นจาก Hacker News
  • เรื่องนี้เคยเกิดขึ้นในทีม Dropbox ของฉัน โดยการเขียนลง data structure ในเซิร์ฟเวอร์ Go โดยไม่มีการซิงก์ เป็นเหมือนพิธีรับน้องที่ทำให้วิศวกรใหม่ ๆ เจอ segfault ซ้ำแล้วซ้ำเล่า
    Swift ก็มีปัญหาเดียวกัน และฉันเคยเขียนโปรแกรมเพื่อแสดงให้เห็นว่า Swift สามารถทำให้เกิด segfault ได้ง่ายมากเมื่อเข้าถึง shared data structure
    การบอกว่า Go เป็น memory-safe ในความหมายเดียวกับ Rust หรือ Java จึงค่อนข้างพูดเกินจริง
  • Swift กำลังพยายามแก้ปัญหานี้อยู่ แต่ในโลกความเป็นจริงมีโค้ดที่ไม่ปลอดภัยอยู่มากแล้ว ทำให้การเปลี่ยนแปลงช้ามากและเจ็บปวดมาก
  • มีเรื่องที่สงสัยคือ ปกติ basic structure อย่าง map ไม่ได้เป็น thread-safe อยู่แล้ว จึงต้องระวังเวลาจะแก้ไข ซึ่งในสเปกของ Go ก็เขียนไว้ชัดเจน
    อยากฟังรายละเอียดเพิ่มเติมเกี่ยวกับสถานการณ์ปัญหาที่เกิดขึ้นที่ Dropbox
  • อยากเน้นว่าสิ่งที่พูดถึงตรงนี้ว่า “memory safety ในความหมายของ Rust หรือ Java” ไม่ใช่คำจำกัดความทางศัพท์แบบเคร่งครัด
    memory safety ไม่ได้เป็นแนวคิดของ PLT (programming language theory) เท่ากับเป็นคำศัพท์ด้าน software security มากกว่า
    สุดท้ายแล้วโปรแกรมเมอร์ Go ก็เข้าใจความต่างนี้ดีอยู่แล้ว และนั่นจึงเป็นเหตุผลที่ Go ตั้งต้นจากแนวคิดประมาณว่า “อย่าสื่อสารด้วยการแชร์ แต่ให้แชร์ผ่านการสื่อสาร”
    แน่นอนว่าในโลกจริงแนวคิดนี้ไม่ได้เกิดขึ้นอย่างสมบูรณ์ และทุกคนก็เข้าใจว่า Go ยุคใหม่มีการแชร์มากขึ้นและต้องการการซิงก์
  • เพื่อให้เห็นภาพ อยากถามตัวเองว่าจริง ๆ แล้วมีกรณีแบบไหนบ้างที่ทำให้ Go ไม่ memory-safe หรือความน่าจะเป็นที่โปรแกรม Go จะไม่ memory-safe ในทางปฏิบัติมีมากแค่ไหน
  • Java เองก็ไม่ได้ memory-safe ในระดับเดียวกับความหมายของ Rust
  • ประเด็นนี้มักถูกหยิบยกขึ้นมาซ้ำ ๆ คล้ายกับปัญหา soundness hole ของ Rust และไม่ใช่เรื่องไร้สาระเลย แต่โอกาสที่จะเจอโดยบังเอิญค่อนข้างต่ำ
    จากประสบการณ์ที่ใช้งาน Go ในระบบจริงมาหลายปี ฉันแทบไม่เคยเห็นบั๊กแบบนี้เกิดขึ้นจริง
    Uber เคยสรุปรายละเอียดเกี่ยวกับบั๊กที่เกิดในโค้ด Go เอาไว้ และบทความนี้มีตารางสรุปว่าปัญหานี้เกิดบ่อยแค่ไหนในทางปฏิบัติ
    ปัญหาการเข้าถึง map หรือ slice พร้อมกันใน Go ส่วนใหญ่มักเกิดกับ slice เดียวกัน และต้องมีปรากฏการณ์ “torn read” จึงทำให้ในความเป็นจริงไม่ได้พบบ่อย
    ถึงอย่างนั้น เหตุผลที่ผู้คนเลี่ยงปัญหาแบบนี้ได้ดีก็น่าจะเป็นเพราะโดยทั่วไปค่อนข้างระมัดระวัง และรู้ดีถึงความเสี่ยงของการ reassign ตัวแปรในสถานการณ์ที่มี concurrent access
    ตัวภาษาเองก็มี atomics, channel และ mutex ให้ใช้ จึงพบไม่บ่อยนักที่คนจะใช้ผิดในสถานการณ์ concurrent access และยังมี race detector ที่ช่วยให้เจอปัญหาแบบนี้ได้เร็ว
    ต่อให้มี performance hit ฉันก็คิดว่าปัญหา torn read เป็นเรื่องที่แก้ได้ตรงไปตรงมา และในโค้ด Go ที่รันจริงก็ไม่เคยเป็นปัญหาใหญ่
    วิดีโอที่เกี่ยวข้อง
  • ฉันเคยมีประสบการณ์ใช้เวลาหลายเดือนเพื่อจับบั๊ก data race ใน Go
    แม้แต่ race detector ก็ไม่พบอะไร และไม่มีใครเข้าใจว่าเกิดอะไรขึ้น
    ท้ายที่สุดพบว่า loop counter overflow ทำให้การคำนวณเดิมถูกทำซ้ำมหาศาล และบางครั้งคำขอใช้เวลา 3 นาทีแทนที่จะเป็น 100ms
    เรารู้ปัญหาทางอ้อมจากการใช้ perf ใน production และประสบการณ์การดีบักแบบ platform developer ของฉันก็ช่วยทีมได้มาก
    เพราะเจอสถานการณ์ race ใน Go มาหลากหลายแบบ ส่วนตัวจึงหวังว่า Rust จะถูกนำมาใช้ทุกที่
  • ฝั่งผู้ดูแล Rust เองก็ยอมรับว่า soundness hole เป็นบั๊ก
    ตัวอย่างเช่น issue นี้ ต้องอาศัยการ refactor ใหญ่ในคอมไพเลอร์จึงใช้เวลานานมาก
  • Uber บอกว่าโปรแกรม Go “เผยให้เห็น concurrency มากกว่า 8 เท่า” เมื่อเทียบกับ Java microservice แต่สงสัยว่าการใช้คำว่า concurrency เหมือนเป็นคำนามนับได้ตรงนี้หมายถึงอะไร
  • Zig ก็อ้างว่า memory-safe เช่นกัน แต่ไม่มีแนวคิดแบบ Send/Sync type ของ Rust
    ในทางปฏิบัติจนถึงตอนนี้ยังมีโค้ด Zig แบบ concurrent ไม่มาก ปัญหาจึงยังไม่ปะทุชัด แต่คิดว่าเมื่อฟีเจอร์ async ถูกใช้อย่างแพร่หลายมากขึ้น ปัญหาหลายอย่างอาจระเบิดออกมาพร้อมกัน
  • แม้แต่โปรแกรม Zig แบบ single-thread ที่ build ด้วย ReleaseSafe ก็ยังไม่ปลอดจากความเสี่ยง memory corruption ในทุกโหมด optimization เช่นกรณี dereference pointer ที่หมดอายุของ local variable ไปแล้ว
  • การอ้างว่า Zig มี memory safety ฟังดูเกือบจะเป็นมุกตลก
    แน่นอนว่ามันลดบั๊กได้มากกว่า C แต่ C++ ก็เป็นแบบนั้นเหมือนกัน และไม่มีใครบอกว่า C++ เป็น memory-safe
  • ในโค้ดจริง ฉันไม่เคยเห็นโค้ด Go ที่มีช่องโหว่จาก data race หากไม่ได้ถูกออกแบบมาอย่างมุ่งร้าย
    แน่นอนว่านี่ไม่ได้แปลว่าความเสี่ยงเป็นศูนย์ แต่ก็บ่งชี้ว่าในมุมความปลอดภัยของแอป Go มันอาจไม่ใช่ประเด็นลำดับต้น ๆ
    ในทางกลับกัน โค้ด C/C++ มีช่องโหว่จริง 60~75% ที่มาจากปัญหา memory safety
    ฉันคิดว่า memory safety ก็เป็นเหมือนสเปกตรัมต่อเนื่อง และหลังจากระดับหนึ่งไปแล้วประโยชน์จะเริ่มลดลง
  • ฉันเคยเห็นโค้ด Go ที่มีช่องโหว่จริงจาก data race
  • ตอนนี้รู้สึกว่าความเจ็บปวดในการบำรุงรักษาหนักหนากว่า CVE มาก
    ต่อให้เป็นบั๊กที่ exploit ไม่ได้ มันก็ยังเป็นบั๊กที่ต้องแก้อยู่ดี
    เวลาส่วนใหญ่หมดไปกับการบำรุงรักษามากกว่าการพัฒนาช่วงแรก ถ้าช่วยลดภาระการบำรุงรักษาได้ ต่อให้การเปิดตัวครั้งแรกช้าลงก็คุ้มค่า
  • เหตุผลที่ memory safety สำคัญก็เพราะ CVE ของโปรแกรม C ส่วนใหญ่เกิดจากบั๊ก memory safety
    แต่ใน Go นั้น thread safety ไม่ใช่สาเหตุหลักของ CVE
    ในทางทฤษฎีมันมีเหตุผลรองรับ แต่ในโลกจริงไม่ได้เด่นชัดขนาดนั้น
  • สิ่งสำคัญจริง ๆ คือใน thread หนึ่ง ๆ สามารถทำอะไรได้บ้าง
    เมื่อมีการแชร์หน่วยความจำ หากทำให้ data structure เสียหาย ก็อาจทำให้เกิดพฤติกรรมที่ไม่ปลอดภัยหรือไม่ถูกต้องในอีก thread หนึ่งได้
    ตัวอย่างเช่น หาก thread หนึ่งเปลี่ยนขนาดของ vector ขณะที่อีก thread เข้าถึงมัน งานที่ปลอดภัยในการรันแบบลำดับก็อาจกลายเป็นอันตรายในสภาวะ concurrent
    Go เองก็หนีเรื่องนี้ไม่พ้น
  • ปัญหา memory safety แบบคลาสสิกของ C มักมีโอกาสนำไปสู่ RCE (remote code execution) สูง
    ในทางกลับกัน ถ้าปัญหา thread safety จบแค่ segfault มันอาจเป็นเพียงการโจมตีแบบ DoS (denial of service) เท่านั้น
    race condition อาจนำไปสู่การโจมตีที่รุนแรงกว่านั้นได้ แต่ก็ยากกว่ามากในการกระตุ้นให้เกิด
  • แม้ CVE จะร้ายแรงกว่า แต่ data corruption/แครชจากบั๊ก threading ก็ยังเป็นบั๊กที่ต้องมีคนมา triage วิเคราะห์ และแก้ไขอยู่ดี
  • ความจริงที่น่าเศร้าคือภาษาส่วนใหญ่ที่ใช้ thread มักเปิดให้ใช้ global variable และแชร์หน่วยความจำแบบไม่จำกัดเป็นค่าเริ่มต้น
    นี่คือสาเหตุหลักของ data corruption และ race
    ในหลายสถานการณ์ โมเดลแบบ process-based อาจดีกว่า thread-based สำหรับงาน concurrency แต่ก็มีข้อเสียคือหนักเกินไป
    ถ้าค่าเริ่มต้นคือส่งข้อมูลที่แต่ละ thread ต้องใช้ผ่าน message passing ทั้งหมด ปัญหาพวกนี้ส่วนใหญ่ก็คงหายไป
    อย่างไรก็ดี ตอนนี้เรามีอิสระที่จะใช้ global variable และ shared memory บนแพลตฟอร์ม ก็แค่ต้องเลือกไม่ใช้เอง
  • Rust เป็นตัวอย่างเด่นของภาษาแบบ modern ที่สามารถฝัง thread safety ไว้ใน type system ได้
    เป้าหมายดั้งเดิมของ Rust ไม่ใช่การเป็น memory-safe systems language แต่เป็น thread-safe systems language และ memory safety เป็นผลที่ตามมาโดยธรรมชาติ
    ใน Rust สามารถใช้ structured concurrency ผ่าน thread::scope เป็นต้นได้ ทำให้การทำงานกับ thread สะดวกมาก
  • message passing อาจก่อให้เกิดปัญหาเชิงตรรกะได้มากกว่า shared memory ด้วยซ้ำ เช่น race condition/deadlock จึงไม่ใช่คำตอบสารพัดนึก
  • ใน Go มีแนวโน้มจะเน้นการสื่อสารระหว่าง goroutine (เช่น channel) มากกว่าการแชร์หน่วยความจำโดยตรง
    ดูเอกสารนี้
  • ต่อให้ส่ง object ระหว่าง goroutine ผ่าน channel แต่ Go ก็ไม่มีแนวคิดอย่าง sendable type, ownership หรือ read-only reference จึงไม่ง่ายที่จะเขียนให้ปลอดภัย
    ตัวอย่างจริง:
    func processData(lines <-chan []byte) {
     for line := range lines {
      fmt.Printf("processing line: %v\n", line)
     }
    }
    
    func main() {
     lines := make(chan []byte)
     go processData(lines)
    
     var buf bytes.Buffer
     for range 3 {
      buf.WriteString("mock data, assume this got read into the buffer from a file or something")
      lines <- buf.Bytes()
      buf.Reset()
     }
    }
    
    ในโค้ดด้านบน buf.Bytes() จะส่งต่อการอ้างอิงถึงหน่วยความจำภายในโดยตรง และเมื่อเรียก Reset() หน่วยความจำ backing memory จะถูกนำกลับมาใช้ใหม่ ทำให้ทั้ง processData/main เข้าถึงหน่วยความจำเดียวกันพร้อมกันและเกิด data race
    ใน Rust โค้ดแบบนี้จะคอมไพล์ไม่ผ่านตั้งแต่แรก เพราะมันคือ mutable reference สองตัว และจะถูกบังคับให้โอน ownership หรือทำสำเนาแทน
    ใน Go มันทำให้สับสนได้ง่าย และแม้ bytes.Buffer.ReadBytes("\n") หรือ .String() จะคืนค่าที่เป็นสำเนาจึงปลอดภัย แต่ .Bytes() กลับเสี่ยงอย่างในตัวอย่างนี้
    channel ของ Rust ป้องกันปัญหานี้โดยพื้นฐานผ่านแนวคิด ownership/transfer แต่ Go ไม่มี safety guard แบบนี้
    ผลลัพธ์คือมันอาจช้ากว่า mutex และให้ประสบการณ์ที่ยากกว่าสำหรับผู้เริ่มต้น Go ในการใช้งานให้ถูกต้อง
  • ในโปรแกรม golang จริง ๆ รูปแบบ “สื่อสารผ่านการแชร์” ก่อปัญหาเชิงตรรกะจำนวนมาก และสุดท้ายการแชร์หน่วยความจำก็กลายเป็นเรื่องปกติ
    กล่าวคือ race แบบ “ปลอดภัย” หรือ deadlock แบบ “ปลอดภัย” กลับพบได้บ่อยกว่า
  • การถกเรื่องบั๊ก concurrency มักมองข้ามประเด็นที่ว่าในแอปส่วนใหญ่ บั๊กสำคัญจำนวนมากเกิดจากการใช้ lock, transaction และ transaction isolation ภายใน DB ผิดพลาด
    ในเชิงทฤษฎีภาษา แนวทาง race freedom ของ Rust อาจดูน่าสนใจ แต่ในแอปจริงข้อมูลสำคัญมักอยู่ใน RDBMS ทั้งหมด และถ้าไม่ใช้ FOR UPDATE กับ SELECT ก็ยังเกิด race ได้อยู่ดี
    ต่อให้แอป Rust ไม่ใช้ unsafe เลย race ก็ยังมีอยู่ตามพฤติกรรมของ DB
  • คำว่า “memory safety” เดิมเกิดขึ้นเพื่ออธิบายแนวคิดที่ซับซ้อน แต่เมื่อเวลาผ่านไปความหมายก็ขยายหรือแคบลง
    Go มีโครงสร้างที่แทบไม่ยอมให้เกิดบั๊ก memory corruption ซึ่งเราพอเห็นได้จากการแทบไม่มี exploit จริง
    ถ้าใช้ตรรกะตามบทความนี้ ภาษาระดับสูงส่วนใหญ่ (ยกเว้น Java ที่บทความเหมือนจะยกเว้นไว้) ก็จะกลายเป็นไม่ memory-safe ด้วย
    Rust อาจ “ปลอดภัยกว่า” Go ได้ แต่ “memory safety” ไม่ใช่สเปกตรัมต่อเนื่องในความหมาย pass/fail
    ถ้าจะอ้างว่าภาษาหนึ่ง memory-unsafe ก็ควรต้องแสดง POC ให้ชัดเจน
  • ถ้าส่วนสำคัญของคำว่า memory safety คือเรื่อง “type confusion” Go ก็ไม่ได้เป็นข้อยกเว้น
    ตัวอย่างในบทความแสดงให้เห็นว่า memory corruption สามารถเกิดขึ้นได้ง่ายเพียงเพราะตีความ int เป็น pointer ผิด
    ในเดโมจงใจใช้ค่า 42 เลยเกิด segfault แต่ถ้าใช้ค่าที่เป็น address จริงก็จะเกิด corruption จริง
  • data race ถือเป็นการละเมิด memory safety เพราะมันสามารถทำให้โปรแกรมเข้าสู่สถานะที่สเปกของภาษาไม่รับรู้ได้ (เช่นถูกบังคับจบด้วย SIGSEGV)
    ดังนั้นภาษาที่ปล่อยให้เกิด data race ได้ก็ไม่อาจเรียกว่า memory-safe ได้
  • อย่างที่บทความยกตัวอย่างไว้ torn read ของ fat pointer ผ่าน type confusion หรือ torn read ของ slice ที่นำไปสู่ out-of-bounds write ก็เกิดขึ้นได้จริง
    ในกรณีแบบนี้ก็ชวนให้สงสัยว่าจะยังเรียกว่า memory-safe ได้หรือไม่
  • การที่ศัพท์พัฒนาและเปลี่ยนความหมายเป็นเรื่องที่เกิดขึ้นบ่อยในคณิตศาสตร์และฟิสิกส์เช่นกัน
    เพื่อหลีกเลี่ยงปัญหานี้จึงมีการใช้ชื่อบุคคลกำกับ เช่น “Gaussian Curvature” หรือ “Riemann Integrals”
    ส่วนกรณีที่ “ความหมายดั้งเดิมยังคงอยู่แบบแคบ แต่ถูกขยายไปใช้ในความหมายกว้าง” ก็มีตัวอย่างอย่าง “Galois Group”
    memory safety ก็ไม่ใช่ข้อยกเว้นเช่นกัน
  • หากยึดตามนิยามของผู้เขียน ก็สงสัยว่าเหตุผลอะไรที่บอกว่า Java ไม่ memory-safe
    ขอให้ยกตัวอย่างที่เป็นรูปธรรม
  • ตัว Go เองก็ไม่มีนิยามอย่างเป็นทางการของ memory safety ที่ชัดเจน
    ใน FAQ อย่างการพูดถึง memory safetyหรือคำตอบเรื่อง unions มีการสื่อเป็นนัยว่า Go เป็น memory-safe แต่ไม่ได้ชัดว่าหมายถึงอะไร
    ในงานนำเสนอปี 2012 ของ Rob Pike ก็มีคำว่า "Not purely memory safe" แต่แม้แต่คำว่า 'purely' ก็ยังไม่ได้ถูกนิยาม
    เอกสาร race detector ของ Go เองก็มีคำว่า "safe" ที่ความหมายไม่ชัด(ตัวอย่างเอกสาร)
    ฝั่งภายนอกกลับยิ่งมีคนอธิบาย Go อย่างหนักแน่นว่าเป็น “memory-safe programming language” บ่อยมาก
    ตัวอย่างเช่นเอกสารความปลอดภัยของ fly.io หรือเอกสารของ memorysafety.org ที่จัด Go ว่า memory safe
    แต่ในเอกสารเดียวกันนั้นก็อธิบายว่า “Out of Bounds Reads and Writes” เป็นปัญหา memory safety เช่นกัน ซึ่งข้อผิดพลาดของ Go ที่โพสต์นี้ชี้ก็เข้าข่ายเงื่อนไขดังกล่าว
    อย่างน้อยที่สุดก็รู้สึกว่า Go และชุมชนควรทำให้ความหมายที่แม่นยำของ “memory safety” ชัดเจนไว้
    ตราบใดที่ยังมีกรณีแบบนี้อยู่ ก็คงเหมาะกว่าที่จะไม่เรียก Go ว่าเป็นภาษา memory-safe โดยไม่อธิบายเพิ่มเติม
  • นิยามของ memory safety เองก็เปลี่ยนไปบ้างตามยุคสมัย
    ตอนที่ Go ถูกสร้างขึ้นนั้น แนวคิดหลักยังเป็นประมาณว่า “ถ้ามี garbage collector ก็ถือว่า memory-safe” และเมื่อเทียบกับ C/C++ แล้วมันก็ปลอดภัยกว่ามาก