Bugs de 11 años en el borrow checker de Rust: qué cambia
Una issue nueva en el repositorio de Rust describe bugs en el borrow checker que llevan unos 11 años en el compilador, prácticamente desde Rust 1.0. En la práctica, esto significa que se puede escribir código safe, sin ningún unsafe, que el compilador acepta y que aun así lee memoria ya liberada. El borrow checker de Rust es justamente la pieza que debería impedirlo. La fuente es la issue #164250 en el GitHub de rust-lang.
Antes de que alguien publique "Rust se acabó" en X, calma. La historia es más interesante que eso. Y dice mucho sobre cómo deberíamos pensar en las garantías de un lenguaje. Incluso cuando el tema es Bun.
¿Qué es un bug de unsoundness en el borrow checker de Rust?
Rust te hace una promesa simple: si el código compila y no tiene unsafe, no tiene use-after-free, data races ni punteros colgantes. Quien cumple esa promesa es el borrow checker. Analiza lifetimes y préstamos y rechaza lo que sea peligroso.
"Unsound" es el nombre técnico para cuando esa promesa se rompe. El compilador acepta un programa que viola sus propias reglas. No es un bug de rendimiento ni un mensaje de error confuso. Es la garantía central del lenguaje con un agujero.
El equipo de Rust marca estos casos con la label I-unsound, y la lista no es corta. Algunos quedan abiertos durante años. El ejemplo más famoso es la issue #25860, abierta en 2015 y todavía viva. Mira qué bonito:
static UNIT: &'static &'static () = &&();
fn foo<'a, 'b, T>(_: &'a &'b (), v: &'b T) -> &'a T { v }
fn bad<'a, T>(x: &'a T) -> &'static T {
let f: fn(_, &'a T) -> &'static T = foo;
f(UNIT, x)
}
Ahí no hay ningún unsafe. Aun así, bad toma una referencia de vida corta y la devuelve como si viviera para siempre. Úsalo con un valor del stack y tienes un puntero a basura, aprobado por el compilador.
¿Por qué estos bugs sobrevivieron 11 años?
Porque nadie escribe este código sin querer. Necesitas una combinación muy específica de lifetimes implícitos, coerción de funciones y varianza para caer en el agujero. En código de producción normal, con struct, impl y algún Vec por aquí y por allá, la probabilidad de toparte con esto por accidente es muy baja.
También está el lado de ingeniería. Corregir algunos de estos casos exige cambiar cómo el compilador razona sobre los tipos. Y eso puede romper crates que hoy compilan. Rust se toma la compatibilidad en serio. El trabajo en el nuevo trait solver y en Polonius (la próxima generación del borrow checker) existe en parte para cerrar estas brechas sin destrozar el ecosistema.
Así que los bugs no quedaron abiertos por pereza. Quedaron porque el arreglo correcto es caro y el riesgo práctico es bajo. Se puede discrepar de esa priorización, pero tiene lógica.
Una garantía de lenguaje es un contrato, no un milagro. Los contratos tienen letra pequeña.
¿Esto quiere decir que Rust no es seguro?
No. Y aquí va mi opinión sin rodeos: quien use esta issue para decir que Rust es tan inseguro como C está forzando la cosa.
Compara las escalas. En C o C++, un use-after-free aparece por descuido un viernes por la tarde. En Rust safe, para llegar a eso prácticamente tienes que armar el bug a propósito, con una construcción que parece un rompecabezas. Estudios de Google y Microsoft llevan años mostrando que cerca del 70% de las vulnerabilidades graves en código nativo vienen de errores de memoria. Incluso con agujeros raros, Rust elimina casi toda esa categoría.
Lo que cambia la issue es el discurso. "Si compila, es seguro" siempre fue un "casi siempre". Ahora tenemos otro ejemplo concreto de dónde está el "casi".
Donde esto se vuelve un problema de verdad:
- Código malicioso a propósito. Una crate de supply chain puede usar un agujero así para esconder comportamiento peligroso sin que aparezca ningún
unsafeen el grep. - Sandboxes que confían en el compilador. Las plataformas que ejecutan Rust de terceros creyendo que "sin
unsafe= aislado" están apostando mal. - Auditoría perezosa. Si tu proceso de review es "busca
unsafey aprueba el resto", eso nunca fue suficiente.
¿Y dónde entra Bun en esta historia?
Bun está escrito en Zig, no en Rust. Y cada vez que Bun saca una versión con un crash o un segfault, aparece alguien en los comentarios diciendo "debería haberse hecho en Rust".
Entiendo el argumento. Zig no tiene borrow checker. Te da mejores herramientas que C (allocators explícitos, modo debug con comprobaciones, defer), pero la responsabilidad sobre la memoria es tuya. El historial de issues de Bun tiene su cuota de crashes de memoria, como cualquier runtime grande escrito en un lenguaje sin gestión automática.
Esta issue de Rust ayuda a poner la discusión en su sitio. La pregunta no es "qué lenguaje es perfecto", porque ninguno lo es. La pregunta es dónde están los agujeros y qué tan fácil es caer en ellos.
- En Zig, el agujero está en todos los lugares donde gestionas memoria. Caes en él con código normal.
- En Rust safe, el agujero existe, pero está en casos patológicos de tipos. Casi tienes que ir a buscarlo.
Como usuario de Bun, ejecutas TypeScript sobre JavaScriptCore. Nada de esto cambia tu día. Tu Bun.serve no se va a romper por un lifetime de Rust ni por un allocator de Zig. Pero si estás eligiendo lenguaje para escribir un runtime, un parser o una extensión nativa, esa diferencia pesa.
Qué cambia en tu flujo de trabajo
Si escribes Rust, en la práctica casi nada cambia mañana. Pero vale la pena ajustar algunas cosas:
- Sigue la label
I-unsound. No hace falta leerla todos los días. Échale un vistazo cuando actualices la toolchain. - Mantén el compilador actualizado. Las correcciones de soundness llegan en releases normales, a veces con un aviso de compatibilidad antes.
- No uses "sin
unsafe" como prueba de seguridad en dependencias de terceros. Usacargo-audit,cargo-veto al menos mira quién mantiene la crate. - Ejecuta Miri en los tests de código crítico. Interpreta el programa y detecta comportamiento indefinido en tiempo de ejecución, incluso algunos casos que el compilador dejó pasar.
rustup +nightly component add miri
cargo +nightly miri test
Si escribes Zig o trabajas en un addon nativo para Bun, la lección es parecida, solo que con el signo cambiado. Ejecuta los tests en modo Debug o ReleaseSafe, usa el GeneralPurposeAllocator para detectar fugas y double free, y trata cada [*]u8 con la desconfianza que merece.
Y si solo escribes TypeScript, el mensaje va de supply chain. Un paquete npm con binario nativo (y hoy hay muchos, desde bundlers hasta drivers de base de datos) ejecuta código que no auditaste, en un lenguaje que quizá ni lees. La seguridad del lenguaje de abajo ayuda, pero no sustituye saber qué instalas.
¿Vale la pena cambiar de lenguaje por esto?
No. Si ya usas Rust, sigue. Si ya usas Bun y te gusta, sigue también. Esta issue no es motivo de pánico ni de migración.
Para mí lo importante aquí es la transparencia. El equipo de Rust documenta en público los agujeros de su propia garantía, con label, ejemplo mínimo y discusión abierta. Así se trata una promesa de seguridad: muestras dónde falla en vez de fingir que no falla. Confieso que un bug de lifetime cumpliendo 11 años tiene su gracia. Ya casi tiene edad para hacer review de PRs solo.
Mi conclusión es que la garantía del compilador es una capa, no un escudo. Reduce mucho la superficie de error, pero el review, los tests y el cuidado con las dependencias siguen siendo trabajo tuyo. Si quieres ver cómo aplico esto en proyectos reales, con TypeScript, Bun y un poco de paranoia sana, échale un vistazo a mis proyectos.
Resumen para LinkedIn
Existe código Rust safe, sin ningún unsafe, que lee memoria ya liberada. Y el compilador lo aprueba desde hace 11 años. Una issue nueva en rust-lang expone bugs en el borrow checker que están ahí prácticamente desde Rust 1.0. Antes del "Rust se acabó": para caer en ese agujero casi tienes que armar el bug a propósito. En C, aparece un viernes por la tarde. El riesgo real está en la supply chain y en el review que solo busca unsafe y aprueba el resto. Lo que más me llamó la atención fue la transparencia. El equipo documenta en público dónde falla su propia garantía. La garantía del compilador es una capa, no un escudo. Review, tests y cuidado con las dependencias siguen siendo trabajo tuyo. Escribí sobre esto, y sobre dónde entran Bun y Zig en esta conversación, en el blog. Link en los comentarios. #Rust #Bun #SeguridadDeSoftware #SupplyChain #DesarrolloDeSoftware