Technische Schulden: Meine Rust-Bibliothek wird jetzt zum CDO
(lucumr.pocoo.org)- Im Rust-Ökosystem wird in dem Moment, in dem eine nicht mehr gewartete Dependency in RUSTSEC auftaucht, auch eine Bibliothek ohne direktes Problem über Nutzer und CI zur technischen Schuld
- Bei
yaml-rust, von deminstaabhing, hatten sich Feature Requests und Bugs angesammelt, nachdem das Interesse des ursprünglichen Autors nachgelassen hatte - Nach dem Eintrag in RUSTSEC schlugen die CIs direkter und indirekter Nutzer fehl; in der Finanzmetapher entspricht das einer Herabstufung und einem Margin Call
- Alternative Bibliotheken oder Forks waren ebenfalls keine klare Lösung: Auch nach einem Dependency-Wechsel bleiben Wartungsaufwand und Risiken neuer Dependencies bestehen
- Die endgültige Reaktion bestand darin, den Code von
yaml-rustininstazu vendorn; das führte zur Kritik, dies komme einem CDO nahe, bei dem faule technische Schulden alsAAAverpackt werden
Wie sich die Dependency yaml-rust als technische Schuld herausstellte
instahing vonyaml-rustab, und beiyaml-rusthatten sich weiter Issues angesammelt, nachdem der ursprüngliche Autor das Interesse verloren hatte- Einige davon waren Feature Requests, andere echte Bugs
- Der Maintainer von
instawar von diesen Problemen nicht direkt betroffen, doch als nicht mehr gewartete Dependency war es technische Schuld
- Als
yaml-rustin eine Diskussion über die Aufnahme in die RUSTSEC-Datenbank geriet, änderte sich die Lage- RUSTSEC spielt in der Finanzmetapher die Rolle einer Ratingagentur
- Nach der Aufnahme begannen innerhalb weniger Minuten die CIs vieler Projekte fehlzuschlagen, die
yaml-rustdirekt oder indirekt nutzen - Als Nutzer den Maintainer von
instaauf das Problem mit der Nutzung vonyaml-rusthinwiesen, war das in der Finanzmetapher gewissermaßen ein Margin Call
Optionen und tatsächliche Reaktion
- Ein Umstieg auf eine Alternative war nicht attraktiv
- Eine Alternative ist ein Fork von
yaml-rust, hat nur einen Maintainer und fügt drei Dependencies hinzu - Eine davon hatte bereits ein „B-“-Rating erhalten
- Eine andere Option im Ökosystem hatte, bevor sie kritisiert wurde, eine Entscheidung zur Änderung von Standardwerten getroffen
- Eine Alternative ist ein Fork von
- Selbst zu forken war ebenfalls keine grundsätzliche Lösung
- Eine geforkte Bibliothek bringt denselben Wartungsbedarf mit sich
- Wenn man auf Bugreports nicht reagiert, wird sie am Ende genauso kritisiert wie das ursprüngliche
yaml-rust - Ein Fork kann daher Zeit verschaffen, beseitigt das Problem aber nicht
- Die tatsächliche Reaktion war das Vendoring, also das Zusammenführen des Codes von
yaml-rustininstainstabesteht nun aus einer Kombination voninsta-Code undyaml-rust- Das ist eine Struktur, als würde man faule technische Schulden auf
AAAhochstufen - Das CDO im Titel verweist auf die während der Finanzkrise 2007 berüchtigt gewordene Collateralized Debt Obligation
- Die abschließende Bewertung kommt „Niemand hat gewonnen“ nahe
- Der problematische Code ist nicht verschwunden, sondern nur ins Innere von
instaverschoben worden - Der Druck, der entstand, solange er als externe Dependency sichtbar war, ist geringer geworden, doch der Wartungsaufwand selbst bleibt bestehen
- Der problematische Code ist nicht verschwunden, sondern nur ins Innere von
1 Kommentare
Meinungen auf Hacker News
Kurz gesagt: Der Autor von serde_yaml(https://lib.rs/crates/serde_yaml), dem beliebtesten YAML-Parser, hat sich plötzlich ohne Vorankündigung und ohne Benennung eines Nachfolgers zurückgezogen und das Paket als deprecated und unmaintained markiert.
Es ist nicht ganz dasselbe wie left-pad. Das Paket funktioniert weiterhin, und crates.io erlaubt auch kein Löschen, aber dieses Paket wird von 4.000 anderen Crates genutzt.
Audit- und Auto-Update-Tools werden die Nutzung eines nicht mehr gepflegten Crates als Problem melden.
Gleichzeitig wurde serde_yaml ebenfalls als unmaintained markiert, um zu verhindern, dass massenhaft Leute dorthin wechseln.
Ich wünschte, Audit-Tools und Unternehmensrichtlinien wären klug genug, solche Fälle zu unterscheiden, statt nur wegen fehlender neuer Commits ständig „unmaintained“ zu melden.
Ich frage mich, ob es dazu nähere Informationen gibt.
Die Abkürzung CDO war ohne Erklärung nicht sofort verständlich, aber da im Artikel mehrfach das Wort collateralized vorkommt, dürfte Collateralized Debt Obligation gemeint sein.
https://en.wikipedia.org/wiki/Collateralized_debt_obligation
Zuerst dachte ich an Chief Data Officer.
Konkret waren es 2008 so etwas wie Teil-Eigentumsrechte an mehreren Hypothekendarlehen, und als faule Hypotheken ausfielen, gingen die CDOs gleich mit kaputt [1].
Was der Autor im Sinn hat, ist jedenfalls weniger eine schuldenbasierte Analogie als eher ein Systemrisiko wie leftpad oder kaskadierende DNS-Ausfälle. Ein wesentlicher Grund, warum 2008 so ein großes Chaos entstand, waren auch eher systemische Probleme als die bloße Tatsache, dass man aus Schulden Produkte gemacht hatte [2].
Es bedeutet auch, dass die Ratingagenturen, die eigentlich regulierend wirken sollten, bereits unterwandert wurden.
Ob das ein „Sieg“ ist, hängt zu stark von der Definition von „Sieg“ ab, als dass ich darüber streiten möchte, aber es gibt eindeutig Vorteile. Verwundbare Codepfade, die nicht ausgeführt werden und von externen Bibliotheken aus auch nicht erreichbar sind, werden nun zu sicheren Codepfaden.
Natürlich ist das trotzdem ein beunruhigender Zustand, aber sicher ist er.
Diese Art von Vendoring hat noch einen weiteren Vorteil. Wenn die eigene Bibliothek eine solide Testabdeckung hat, kann man Code-Coverage-Tools über die neu hereingeholte Bibliothek laufen lassen.
Änderungen an der Bibliothek können schwierig sein, aber Teile, die der eigene Code nicht berührt, lassen sich vergleichsweise leicht nach und nach entfernen. Je nach Struktur ist es ein klarer Gewinn, wenn der verwundbare Code vollständig gelöscht wird; und selbst wenn man entdeckt, dass man tatsächlich Teile davon genutzt hat, kann das ein noch deutlicherer Gewinn sein.
Eine Bibliothek für die öffentliche Wartung zu forken ist also eine große Verantwortung, aber nur die benötigten Teile zu vendoren und anzupassen, ist eine viel kleinere Belastung. Selbst wenn man nicht tatsächlich beschneidet, ist schon die Tatsache, dass es dadurch einfacher wird, ein Fortschritt.
Es gibt eindeutig auch Nachteile, aber es ist nicht alles schlecht.
Wenn aber niemand mehr eine externe Abhängigkeit beobachtet, fällt dieses Argument weg, und die Abhängigkeit wird zur Last.
Dass eine Abhängigkeit als abandoned markiert wird und in Sicherheitsberichten auftaucht, ist das gewünschte Verhalten. Dadurch kann man informiert entscheiden, ob man sie vendort, also das Risiko entfernt, dass „ein böswilliger Akteur heimlich etwas einschleust“, oder ob man eine andere Wahl trifft.
Gut ist auch, dass das Rust-Build-System dies leicht ermöglicht.
Normalerweise ist im gesamten riesigen Monorepo auch nur eine einzige Version dieser Abhängigkeit erlaubt. Das kann sich geändert haben, seit ich dort war.
Außerdem gibt es für jede third_party-Abhängigkeit benannte Verantwortliche oder OWNERS.
Dadurch bleibt es vergleichsweise geordnet.
Allerdings passt eine solche erzwungene Disziplin wohl gut zu Organisationen wie Google, die genug Zeit und Geld haben und nicht an die „move fast and break things“-Philosophie gebunden sind. Wie gut das bei Startups passt, weiß ich nicht.
In gewisser Weise liefern andere Bibliotheken nahezu zufällige Aufrufe als gerichteter Fuzz-Test, was eine interessante Teststrategie sein könnte.
Dasselbe Muster habe ich auch im JS-npm-Ökosystem gesehen.
npm audit verhält sich bei Sicherheitsproblemen meistens wie der Junge, der immer „Wolf“ ruft, und wenn die Lizenz es erlaubt, ist es eine der stabilsten Methoden, den Code ins eigene Projekt zu holen, um nicht von falschen Issues der Nutzer überwältigt zu werden.
Oft verstehen die Nutzer den Kontext nicht, oder es ist ihnen egal, weil die Richtlinien ihres Arbeitgebers an einem Ort gemacht wurden, der weit von der Realität entfernt ist.
Eine Regex, die irgendwo in einem transitiven Abhängigkeits-Codebase-Teil der Build-Pipeline verwendet wird, lässt sich nicht tatsächlich für einen Denial-of-Service-Angriff auf den Dienst ausnutzen.
„Issues“ in tiefen transitiven Abhängigkeiten können besonders lästig zu umgehen sein. Strukturell ist es oft schwer, technisch nachzuweisen, dass „wir diesen Codepfad nie betreten und daher von dem Fehler nicht betroffen sind“ oder „der einzige Fall, in dem dieser Pfad betreten wird, sind vertrauenswürdige Eingaben in einer Offline-Umgebung“.
Es gibt durchaus Szenarien, in denen solche Probleme wichtig sind. Zum Beispiel, wenn ein kompromittiertes Build-Tool während des Builds bösartigen Code in die gerade gebaute Library einschleust.
Solche Fälle sind aber extrem selten und gehen in einer Welle von potenziell DoS-fähigen Regexes unter, die in Wirklichkeit keine Rolle spielen, weil sie nur im Build aufgerufen werden.
Wenn man dann noch berücksichtigt, dass ein typisches Build-Tool einen Baum mit gefühlt 50 Milliarden transitiven Abhängigkeiten hat, wird es wirklich mühsam.
Tools, die solche Issues melden, sollten meiner Meinung nach zwischen „ausnutzbar, wenn weiterverteilt“ und „ausnutzbar, wenn in der Build-Pipeline verwendet“ unterscheiden.
Mit „jetzt ist aus minderwertiger technischer Schuld plötzlich ein AAA-Rating geworden“ scheint „plötzlich“ zu bedeuten, dass es keinen Sinn ergibt, wenn derselbe Code ein besseres Schulden-Rating bekommt als vorher, nur weil er vendort wurde.
Aber das betrachtet nur den Wert des Codes selbst und übersieht den wichtigsten Teil des gesamten Wertversprechens.
Wenn ein Maintainer den Code ins Projekt holt, gehört dieser Code nun diesem Maintainer. Wenn ein aktiver Maintainer den Code eines toten Projekts vendort, gibt es plötzlich eine aktive Person, die auf Issues reagieren, Pull Requests prüfen und Bugs beheben kann; dadurch steigt der Wert dieses Codes.
Eine andere Analogie: Wenn ein vernachlässigtes Haustier zu einem neuen Besitzer kommt, steigt sein Wert, weil es besser versorgt wird, gesünder wird und länger leben kann.
Auch der Maintainer muss sich erst in eine große, fremde Codebase einarbeiten, wodurch eine Einstiegshürde für Implementierung und Review von Änderungen entsteht.
Ein etwas abseitiger und kontroverser Gedanke: Wenn ein quellbasierter Paketmanager dem Registry-Betreiber nicht das rechtliche Recht zusichert, die Wartung eines veröffentlichten Pakets zwangsweise zu übernehmen, lassen sich meiner Ansicht nach schlimme Probleme kaum vermeiden.
Gemeint sind Probleme wie Vernachlässigung, bösartige Änderungen, bösartiges Entfernen oder Impersonation.
Wenn ein Paket als wichtig genug für die größere Community angesehen wird, braucht es eine Möglichkeit, den Paket-Registry-Eintrag aus den Händen des ursprünglichen Besitzers zu nehmen und auf einen Fork zeigen zu lassen.
Natürlich würde so ein Schritt viel Drama verursachen, könnte aber nachgelagerte Nutzer aktiv schützen.
Das eigentliche Problem ist, dass die Wartung eines Open-Source-Projekts Zeit und Mühe kostet und es nicht leicht ist, andere Leute mit freier Zeit dafür zu finden.
Das ist noch weniger attraktiv als ein Copyright-Modell, bei dem alle Beiträge automatisch in den Besitz einer bestimmten Gruppe übergehen.
Man könnte zwar argumentieren, dass GitHub beim Hosting freier Software ein Ort ist, der auf Urheberrechtsverletzungen an dieser Software spezialisiert ist, aber zumindest versucht GitHub derzeit nicht, die nominelle Ownership von Paketen zu übernehmen.
Da es um Community geht, handelt es sich nicht um eine Kunden-Lieferanten-Beziehung, und die meisten Pakete sind nur wichtig genug, um kostenlos bereitgestellt zu werden.
Es könnte am Ende auch auf eine erniedrigende Summe hinauslaufen wie: „Wir zahlen 10 Dollar im Monat, bitte verwalte dafür 100.000 Pakete für uns.“
Im Internet gibt es viel nicht mehr gepflegten Quellcode, der einfach einmal so veröffentlicht wurde. Niemand bekommt für kostenlosen Code Updates garantiert.
Es war ziemlich glücklich, dass jemand yaml-rust bereits geforkt und yaml-rust2(https://github.com/Ethiraric/yaml-rust2/blob/master/document...) erstellt hat.
Schön ist auch, dass dieser Fork die YAML-Testsuite vollständig besteht und in Benchmarks schneller ist. Die Migration scheint ebenfalls einfach zu sein.
Am Ende bleibt das Problem bestehen: Wir verlassen uns derzeit auf die Arbeit anderer Leute, die bereitwillig kostenlose Arbeit leisten, aber das wird vielleicht nicht ewig so weitergehen.
Ich weiß nicht, ob es einen Ausweg gibt, außer ihre Zeit und Mühe zu vergüten und zu hoffen, dass sie weiter gute Arbeit leisten.
„A pure rust YAML implementation.“
Im Gegenteil war serde_yaml schwerer als reines Rust zu betrachten, weil es von unsafe-libyaml abhing, einer per c2rust konvertierten Version von libyaml.
Über einen ausreichend langen Zeitraum ist die Wahrscheinlichkeit 1, dass ein Open-Source-Maintainer ein Projekt aufgibt.
Die einzige Möglichkeit, das zu vermeiden, besteht darin, Ein-Personen-Maintenance-Projekte zu tabuisieren.
Für Rust-Projekte ist das vielleicht keine Option, weil die Leute hinter der Rust-Standardbibliothek diese bewusst schlank halten wollen.
Wenn man aber andere Sprachen mit ausreichend umfangreicher Standardbibliothek verwendet, ist es definitiv eine mögliche Option.
Weil ich bewusst Sprachen nutze, die die benötigten Dinge eingebaut haben, verwende ich abgesehen von Datenbanktreibern kaum externe Libraries.
Diese ganze Situation wirkt irgendwie grotesk. Wenn der Code funktioniert und das seit Jahren getan hat, verstehe ich nicht, warum es ein Problem sein soll, dass er nicht gewartet wird.
Wenn nichts geändert werden muss und man seine Grenzen und Funktionen kennt, ist das in Ordnung.
Code verdirbt nicht von selbst. Ich habe schon mehrfach Jahrzehnte alten Code übernommen oder integriert und gut damit gearbeitet.
Persönlich würde ich wahrscheinlich alle Beschwerden über diese Library einfach ignorieren und weitermachen.
Es haben sich bereits viele Bugs angesammelt, und obwohl es Fixes gibt, werden sie nie gepatcht werden. Siehe https://github.com/chyh1990/yaml-rust/issues und /pulls.
Das ist im Grunde dasselbe, als würde man die Wartung des Code-Teils übernehmen, den man nutzt.
Manchmal kann das schlecht sein, weil es Copy-and-paste-Coding oder doppelte Arbeit bedeuten kann.
Aber wenn eine Third-Party-Komponente komplett ungewartet ist, ist das ziemlich nachvollziehbar.
Stimmt, Dependencies kann man vendorn. Bei Dependencies, die „fast fertig“ sind und deren Entwicklung und Wartung langsamer geworden sind, haben wir das in den letzten 20 Jahren meist so gemacht.
Allerdings habe ich noch nie in einer Sprache gearbeitet, bei der „batteries are not included“ gilt.
Könnte man so etwas wie
cargo vendor --aggressivebauen, um ausgehend von meiner Crate sämtlichen toten Code in den Dependencies wegzuschneiden?Ich frage mich, ob sich das Problem „reviewt eure Dependencies“ dadurch handhabbarer machen ließe.
Das geht zwar am Kern des Artikels vorbei, hängt aber insofern damit zusammen, als die Wahl der Dependencies und alle damit verbundenen Verantwortlichkeiten letztlich bei uns liegen.
Es scheint Raum für Tools zu geben, die dabei helfen, mehr Verantwortung für das zu übernehmen, was tatsächlich in die Crate hinein kompiliert wird.
Wenn nicht, sehe ich nicht, was sich dadurch verbessert.