Deuda técnica: mi biblioteca de Rust, ahora convertida en un CDO
(lucumr.pocoo.org)- En el ecosistema de Rust, en cuanto una dependencia sin mantenimiento aparece en RUSTSEC, incluso una biblioteca que no tenía problemas directos se convierte en deuda técnica a través de sus usuarios y CI
instadependía deyaml-rust, que había acumulado solicitudes de funcionalidades y bugs después de que disminuyera el interés de su autor original- Tras su inclusión en RUSTSEC, el CI de usuarios directos e indirectos empezó a fallar; usando una analogía financiera, fue como una rebaja de calificación y una margin call
- Ni una biblioteca alternativa ni un fork eran una solución clara: aunque se cambiara la dependencia, seguirían existiendo la carga de mantenimiento y el riesgo de nuevas dependencias
- La respuesta final fue vendorizar el código de
yaml-rustdentro deinsta, lo que llevó a críticas de que era parecido a empaquetar deuda técnica de mala calidad como un CDO con calificaciónAAA
Cómo la dependencia yaml-rust se reveló como deuda técnica
instadependía deyaml-rust, yyaml-rusttenía issues acumulándose después de que su autor original perdiera interés- Algunos eran solicitudes de funcionalidades y otros eran bugs reales
- El mantenedor de
instano sufría esos problemas directamente, pero el hecho de que fuera una dependencia sin mantenimiento la convertía en deuda técnica
- La situación cambió cuando
yaml-rustentró en una discusión para ser agregado a la base de datos RUSTSEC- En la analogía financiera, RUSTSEC cumple el papel de agencia calificadora
- Después de su registro, el CI de muchos proyectos que usaban
yaml-rustdirecta o indirectamente empezó a fallar en cuestión de minutos - Cuando los usuarios empezaron a señalarle al mantenedor de
instael problema de usaryaml-rust, en términos de la analogía financiera se produjo una margin call
Opciones y respuesta real
- Cambiarse a una alternativa no resultaba atractivo
- Una alternativa era un fork de
yaml-rust, tenía un solo mantenedor y agregaba 3 dependencias - Una de ellas ya tenía una calificación “B-”
- Otra opción del ecosistema tomó la decisión de cambiar el valor predeterminado antes de ser señalada
- Una alternativa era un fork de
- Hacer un fork propio tampoco era una solución de fondo
- Una biblioteca forkeada trae consigo las mismas exigencias de mantenimiento
- Si no se responde a los reportes de bugs, tarde o temprano terminará siendo señalada igual que el
yaml-rustoriginal - Por lo tanto, un fork puede ganar tiempo, pero no elimina el problema
- La respuesta real fue vendorizar el código de
yaml-rustdentro deinstainstaahora queda como una combinación del código deinstayyaml-rust- Es una estructura parecida a elevar deuda técnica de mala calidad a
AAA - El CDO del título se refiere a las obligaciones de deuda colateralizada, que se volvieron tristemente célebres durante la crisis financiera de 2007
- La evaluación final se acerca a “nadie ganó”
- El código problemático no desapareció; solo se movió dentro de
insta - La presión de verlo como una dependencia externa disminuyó, pero la carga de mantenimiento en sí permanece
- El código problemático no desapareció; solo se movió dentro de
1 comentarios
Opiniones en Hacker News
Dicho sin rodeos, el autor de serde_yaml(https://lib.rs/crates/serde_yaml), el parser de YAML más popular, de pronto se retiró sin aviso ni designar a un mantenedor sucesor, y lo marcó como deprecated y unmaintained.
No es exactamente lo mismo que left-pad. El paquete sigue funcionando y crates.io ni siquiera permite borrarlo, pero este paquete se usa en otros 4,000 crates.
Las herramientas de auditoría y actualización automática van a empezar a marcar como problema el uso de crates sin mantenimiento.
Al mismo tiempo, serde_yaml también fue marcado como unmaintained para evitar que la gente migre masivamente hacia allá.
Ojalá las herramientas de auditoría y las políticas de empresa fueran lo bastante inteligentes para distinguir estos casos, en vez de seguir alertando “sin mantenimiento” solo porque no hay commits recientes.
Me pregunto si hay más información.
La sigla CDO, usada sin explicación, no me resultó obvia de inmediato, pero como en el texto aparece varias veces “collateralized”, supongo que se refiere a obligación de deuda colateralizada (collateralized debt obligation).
https://en.wikipedia.org/wiki/Collateralized_debt_obligation
Al principio pensé en chief data officer.
En concreto, en 2008 eran algo así como participaciones parciales sobre varias hipotecas, y cuando las hipotecas basura entraron en default, los CDO también se rompieron [1].
En cualquier caso, lo que el autor tiene en mente no es tanto una analogía basada en deuda, sino algo más parecido a riesgo sistémico, como leftpad o fallas en cadena de DNS. Una razón importante por la que 2008 fue un caos tan grande también tuvo más que ver con problemas sistémicos que con el hecho en sí de crear productos a partir de deuda [2].
También implica que las agencias de calificación que deberían actuar como reguladores ya fueron capturadas.
No quiero discutir si esto es una “victoria”, porque depende demasiado de cómo se defina “victoria”, pero definitivamente tiene ventajas. Una ruta de código vulnerable que ni siquiera se ejecuta y a la que no se puede llegar desde una biblioteca externa ahora se convierte en una ruta de código segura.
Claro, sigue siendo una situación inquietante, pero es segura.
Este tipo de vendorización también tiene otra ventaja. Si tu biblioteca tiene una cobertura de pruebas sólida, puedes correr herramientas de cobertura de código sobre la biblioteca que acabas de incorporar.
Modificar una biblioteca puede ser difícil, pero también puedes ir eliminando con relativa facilidad las partes que tu propio código no toca. Depende de la estructura, pero si todo el código vulnerable se elimina, es una ganancia clara; y si descubres que en realidad estabas usando parte de él, puede ser una ganancia todavía más evidente.
Es decir, hacer un fork de una biblioteca para mantenerla públicamente es una gran responsabilidad, pero vendorizar solo las partes que necesitas y ajustarlas es una carga mucho menor. Incluso si no haces esa poda en la práctica, el solo hecho de facilitarla ya es un avance.
Sin duda hay desventajas, pero no todo es malo.
Pero si ya no hay nadie vigilando una dependencia externa, ese argumento desaparece, y esa dependencia se vuelve una carga.
Que una dependencia se marque como abandoned y empiece a aparecer en reportes de seguridad es el comportamiento deseado. Gracias a eso puedes decidir con información si vendorizarla, es decir, eliminar el riesgo de que “un actor malicioso meta algo a escondidas”, o tomar otra opción.
También es bueno que el sistema de build de Rust permita hacer esto fácilmente.
Normalmente también se permite una sola versión de esa dependencia en todo el enorme monorepo. Puede que eso haya cambiado desde que estuve ahí.
Y cada dependencia en third_party tiene responsables u OWNERS designados.
Eso ayuda a mantener bastante orden.
Dicho eso, esta disciplina impuesta puede encajar bien en una organización como Google, con suficiente tiempo y dinero y no atada a la filosofía de “muévete rápido y rompe cosas”. No sé qué tan bien funcionaría en una startup.
En cierto sentido, como otras bibliotecas aportan llamadas casi aleatorias, podría ser una estrategia de pruebas interesante, parecida a un fuzz test direccional.
Vi el mismo patrón en el ecosistema de JS npm
npm audit suele comportarse como el pastorcito mentiroso con los problemas de seguridad, y si la licencia lo permite, traer el código hacia adentro es una de las formas más estables de no quedar abrumado por issues falsos de los usuarios
Muchas veces los usuarios no entienden el contexto, o no les importa porque las políticas de su empleador se crearon en un lugar desconectado de la realidad
Que una expresión regular usada en alguna parte del codebase de una dependencia transitiva del pipeline de build pueda explotarse realmente para un ataque de denegación de servicio no es el caso
Los “issues” en dependencias transitivas profundas pueden ser especialmente molestos de esquivar. Por la estructura, muchas veces es difícil demostrar técnicamente hechos como “nunca entramos en esa ruta de código, así que el defecto no nos afecta” o “la única vez que se entra en esa ruta es con input confiable en un entorno offline”
Sí existen escenarios donde estos problemas importan. Por ejemplo, cuando una herramienta de build comprometida inyecta código malicioso en la librería que se está compilando
Pero esos casos son extremadamente raros y quedan sepultados bajo una ola de expresiones regulares con potencial de denegación de servicio que en realidad no importan porque solo se invocan durante el build
Si a eso se suma que una herramienta de build típica tiene un árbol de unos 50 mil millones de dependencias transitivas, se vuelve un trabajo realmente pesado
Creo que las herramientas que reportan estos issues deberían distinguir entre “explotable si se redistribuye” y “explotable si se usa en el pipeline de build”
En “ahora la deuda técnica mala de repente recibió calificación AAA”, la expresión “de repente” parece querer decir que no tiene sentido que el mismo código reciba una mejor calificación de deuda solo porque fue vendorizado
Pero eso mira solo el valor del código en sí y se pierde la parte más importante de la propuesta de valor completa
Cuando un maintainer trae el código hacia adentro, ese código pasa a ser de su propiedad. Si un maintainer activo vendoriza código de un proyecto muerto, aparece una persona activa que puede responder a issues, revisar pull requests y corregir bugs, así que el valor de ese código aumenta
Como otra analogía, es como cuando una mascota abandonada pasa a un nuevo dueño: puede recibir mejores cuidados, estar más sana y vivir más tiempo, por lo que su valor sube
El maintainer también tendrá que adaptarse a un codebase grande y desconocido, así que habrá una barrera de entrada para implementar o revisar cambios
Es una idea un poco tangencial y polémica, pero creo que si un gestor de paquetes basado en código fuente no garantiza al registro el derecho legal de tomar por la fuerza el mantenimiento de un paquete publicado, es difícil evitar problemas terribles
Problemas como abandono, cambios maliciosos, eliminación maliciosa o suplantación
Si se determina que un paquete es lo suficientemente importante para una comunidad más amplia, hace falta una forma de quitar de manos del dueño original la entrada del registro del paquete y hacer que apunte a un fork
Obviamente una medida así vendría con mucho drama, pero podría proteger activamente a los usuarios aguas abajo
El problema real es que mantener un proyecto open source requiere tiempo y esfuerzo, y no es fácil encontrar a otra persona con tiempo libre
Es incluso menos atractivo que un modelo de copyright donde todas las contribuciones pasan automáticamente a ser propiedad de un grupo específico
Se podría argumentar que GitHub, al alojar software libre, es un lugar especializado en infringir el copyright de ese software, pero al menos por ahora no intenta quitar la propiedad nominal de los paquetes
Como estamos hablando de comunidad, no es una relación cliente-proveedor, y la mayoría de los paquetes solo son lo bastante importantes como para ofrecerse gratis
Incluso podría terminar siendo una cifra humillante, como “te damos 10 dólares al mes, así que mantén 100.000 paquetes para nosotros”
En internet hay mucho código fuente sin mantenimiento, simplemente publicado una vez tal cual. Nadie tiene garantizadas actualizaciones para código gratis
Fue bastante afortunado que alguien ya haya hecho un fork de yaml-rust y creado yaml-rust2(https://github.com/Ethiraric/yaml-rust2/blob/master/document...)
También es genial que ese fork pase por completo el test suite de YAML y que además sea más rápido en los benchmarks. La migración parece sencilla
Al final el problema sigue ahí. Dependemos del trabajo de otras personas que hoy están dispuestas a aportar trabajo gratis, pero eso puede no continuar para siempre
No sé si hay una forma de evitarlo aparte de compensarlas por su tiempo y esfuerzo y esperar que sigan haciendo buen trabajo
“A pure rust YAML implementation.”
En cambio, serde_yaml era más difícil de considerar Rust puro, porque dependía de unsafe-libyaml, que era libyaml convertido con c2rust
En un horizonte de tiempo lo suficientemente largo, la probabilidad de que un maintainer de open source deje el proyecto es 1
La única forma de evitarlo es volver tabú los proyectos mantenidos por una sola persona
Probablemente no sea una opción para proyectos Rust, porque quienes hicieron la biblioteca estándar de Rust quieren mantenerla minimalista
Pero al usar otros lenguajes con una biblioteca estándar suficientemente completa, sin duda es una opción posible
Como uso deliberadamente lenguajes que ya traen incorporado lo que necesito, casi no uso librerías externas aparte de drivers de bases de datos
Toda esta situación se ve un poco ridícula. Si el código funciona y lo ha hecho durante años, no veo por qué sería un problema que no esté mantenido.
Si no hace falta modificarlo y conoces sus límites y funcionalidades, está bien.
El código no se echa a perder solo. Varias veces he tomado prestado o integrado código de hace décadas y lo he usado sin problemas.
Personalmente, creo que simplemente ignoraría todas las quejas sobre esa biblioteca y seguiría adelante.
Ya se acumularon muchos bugs, y aunque existen correcciones, nunca se van a aplicar. Basta ver https://github.com/chyh1990/yaml-rust/issues y /pulls.
Es como hacerse cargo del mantenimiento del fragmento de código que uno usa.
A veces puede ser malo, porque puede convertirse en programación de copiar y pegar o en duplicación de esfuerzo.
Pero si un componente de terceros está totalmente sin mantenimiento, es bastante entendible.
Sí, las dependencias se pueden vendorear. En general, eso es lo que se ha hecho durante 20 años con dependencias que están “casi completas” y cuyo desarrollo y mantenimiento se volvieron lentos.
Aunque nunca he trabajado en un lenguaje donde “batteries are not included”.
¿Habrá alguna forma de crear algo como
cargo vendor --aggressive, para podar todo el código muerto dentro de las dependencias desde la perspectiva de mi crate?Me pregunto si eso podría hacer más manejable el problema de “revisar las dependencias”.
Se sale un poco del tema principal del texto, pero tiene relación en el sentido de que al final elegir dependencias y asumir todas las responsabilidades que conllevan depende de nosotros.
Parece haber espacio para herramientas que nos ayuden a hacernos más responsables de lo que realmente termina compilado dentro del crate.
Si no, no veo qué mejora.