หนี้ทางเทคนิค: ไลบรารี Rust ของผม ตอนนี้กลายเป็น CDO
(lucumr.pocoo.org)- ในระบบนิเวศ Rust ทันทีที่ dependency ที่หยุดดูแลรักษาถูกขึ้นทะเบียนใน RUSTSEC แม้แต่ไลบรารีที่ไม่ได้มีปัญหาโดยตรงก็กลายเป็นหนี้ทางเทคนิคผ่านผู้ใช้และ CI
instaพึ่งพาyaml-rustซึ่งหลังจากผู้เขียนเดิมลดความสนใจลง ก็อยู่ในสภาพที่มี คำขอฟีเจอร์และบั๊ก สะสมอยู่- หลังจากถูกขึ้นทะเบียนใน RUSTSEC ทำให้ CI ของผู้ใช้ทั้งทางตรงและทางอ้อมล้มเหลว หากเปรียบเทียบกับการเงิน ก็เหมือนเกิด การลดอันดับเครดิต และ margin call
- ไลบรารีทดแทนหรือ fork ก็ไม่ใช่ทางออกที่ชัดเจน ดังนั้นแม้เปลี่ยน dependency ก็ยังเหลือภาระการดูแลรักษาและความเสี่ยงจาก dependency ใหม่
- ทางรับมือสุดท้ายคือการ vendoring โค้ด
yaml-rustเข้าไปในinstaและนำไปสู่คำวิจารณ์ว่าใกล้เคียงกับ CDO ที่ห่อหนี้ทางเทคนิคคุณภาพต่ำให้ดูเป็นAAA
กระบวนการที่ dependency yaml-rust ถูกเปิดเผยว่าเป็นหนี้ทางเทคนิค
instaพึ่งพาyaml-rustและyaml-rustอยู่ในสภาพที่ issue สะสมต่อเนื่องหลังจากผู้เขียนเดิมหมดความสนใจ- บางส่วนเป็นคำขอฟีเจอร์ และบางส่วนเป็นบั๊กจริง
- ผู้ดูแล
instaไม่ได้เจอปัญหานั้นโดยตรง แต่ในแง่ที่เป็น dependency ที่หยุดดูแลรักษา จึงถือเป็น หนี้ทางเทคนิค
- สถานการณ์เปลี่ยนไปเมื่อ
yaml-rustถูกนำเข้าสู่การอภิปรายเรื่องการเพิ่มลงในฐานข้อมูล RUSTSEC- RUSTSEC ทำหน้าที่เป็น สถาบันจัดอันดับเครดิต ในอุปมาเชิงการเงิน
- หลังจากขึ้นทะเบียน CI ของหลายโปรเจกต์ที่ใช้
yaml-rustโดยตรงหรือโดยอ้อมเริ่มล้มเหลวภายในไม่กี่นาที - เมื่อผู้ใช้ชี้ปัญหาการใช้
yaml-rustต่อผู้ดูแลinstaก็เท่ากับเกิด margin call ตามอุปมาเชิงการเงิน
ทางเลือกและการรับมือจริง
- การย้ายไปใช้ทางเลือกอื่นไม่ได้น่าดึงดูดนัก
- ทางเลือกหนึ่งเป็น fork ของ
yaml-rustมีผู้ดูแลเพียง 1 คน และเพิ่ม dependency อีก 3 ตัว - หนึ่งในนั้นได้รับอันดับ “B-” ไปแล้ว
- ตัวเลือกอื่นในระบบนิเวศได้ตัดสินใจเปลี่ยนค่าเริ่มต้นก่อนที่จะถูกชี้ปัญหา
- ทางเลือกหนึ่งเป็น fork ของ
- การ fork เองก็ไม่ใช่ทางแก้เชิงรากฐาน
- ไลบรารีที่ fork มาเองยังมีข้อเรียกร้องด้านการดูแลรักษาแบบเดียวกันตามมา
- หากไม่ตอบสนองต่อรายงานบั๊ก สุดท้ายก็จะถูกชี้ปัญหาเหมือน
yaml-rustเดิม - ดังนั้น fork อาจซื้อเวลาได้ แต่ไม่ได้ทำให้ปัญหาหายไป
- การรับมือจริงคือการ vendoring โดยรวมโค้ด
yaml-rustเข้าไปในinsta- ตอนนี้
instaกลายเป็นรูปแบบที่รวมโค้ดinstaกับyaml-rustเข้าด้วยกัน - โครงสร้างนี้เหมือนกับการยกระดับหนี้ทางเทคนิคคุณภาพต่ำให้เป็น
AAA - CDO ในชื่อเรื่องหมายถึงตราสารหนี้ที่มีสินทรัพย์ค้ำประกัน ซึ่งมีชื่อเสียในช่วงวิกฤตการเงินปี 2007
- ตอนนี้
- การประเมินสุดท้ายใกล้เคียงกับคำว่า “ไม่มีใครชนะ”
- โค้ดที่มีปัญหาไม่ได้หายไป เพียงแค่ย้ายตำแหน่งเข้ามาอยู่ภายใน
insta - แรงกดดันเมื่อมองเห็นเป็น dependency ภายนอกลดลง แต่ภาระการดูแลรักษาเองยังคงอยู่
- โค้ดที่มีปัญหาไม่ได้หายไป เพียงแค่ย้ายตำแหน่งเข้ามาอยู่ภายใน
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
พูดกันตรง ๆ คือ ผู้เขียน serde_yaml(https://lib.rs/crates/serde_yaml) ซึ่งเป็น YAML parser ที่ได้รับความนิยมมากที่สุด จู่ ๆ ก็ถอนตัวโดยไม่แจ้งล่วงหน้าหรือแต่งตั้งผู้ดูแลต่อ และทำเครื่องหมายว่า deprecated และ unmaintained
ไม่เหมือน left-pad เสียทีเดียว แพ็กเกจยังทำงานอยู่ และ crates.io ก็ไม่อนุญาตให้ลบอยู่แล้ว แต่แพ็กเกจนี้ถูกใช้โดย crate อื่น ๆ 4,000 ตัว
เครื่องมือตรวจสอบและอัปเดตอัตโนมัติจะเริ่มมองว่าการใช้ crate ที่ไม่มีผู้ดูแลเป็นปัญหา
ขณะเดียวกัน serde_yaml ก็ถูกทำเครื่องหมายว่า unmaintained เพื่อกันไม่ให้ผู้คนแห่ย้ายไปทางนั้น
อยากให้เครื่องมือตรวจสอบกับนโยบายบริษัทฉลาดพอจะแยกแยะกรณีแบบนี้ได้ แทนที่จะเตือนว่า “ไม่มีผู้ดูแล” เพียงเพราะไม่มี commit ใหม่ ๆ
เลยสงสัยว่ามีข้อมูลละเอียดกว่านี้ไหม
ตอนแรกผมไม่เข้าใจตัวย่อ CDO ที่โผล่มาโดยไม่มีคำอธิบาย แต่ในบทความใช้คำว่า collateralized หลายครั้ง น่าจะหมายถึง ตราสารหนี้ที่มีสินทรัพย์ค้ำประกัน (collateralized debt obligation)
https://en.wikipedia.org/wiki/Collateralized_debt_obligation
ตอนแรกนึกถึง chief data officer
โดยเฉพาะในปี 2008 มันเป็นเหมือนสิทธิ์ถือครองบางส่วนในสินเชื่อที่อยู่อาศัยหลาย ๆ ก้อน และเมื่อสินเชื่อที่อยู่อาศัยด้อยคุณภาพผิดนัดชำระ CDO ก็พังไปด้วย [1]
อย่างไรก็ตาม สิ่งที่ผู้เขียนคิดถึงน่าจะใกล้กับ ความเสี่ยงเชิงระบบ แบบ leftpad หรือเหตุขัดข้อง DNS แบบลูกโซ่มากกว่าการเปรียบเปรยบนฐานหนี้ เหตุผลใหญ่ที่ทำให้ปี 2008 วุ่นวายหนักก็เพราะปัญหาเชิงระบบ มากกว่าข้อเท็จจริงว่ามีการสร้างผลิตภัณฑ์จากหนี้เสียเอง [2]
ยังสื่อด้วยว่าสถาบันจัดอันดับที่ควรทำหน้าที่กำกับดูแลนั้นถูกเจาะไปแล้ว
ผมไม่อยากเถียงว่านี่เป็น “ชัยชนะ” หรือไม่ เพราะขึ้นอยู่กับนิยามของคำว่า “ชัยชนะ” มากเกินไป แต่ข้อดีก็มีชัดเจน เส้นทางโค้ดที่มีช่องโหว่ซึ่งไม่ถูก execute และไม่สามารถเข้าถึงได้จากไลบรารีภายนอก ตอนนี้กลายเป็นเส้นทางโค้ดที่ปลอดภัยแล้ว
แน่นอนว่ายังเป็นสภาพที่น่ากังวล แต่ก็ปลอดภัย
การ vendoring แบบนี้ยังมีข้อดีอีกอย่างด้วย หากไลบรารีของคุณมี test coverage ที่แข็งแรง คุณก็สามารถรันเครื่องมือ code coverage กับไลบรารีที่เพิ่งนำเข้ามาได้
การแก้ไลบรารีอาจทำได้ยาก แต่คุณก็อาจค่อย ๆ ลบส่วนที่โค้ดของตัวเองไม่ได้แตะต้องออกได้ค่อนข้างง่าย ขึ้นอยู่กับโครงสร้าง แต่ถ้าโค้ดที่มีช่องโหว่ถูกลบออกไปหมด นั่นก็เป็นประโยชน์ที่ชัดเจน และถ้าพบว่าจริง ๆ แล้วมีบางส่วนที่ใช้อยู่ ก็อาจเป็นประโยชน์ที่ชัดเจนยิ่งกว่า
กล่าวคือ การ fork ไลบรารีเพื่อดูแลแบบสาธารณะเป็นความรับผิดชอบก้อนใหญ่ แต่การ vendor เฉพาะส่วนที่ต้องใช้เข้ามาแล้วปรับแต่งเองนั้นเป็นภาระที่เล็กกว่ามาก แม้จะไม่ได้ตัดกิ่งจริง ๆ แค่ทำให้การทำเช่นนั้นง่ายขึ้นก็ถือเป็นความก้าวหน้าแล้ว
ข้อเสียก็มีชัดเจนเช่นกัน แต่ไม่ได้แย่ไปเสียทั้งหมด
แต่ถ้าไม่มีใครเฝ้าดู dependency ภายนอกนั้นอีกต่อไป เหตุผลนั้นก็หายไป และ dependency นั้นกลายเป็นภาระ
การที่ dependency ถูกทำเครื่องหมายว่า abandoned และเริ่มถูกจับในรายงานความปลอดภัยคือพฤติกรรมที่ต้องการ เพราะทำให้ตัดสินใจได้อย่างมีข้อมูลว่าจะ vendor มันหรือไม่ กล่าวคือจะตัดความเสี่ยงที่ “ผู้ไม่หวังดีแอบใส่อะไรบางอย่างเข้าไป” ออกไป หรือจะเลือกทางอื่น
อีกอย่างที่ดีคือระบบ build ของ Rust อนุญาตให้ทำสิ่งนี้ได้ง่าย
โดยปกติจะอนุญาตให้มีเวอร์ชันของ dependency นั้นเพียงเวอร์ชันเดียวทั่ว monorepo ขนาดใหญ่ทั้งหมดด้วย อาจเปลี่ยนไปแล้วหลังจากที่ผมออกมา
และ dependency ใน third_party แต่ละตัวจะมีผู้รับผิดชอบหรือ OWNERS ที่กำหนดไว้
สิ่งนี้ช่วยให้ทุกอย่างค่อนข้างเป็นระเบียบ
แต่ระเบียบวินัยที่ถูกบังคับแบบนี้อาจเหมาะกับองค์กรอย่าง Google ที่มีเวลาและเงินเพียงพอ และไม่ได้ถูกผูกกับปรัชญา “เดินหน้าให้เร็วและทำของพัง” ส่วนจะเหมาะกับสตาร์ทอัพแค่ไหนนั้นไม่แน่ใจ
ในแง่หนึ่ง ไลบรารีอื่น ๆ เหล่านั้นเหมือน fuzz test แบบมีทิศทางที่ให้การเรียกใช้งานเกือบสุ่ม จึงอาจเป็นกลยุทธ์การทดสอบที่น่าสนใจ
เคยเห็นแพตเทิร์นเดียวกันในระบบนิเวศ JS npm เช่นกัน
npm audit มักทำตัวเหมือนเด็กเลี้ยงแกะในเรื่องปัญหาความปลอดภัย และถ้าไลเซนส์อนุญาต การดึงโค้ดเข้ามาไว้ข้างในก็เป็นหนึ่งในวิธีที่เสถียรที่สุดที่จะไม่ถูกท่วมด้วยอิชชูปลอม ๆ จากผู้ใช้
หลายครั้งผู้ใช้ไม่เข้าใจบริบท หรือไม่สนใจเพราะนโยบายของนายจ้างถูกกำหนดขึ้นในที่ที่ห่างไกลจากความเป็นจริง
regex ที่ใช้ในบางส่วนของ codebase ของ dependency ทางอ้อมใน build pipeline ไม่ได้แปลว่าจะถูกนำไปใช้โจมตีแบบปฏิเสธการให้บริการได้จริง
“อิชชู” ของ dependency ทางอ้อมที่อยู่ลึก ๆ อาจน่ารำคาญเป็นพิเศษในการหลีกเลี่ยง เพราะโดยโครงสร้างแล้ว มักพิสูจน์ทางเทคนิคได้ยากว่า “เราไม่มีทางเข้า code path นั้นเลย จึงไม่ได้รับผลจากข้อบกพร่องนี้” หรือ “กรณีเดียวที่จะเข้า path นั้นคืออินพุตที่เชื่อถือได้ในสภาพแวดล้อมออฟไลน์”
มีสถานการณ์ที่ปัญหาแบบนี้สำคัญอยู่จริง เช่น เครื่องมือ build ที่ถูกเจาะแล้วฉีดโค้ดอันตรายเข้าไปในไลบรารีที่กำลัง build
แต่กรณีแบบนั้นพบได้น้อยมาก และถูกกลบอยู่ในคลื่นของ regex ที่อาจทำให้เกิดการปฏิเสธการให้บริการได้แต่จริง ๆ ไม่สำคัญ เพราะมันแค่ถูกเรียกตอน build เท่านั้น
พอบวกกับข้อเท็จจริงที่ว่าเครื่องมือ build ทั่วไปมีต้นไม้ของ dependency ทางอ้อมราว ๆ ห้าหมื่นล้านตัว ก็ยิ่งกลายเป็นงานหนักมาก
ผมคิดว่าเครื่องมือที่รายงานอิชชูเหล่านี้ควรแยกให้ออกระหว่าง “ถูก exploit ได้เมื่อ redistribute” กับ “ถูก exploit ได้เมื่อใช้ใน build pipeline”
ในประโยค “ตอนนี้หนี้เทคนิคคุณภาพต่ำกลายเป็นระดับ AAA ไปเสียอย่างนั้น” คำว่า “ไปเสียอย่างนั้น” น่าจะหมายความว่า มันไม่สมเหตุสมผลที่โค้ดชุดเดิมได้อันดับหนี้ที่ดีกว่าเดิมเพียงเพราะถูก vendoring
แต่นั่นเป็นการมองแค่มูลค่าของตัวโค้ดเอง และพลาดส่วนที่สำคัญที่สุดของ value proposition ทั้งหมด
เมื่อ maintainer นำโค้ดเข้ามาไว้ข้างใน ตอนนี้โค้ดนั้นก็กลายเป็นของ maintainer คนนั้น ถ้า maintainer ที่ยัง active vendoring โค้ดจากโปรเจกต์ที่ตายไปแล้ว ก็จะมี คนที่ยัง active ซึ่งสามารถตอบสนองต่ออิชชู รีวิว pull request และแก้บั๊กได้ ทำให้มูลค่าของโค้ดนั้นสูงขึ้น
ถ้าเปรียบอีกแบบ ก็เหมือนการส่งสัตว์เลี้ยงที่ถูกละเลยไปให้เจ้าของใหม่ ทำให้ได้รับการดูแลดีขึ้น สุขภาพดีขึ้น และมีชีวิตยืนยาวขึ้น มูลค่าจึงเพิ่มขึ้น
maintainer เองก็ต้องปรับตัวกับ codebase ขนาดใหญ่และไม่คุ้นเคย จึงจะเกิด กำแพงในการเข้าสู่โครงการ สำหรับการลงมือแก้หรือรีวิว
เป็นความคิดที่ออกนอกประเด็นเล็กน้อยและน่าจะถกเถียงกันได้ แต่ผมคิดว่าถ้า package manager แบบอิงซอร์สไม่รับรอง สิทธิ์ทางกฎหมาย ให้ registry เข้าควบคุมการดูแลแพ็กเกจสาธารณะโดยบังคับ ก็ยากที่จะหลีกเลี่ยงปัญหาร้าย ๆ ได้
เช่น การถูกปล่อยทิ้ง การเปลี่ยนแปลงโดยเจตนาร้าย การลบโดยเจตนาร้าย และการแอบอ้าง
ถ้าแพ็กเกจใดถูกมองว่าสำคัญพอต่อชุมชนที่ใหญ่กว่า ก็ควรมีวิธีดึงรายการแพ็กเกจใน registry ออกจากมือเจ้าของเดิมแล้วชี้ไปยัง fork แทน
แน่นอนว่ามาตรการแบบนี้ย่อมมีดราม่ามากมายตามมา แต่สามารถปกป้องผู้ใช้ปลายน้ำได้อย่างจริงจัง
ปัญหาจริงคือการดูแลโปรเจกต์โอเพนซอร์สต้องใช้เวลาและแรง และไม่ง่ายที่จะหาคนอื่นที่มีเวลาว่างมาทำ
มันยังไม่น่าดึงดูดกว่ารูปแบบลิขสิทธิ์ที่ contribution ทั้งหมดกลายเป็นของกลุ่มใดกลุ่มหนึ่งโดยอัตโนมัติด้วยซ้ำ
อาจพออ้างได้ว่า GitHub เป็นที่โฮสต์ซอฟต์แวร์เสรีและเชี่ยวชาญด้านการละเมิดลิขสิทธิ์ของซอฟต์แวร์นั้น แต่至少ตอนนี้ก็ยังไม่ได้พยายามยึด ownership ในนามของแพ็กเกจ
ในเมื่อพูดถึงชุมชน ก็ไม่ใช่ความสัมพันธ์แบบลูกค้า-ผู้จัดหา และแพ็กเกจส่วนใหญ่ก็สำคัญแค่พอที่จะถูกให้ใช้ฟรีเท่านั้น
มันอาจกลายเป็นจำนวนเงินที่น่าอับอายอย่าง “ให้เดือนละ 10 ดอลลาร์ ช่วยดูแลแพ็กเกจ 100,000 ตัวให้เราหน่อย” ก็ได้
บนอินเทอร์เน็ตมีซอร์สโค้ดที่ไม่มีคนดูแลอยู่มากมาย และมันก็แค่ถูกเผยแพร่ไว้ครั้งหนึ่งตามสภาพเท่านั้น ไม่มีใครได้รับการรับประกันว่าจะมีอัปเดตสำหรับโค้ดฟรี
ถือว่าค่อนข้างโชคดีที่มีคน fork yaml-rust แล้วทำเป็น yaml-rust2(https://github.com/Ethiraric/yaml-rust2/blob/master/document...) ไว้แล้ว
น่าดีใจด้วยที่ fork นั้นผ่าน YAML test suite ทั้งหมด และยังเร็วกว่าใน benchmark ด้วย การ migrate ก็ดูง่าย
สุดท้ายปัญหาก็ยังอยู่ เราพึ่งพางานของคนอื่นที่ตอนนี้ยินดีมอบแรงงานให้ฟรี แต่สิ่งนั้นอาจไม่ได้ดำเนินต่อไปตลอดกาล
ผมไม่รู้ว่ามีทางเลี่ยงอื่นไหม นอกจากตอบแทนเวลาและความพยายามของพวกเขา แล้วหวังว่าพวกเขาจะทำงานดี ๆ ต่อไป
“A pure rust YAML implementation.”
ในทางกลับกัน serde_yaml ถือว่าเป็น Rust ล้วนได้ยากกว่า เพราะพึ่งพา unsafe-libyaml ซึ่งแปลง libyaml มาด้วย c2rust
ถ้ามองในกรอบเวลาที่ยาวพอ ความน่าจะเป็นที่ maintainer โอเพนซอร์สจะเลิกทำโปรเจกต์คือ 1
วิธีเดียวที่จะหลีกเลี่ยงสิ่งนี้ได้คือทำให้โปรเจกต์ที่มีผู้ดูแลคนเดียวกลายเป็นเรื่องต้องห้าม
สำหรับโปรเจกต์ Rust อาจไม่ใช่ตัวเลือก เพราะคนที่ทำ standard library ของ Rust ต้องการให้ standard library คงความบางเบาไว้
แต่เมื่อใช้ภาษาอื่นที่มี standard library ซึ่งมีฟังก์ชันเพียงพอ นี่เป็นตัวเลือกที่เป็นไปได้แน่นอน
เพราะผมตั้งใจใช้ภาษาที่มีสิ่งจำเป็น built-in อยู่แล้ว เลยแทบไม่ใช้ไลบรารีภายนอกนอกจาก database driver
สถานการณ์ทั้งหมดนี้ดูค่อนข้างตลก ถ้าโค้ดมันทำงานได้ และเป็นแบบนั้นมาหลายปีแล้ว ก็ไม่รู้ว่าการที่มันไม่มีคนดูแลจะเป็นปัญหาตรงไหน
ถ้าไม่จำเป็นต้องแก้ไข และรู้ข้อจำกัดกับความสามารถของมันอยู่แล้ว ก็โอเค
โค้ดไม่ได้เสียเองไปตามกาลเวลา ผมเคยยืมหรือผสานโค้ดเมื่อหลายสิบปีก่อนมาใช้ได้ดีหลายครั้ง
ส่วนตัวแล้วคงจะมองข้ามคำบ่นทั้งหมดเกี่ยวกับไลบรารีนั้นไปเลย
มีบั๊กสะสมอยู่มากมายแล้ว และแม้จะมีแพตช์แก้ไขอยู่ ก็จะไม่มีวันถูกนำไปแพตช์ ดูได้ที่ https://github.com/chyh1990/yaml-rust/issues และ /pulls
ก็เหมือนกับรับภาระดูแลชิ้นส่วนโค้ดที่ตัวเองใช้อยู่เอง
บางครั้งก็อาจไม่ดี เพราะอาจกลายเป็นการเขียนโค้ดแบบคัดลอก-วาง หรือทำงานซ้ำซ้อน
แต่ถ้าคอมโพเนนต์จากบุคคลที่สามไม่มีคนดูแลโดยสิ้นเชิง ก็ถือว่าเข้าใจได้อยู่
ใช่แล้ว dependency ก็ vendor ได้ dependency ที่ใกล้เคียงกับ “เกือบเสร็จสมบูรณ์” และการพัฒนากับการดูแลชะลอตัวลง โดยทั่วไปเราก็ทำแบบนั้นกันมาตลอด 20 ปี
เพียงแต่ผมไม่เคยทำงานกับภาษาที่เป็นแบบ “batteries are not included”
จะมีวิธีทำอะไรอย่าง
cargo vendor --aggressiveเพื่อ prune โค้ดที่ตายแล้ว ทั้งหมดใน dependency โดยอิงจาก crate ของผมได้ไหม?สงสัยว่าจะช่วยให้ปัญหาเรื่อง “รีวิว dependency” จัดการได้ง่ายขึ้นหรือเปล่า
แม้จะออกนอกประเด็นหลักของบทความ แต่ก็เกี่ยวข้องกันในแง่ที่ท้ายที่สุดแล้ว การเลือก dependency และความรับผิดชอบทั้งหมดที่ตามมาก็เป็นของเรา
ดูเหมือนว่ายังมีที่ว่างสำหรับเครื่องมือที่จะช่วยให้เรารับผิดชอบต่อสิ่งที่ถูกคอมไพล์เข้าไปใน crate จริง ๆ ได้มากขึ้น
ถ้าไม่ทำแบบนั้น ก็ไม่เห็นว่ามันดีขึ้นตรงไหน