Rust aceptó este código aparentemente seguro durante 4.001 días: reproduzco el use-after-free del borrow checker en la estable 1.99.0, donde el arreglo son 8 líneas y la factura en tests es de 38 archivos

El issue #164250 llegó el 2026-10-13: los límites de lifetime de un struct se pueden saltar asignando a través de un campo covariante, sin un solo bloque unsafe. Hice una bisección del compilador (API de Godbolt, 124 toolchains) y confirmé que el código se acepta desde la 1.4.0 - 4.001 días -, reproduje el use-after-free en la estable 1.99.0 y comprobé que Miri y ASan señalan lo que rustc da por bueno. El arreglo de 8 líneas toca 39 archivos, y la misma familia de fallos ya se 'arregló' una vez, en febrero de 2026.

El 2026-10-13 a las 08:45:54Z se abrió un issue contra rust-lang/rust titulado “struct’s lifetime requirements can be bypassed via covariant field assignment”. En quince minutos el equipo de tipos lo etiquetó como P-critical; a las 09:25 ya era P-high con la etiqueta I-unsound. Ese mismo día, un envío a Hacker News llegó a 667 puntos y 373 comentarios. El número de issue es #164250.

Lo que hace que un fallo de compilador así atraiga tanta atención no es el fallo. Es el código que lo dispara. El reproductor no contiene unsafe, ni transmute, ni punteros crudos, ni FFI. Es la clase de pareja struct-más-Drop que aparece en Rust corriente y, a día de hoy, toda versión publicada de rustc y todo nightly lo acepta.

No me conformé con leer el issue y el hilo. Hice una bisección del compilador por versiones, compilé el reproductor en local y lo ejecuté bajo Miri y AddressSanitizer. Todo lo que aparece abajo como medición es algo que ejecuté el 2026-10-13/14, con los comandos y las salidas reproducidas para que puedas verificarlo.

El reproductor

En la primera hora aparecieron cuatro reproductores distintos en el hilo del issue. Este es el que no necesita ni un static ni un segundo hilo, y el que demuestra que el problema son los límites, no la concurrencia:

struct MyType<'a, 'b: 'a, 'c>(&'c mut &'a str, &'b str);

impl<'a, 'b: 'a, 'c> Drop for MyType<'a, 'b, 'c> {
    fn drop(&mut self) {
        *self.0 = self.1;
    }
}

fn main() {
    let mut me_out = "rarw";
    {
        let l = String::from("some cool string");
        let mut q = MyType(&mut me_out, "");
        q.1 = &l;
    }
    println!("me_out: {}", me_out);
}

El struct declara 'b: 'a. La implementación de Drop tiene permiso para asumirlo, y escribe self.1 — nominalmente un &'b str que sobrevive al &'a str de self.0 — en la ranura desde la que se imprimirá me_out.

Después main asigna &l a q.1, donde l es un String dentro de un ámbito que termina dos líneas más abajo, y me_out se imprime después de que ese ámbito haya terminado. El compilador acepta todo ello.

No hay ninguna palabra clave unsafe en ese programa. Compílalo y obtienes una compilación limpia y código de salida 0.

Qué pasa de verdad al ejecutarlo

Instalé el toolchain estable actual (rustc 1.99.0, commit b940084d7, 2026-09-28) y compilé tres demos: el PoC original del issue con static y Mutex, el escape de ámbito con Drop de arriba, y una versión que entrega la referencia falsa 'static a otro hilo. Los tres compilaron sin un solo aviso de soundness, y los tres produjeron basura:

DemoMecanismoResultado observado en rustc 1.99.0
Confusión de tiposOption<T> con T: 'static + Send; el Drop empuja un Box<dyn Any> a un Mutex estáticoImprime bytes del heap en lugar de la cadena guardada
Escape de ámbitoLímite 'b: 'a; el Drop copia self.1 en self.0Imprime basura; la lectura pasa por un &str colgante
Entre hilosLímite 'a: 'static; el Drop lanza un hilo con un &'static StringEl segundo hilo lee el String liberado — y el propio println! entra en pánico

La tercera demo es la más elocuente, porque el fallo no es «se imprimieron bytes equivocados». Es una invariante de String violada:

thread '<unnamed>' panicked at library/core/src/fmt/mod.rs:2949:38:
start byte index 17 is out of bounds for string of length 16

Se produjo un String cuyo campo de longitud dice 16 a partir de un valor que el compilador creía que era un &'static String. El pánico no es el fallo; es un síntoma aguas abajo de leer un bloque del heap que ya había sido reasignado. Ni la compilación ni la ejecución produjeron un solo diagnóstico que apuntara al problema real.

La frontera de versiones, medida en vez de afirmada

En el hilo hay una afirmación — “unsound since 1.4” — y una confirmación de un colaborador del compilador — “unsound since 1.4 🤣”. Estas afirmaciones son baratas. Yo las bisequé con la API de compiladores de Godbolt, que expone 124 toolchains de rustc desde la 1.0.0, compilando la misma fuente contra cada una.

El primer barrido produjo un resultado falso que conviene señalar, porque cualquiera que reproduzca esto se topará con él: todas las versiones anteriores a la 1.58 rechazaban el programa, pero por el motivo equivocado. La 1.58 estabilizó los identificadores capturados en las cadenas de formato, así que println!("me_out: {me_out}") no compila en la 1.32 con “there is no argument named me_out” — no con un error de borrow checker. Reescribir el print como println!("me_out: {}", me_out) elimina el artefacto y deja ver la frontera real.

VariantePrimera versión que aceptaÚltima que rechazaMotivo del rechazo
Con límite explícito 'a: 'c en struct e impl1.4.0 (etiquetada 2015-10-29)1.3.0 (2015-09-17)E0597: 'l' does not live long enough
Sin el límite (&'c mut &'a str, 'b: 'a)1.31.0 (etiquetada 2018-12-06)1.30.0E0491: reference has a longer lifetime than the data it references

Las dos fronteras importan porque son fallos distintos con la misma raíz. La frontera de la 1.4.0 es la del título del issue: el límite 'b: 'a del struct no se demuestra cuando el compilador construye el tipo del local. La de la 1.31.0 es el mismo agujero alcanzado sin límite explícito: la comprobación estructural E0491, que antes atrapaba &'c mut &'a str, dejó de dispararse.

Todas las versiones muestreadas desde esas fronteras hasta hoy aceptan el programa: 1.35.0, 1.40.0, 1.50.0, 1.60.0, 1.70.0, 1.80.0, 1.85.0, 1.90.0, 1.95.0, 1.96.0, 1.97.0, 1.98.0, 1.99.0 y los builds nightly.

Desde la etiqueta 1.4.0 hasta el momento en que se abrió el issue hay 4.001 días — 10,95 años. Desde la 1.31.0 son 2.867 días. Si has leído que es un fallo «de 11 años», la cifra es real y es precisa: la primera versión estable que debería haber rechazado el programa es la 1.4.0, publicada el 2015-10-29, y entre la 1.4.0 y la 1.99.0 hubo 96 versiones menores estables que lo aceptaron todas.

Miri y ASan detectan lo que rustc no

Esta es la parte con valor práctico inmediato. Rust incluye dos herramientas dinámicas que detectan exactamente esta clase de error, y las dos señalan el código que el compilador llama sound.

Miri, sobre la misma fuente, en el nightly 1.101.0-nightly (e9bbadf68 2026-10-12):

error: Undefined Behavior: constructing invalid value of type &std::string::String:
       encountered a dangling reference (use-after-free)

Miri lo reporta en demo1, la primera demo, antes de que el programa imprima nada. No necesita un build con sanitizer, ni suerte en la planificación de hilos, ni un segfault para darse cuenta: el intérprete sabe que la asignación ya no existe.

AddressSanitizer, vía rustc +nightly -Zsanitizer=address:

==47175==ERROR: AddressSanitizer: stack-use-after-scope on address 0x71bcb0bf0408
READ of size 8 at 0x71bcb0bf0408 thread T0
SUMMARY: AddressSanitizer: stack-use-after-scope

Es la misma clase de reporte que citó el reportero original — AddressSanitizer: stack-use-after-scope — reproducida de forma independiente en otra máquina y otro toolchain.

Merece la pena fijarse en lo que implica este emparejamiento para un proyecto real. La capa estática es la única que está equivocada; las dos capas dinámicas son correctas. Un job de CI que ejecute cargo miri test y una pasada con sanitizer habrían detectado este patrón en el módulo que lo contiene, mientras cargo build lo certificaba tan tranquilo.

Por qué el compilador lo aceptó

El fallo está en un único sitio: el type checker del borrow checker de MIR nunca preguntó si el tipo del local de MIR está bien formado (well-formed).

El type checker de Rust funciona en dos pasadas. hir_typeck maneja los tipos a nivel de fuente y ahí sí demuestra la buena formación: por eso los locales con nombre y, por ejemplo, struct RequiresStatic<T: 'static> se rechazan normalmente. Después, el borrow checker de MIR revalida un conjunto mucho más pequeño de propiedades. La buena formación de locales y temporales simplemente no era una de ellas, así que un local Q<&'short String> cuya declaración exige T: 'static se colaba, y el límite se convertía en una suposición no demostrada sobre la que Drop tenía derecho a apoyarse.

La razón profunda de que una comprobación ausente sea tan destructiva: un límite de lifetime es exactamente la clase de cosa que Drop tiene permitido asumir. Cuando se ejecuta impl<T: 'static> Drop for Q<T>, el cuerpo tiene permiso para mover el valor a un static, lanzar un hilo con él o repartir una referencia &'static, porque el límite debía garantizar que el referente sobrevive al programa. En cuanto el límite se puede saltar en la asignación, cada uno de esos permisos se convierte en una licencia para use-after-free — que es justo por lo que las tres demos de mi ejecución fallan por la vía del destructor y no por una lectura normal.

Un matiz merece precisión, porque el equipo de tipos tiene razón al señalarlo y el matiz se pierde al contarlo. El salto del límite en sí no necesita ningún Drop. Compilé las variantes sin Drop en la estable 1.99.0 y compilan:

struct RequiresStatic<T: 'static>(Option<T>);

fn main() {
    let text = String::from("temporary");
    let mut value = RequiresStatic(None);
    value.0 = Some(&text);   // compila en 1.99.0, sin Drop, sin unsafe
    println!("compiled: {:?}", value.0);
}

Imprimió compiled: Some("temporary"). Lo mismo vale para una variante estructurada con Cell<&'static ()> y un par 'a/'b: 'a. La violación de la invariante es incondicional; lo que exige una implementación de Drop es solo la parte en la que la violación se vuelve observable. Si un proyecto nunca escribe un Drop para un tipo que lleva un límite de lifetime, el fallo es latente en lugar de explotable en ese proyecto — pero el estado latente es precisamente lo que una refactorización posterior convierte en use-after-free.

El arreglo son 8 líneas. La factura, 39 archivos.

El pull request #164277, “require MIR local types to be well-formed”, lo abrió un colaborador primerizo a las 18:48:21Z del 2026-10-13 — 10 horas y 2 minutos después de que se abriera el issue. El cambio sustantivo son ocho líneas en compiler/rustc_borrowck/src/type_check/mod.rs:

if self.body.local_kind(local) == LocalKind::Temp {
    self.prove_clause(
        ty::ClauseKind::WellFormed(local_decl.ty.into()),
        Locations::All(local_decl.source_info.span),
        ConstraintCategory::Boring,
    );
}

Las estadísticas de cabecera del PR cuentan algo que las ocho líneas no cuentan:

MétricaValor
Adiciones / borrados+823 / −23
Archivos modificados39
Archivos en compiler/1
Archivos en tests/38
Archivos de expectativas .stderr actualizados20
Estado a 2026-10-14abierto, mergeable_state: blocked, S-waiting-on-review
CItest-tidy falló; test-x86_64-gnu-gcc falló por diferencias de diagnóstico

Vuelve a leer esa tabla. Hubo que reescribir veinte expectativas de test existentes, y 38 archivos de test en total, para añadir una única demostración de buena formación de temporales. Es una medición de cuánta suite de tests del propio rustc codificaba implícitamente la comprobación que faltaba. Y es la respuesta más concreta disponible al comentarista de Hacker News que preguntaba “será interesante ver si arreglar este bug hace que algunos crates no compilen”: dentro del corpus de tests del propio compilador, la respuesta ya es veinte archivos de profundidad.

El arreglo también es deliberadamente estrecho, y esa estrechez es la decisión de diseño interesante. Está restringido a LocalKind::Temp, es decir, a temporales en lugar de a todos los locales, y el PR incluye un test check-pass que demuestra que la covarianza ordinaria sigue compilando:

  • let y = x; y donde x: &'a T — sigue bien.
  • Firmas de closures y de closures async que solo mencionan un lifetime — sigue bien.
  • let mut value = Some("static"); value = Some(input); value — sigue bien.

La frontera no es «la covarianza está prohibida». La frontera es si el tipo del local lleva un límite que los argumentos instanciados violan. Encoger 'static a 'a sigue siendo legal; construir Q<&'short String> donde Q exige T: 'static ya no. Esa es la línea que el compilador debería haber trazado siempre, y el arreglo la traza en ocho líneas.

Para ser claros con el estado: a día de hoy el PR está abierto y bloqueado, el revisor se reasignó de mu001999 al grupo de tipos, y ninguna versión publicada incluye el arreglo. Quien lo pruebe en un toolchain instalable hoy seguirá viendo que el programa compila.

Este agujero ya se «arregló» una vez, en febrero de 2026

Aquí está la parte de la historia que no aparece en los comentarios destacados del hilo de Hacker News, y cambia cómo debe leerse la edad del fallo.

Seis meses antes, otro reportero abrió el #148854, “Lifetime bounds of Drop aren’t checked properly”, el 2025-11-12. Su primera línea dice: “The below code causes use-after-free in safe code. (Prints garbage data in my testing.)” Llevaba las etiquetas I-unsound, P-high, A-destructors, A-lifetimes, T-types. Se le asignó P-critical y se rebajó a P-high esa misma mañana — un patrón que se repitió casi calcado el 2026-10-13.

Ese issue se cerró como arreglado por el PR #149389, “WF check lifetime bounds for locals with type params”, fusionado el 2026-02-14T06:22:35Z (+123 / −26, 22 archivos), cuyo cuerpo dice Fixes #115175 y Fixes #148854. El issue #115175 llevaba abierto desde el 2023-08-24 — 905 días.

Así que en febrero de 2026 aterrizó una comprobación de buena formación para locales con parámetros de tipo. El issue #164250 es el hermano de solo lifetimes de la misma invariante ausente: el arreglo de febrero se apoya en parámetros de tipo sin instanciar, mientras los reproductores de octubre instancian a tipos concretos Q<&'a String> cuya violación está en los argumentos de lifetime. El arreglo de febrero era correcto y cerró unsoundness real; simplemente acotó su propio ámbito, y el caso hermano siguió abierto otros 241 días hasta que alguien escribió el struct de cuatro líneas que lo hizo obvio.

Ese es el hallazgo real, y es más útil que el titular de los 11 años. El fallo tiene 11 años, pero la clase de fallo estuvo delante del equipo del compilador hace ocho meses, se declaró arreglada y una variante seguía distribuyéndose. Los 11 años miden cuánto tiempo se aceptó el código; los 241 días miden cuánto duró la falsa sensación de cierre. El segundo número es el que conviene vigilar en el tracker de tu propio proyecto.

Y la misma familia tiene hermanos abiertos

La buena formación de locales no es una comprobación. Es una pequeña familia de comprobaciones, y varios miembros siguen abiertos en rust-lang/rust:

IssueTítuloAbierto desdeDías abierto
#104478closures accept ill-formed inputs2022-11-161.428
#123312closure rvalue not checked for WF allowing it to assume more implied bounds2024-04-01926
#128225mut struct member var(ref) bind to new obj still hint: does not live long enough2024-07-26810

Los tres llevan etiquetas de la familia NLL-sound. El #104478 y el #123312 son sobre closures — y en el hilo del #148854, un colaborador del compilador ya había señalado que las firmas de las closures nunca se comprueban en busca de buena formación salvo que se llamen, y que arreglarlo “should be a separate PR however”. No fue ese PR.

El tamaño de la cola se mide fácil. A fecha de 2026-10-14, el repositorio rust-lang/rust tiene 140 issues abiertos con la etiqueta I-unsound (721 en total, 581 cerrados), más 42 abiertos de A-destructors y 198 de A-borrow-checker. Exactamente un issue ocupa ahora mismo P-critical: el #163911, “Miscompilation with FFI bool return type on x86_64”, abierto el 2026-10-07 — un recordatorio de que la casilla que el #164250 ocupó brevemente la ocupa algo igualmente capaz de generar código máquina incorrecto.

El contrapeso es que el proyecto lo dice en voz alta. El objetivo Project Zero, cuyo contacto es lcnr y cuyo responsable de tareas es Boxy, declara el objetivo sin rodeos: “Fix every known type system soundness bug in Rust, so that unsafe is the only way to cause undefined behavior.” Sus axiomas de diseño son «Zero means zero» — “Not ‘fewer bugs’ or ’the important ones.’” — y «Migrate, don’t break». Y su motivación no es abstracta: “if they are not fixed, one of them is likely to be the basis for a Rust exploit.”

Los contraargumentos, expuestos con toda su fuerza

Una lectura honesta del #164250 tiene que incluir la defensa de «esto es menos alarmante de lo que parece», porque la versión más fuerte de esa defensa viene de quienes mejor lo entienden.

La posición del equipo de tipos. lcnr anotó en el issue esa misma mañana: el fallo habría sido P-critical “regardless of whether people encountered it in the wild”, y es solo P-high “because it’s 11 years old. A bug like this which has not been found during the last 11 years cannot actually be that easy to hit.” Sobre la explotabilidad: “While it looks easy to trigger, to exploit this you need an intentionally malicious Drop implementation.” Mis propias pruebas respaldan la mitad mecánica de esa afirmación — los fallos observables de mis tres demos pasan todos por Drop, y las variantes sin Drop compilan pero no hacen nada dañino. No dice nada sobre la mitad de código accidental, y el equipo no afirma que lo haga.

«Nadie escribe Drop a mano.» Ygg2 en Hacker News: “Writing manual Drop isn’t usual. And writing drop with such lifetimes is definitely on the weirder scale.”

«El Drop manual está en todas partes.» neysofu, respondiendo en el mismo hilo: “That’s easily the exception rather than the rule. I’ve seen custom Drop implementations in almost every non-trivial Rust codebase I worked on.” gspr añade el caso que lo vuelve concreto: “A custom Drop appears very quickly with FFI code.” Si escribes un wrapper sobre una librería C que exige una llamada explícita de liberación, tienes un Drop escrito a mano — y los wrappers FFI son también donde los lifetimes y los límites se negocian con más agresividad.

«Esto no es una vulnerabilidad de seguridad.» Un comentario temprano del hilo argumentaba exactamente eso, sobre la base de que “Rust explicitly does not defend from malicious programs, only from mistakes.” Ese mismo comentarista se retractó después de la edición de lcnr en el issue: “This edit is newer than my comment (and its writer is an experienced T-types member, so I take back my words).” Una retractación en el hilo vale más que la afirmación original.

«Es una constelación oscura.» Es la afirmación que el hilo rechazó con más firmeza. vincent-uden: “This is not the esoteric, crazy, impractical constellation I expected to find with a title like this. The minimal example is so simple and is definitely something you’d easily come across in normal code.” theemathas, que abrió el issue y produjo varios reproductores: los fallos de soundness no son raros, pero “this one is unusual in how simple the code that causes it can be.”

«Los toolchains certificados están a salvo.» No lo están. Ferrocene no es una implementación separada del compilador: es rustc con un paquete de cualificación, unas versiones por detrás. En Hacker News, afdbcreid: “It uses the same code, so yes.” Y la cuestión de la especificación tiene una respuesta concreta, que verifiqué en lugar de repetir. La especificación publicada de Ferrocene, en public-docs.ferrocene.dev, contiene la modalidad:

4.15.2:40 An assignment expression requires that the type of its value operand is a subtype of the type of its assignee operand.

Descargué la página de la especificación y esa cláusula está ahí literalmente, en la sección titulada “4.15.2. Subtyping and Variance — Legality Rules”. q.0 = Some(&l) viola una regla de legalidad documentada mientras el compilador lo acepta. Un toolchain certificado no es un toolchain incapaz de equivocarse: es un toolchain cuyo error está documentado como regla de legalidad y por tanto es rastreable.

«Entonces las garantías estáticas ya no valen nada.» La conclusión contraria está mejor respaldada. El fallo es un bug del compilador, no del sistema de tipos: el sistema de tipos ya dice que el programa es inválido, y el arreglo hace que el compilador esté de acuerdo, sin cambiar ni una sola regla del lenguaje. Como lo expresó GrantMoyer en el hilo: “The compiler should always have been rejecting that code according to lifetime rules of Rust’s type system, so the fix is to just make it reject the code, rather than having to change the fundamental rules of lifetimes.” Y medler, sobre la consecuencia práctica: “It seems the bug here was caught by Asan instead of at compile time, so basically, for this one bug, rust is reduced to C++-like behavior. You’re still better off using rust because of all the bugs the borrow checker does catch.”

Qué hacer esta semana

Ordenado por coste-beneficio y basado en las mediciones de arriba, no en la tranquilidad.

  1. Asume que el toolchain que tienes está afectado. Verifiqué la aceptación en todas las versiones estables muestreadas desde la 1.4.0 hasta la 1.99.0 y en el nightly 1.101.0-nightly (e9bbadf68, 2026-10-12), anterior al PR del arreglo. El arreglo no está en ninguna versión publicada y el PR sigue abierto. Fijar versiones no ayuda aquí: ninguna estable instalable hoy está libre.
  2. Ejecuta cargo miri test en CI para los crates que definen Drop a mano. Miri detectó esto en la primera demo, sin build de sanitizer y sin suerte. Si Miri sobre todo el workspace es demasiado lento, acótalo a los crates con lifetimes y destructores.
  3. Añade una pasada de sanitizer. -Zsanitizer=address produjo el mismo informe stack-use-after-scope que vio el reportero original, lo que significa que tus tests de integración actuales ya cubren el disparador si los ejecutas bajo ASan.
  4. Audita el patrón concreto. La condición que hay que buscar es un local cuyo tipo lleva un límite de lifetime — T: 'static sobre un parámetro de tipo, o una relación explícita 'b: 'a entre los parámetros del struct — combinado con una asignación a un campo de ese local. La implementación de Drop es lo que convierte la violación en inseguridad de memoria. Es un grep pequeño y revisable, no una reescritura.
  5. Espera avisos de compatibilidad futura, no un muro. El segundo axioma de Project Zero es «Migrate, don’t break», y su lista de objetivos para 2026 incluye “Support emitting FCW for borrowck errors”, para que la comprobación se vuelva a ejecutar y se reporte como aviso antes de convertirse en error. Una crater run sobre el universo público de crates es el acompañante esperado de un arreglo así, y es el mecanismo que te dirá si tu código está entre los afectados.
  6. No trates «sin impacto en producción» como un argumento de durabilidad. El #148854 se cerró el 2026-02-14 con un arreglo real, acotado, que no cubría el caso hermano; el #164250 llegó 241 días después. Ambas cosas fueron ciertas a la vez. Si tu modelo de amenaza incluye una auditoría en 2027, la pregunta relevante no es «¿esto le ha mordido a alguien?», sino «¿está la invariante ya aplicada en todas partes?».

Preguntas frecuentes

¿Esto da a un atacante ejecución remota de código en mi servicio Rust? No por sí solo. Para ser explotable, un programa debe construir el tipo de local mal formado y tener una implementación de Drop que dependa del límite que el compilador no demostró. Es una configuración real en cualquier proyecto con destructores manuales y genéricos acotados por lifetime, y un atacante que contribuya código a tu proyecto puede escribir las dos mitades — pero la presencia del fallo en el compilador no es una vulnerabilidad en tu binario hasta que ese código existe. El encuadre correcto es unsoundness latente, no un CVE activo.

¿Es un fallo del lenguaje Rust o de la implementación? De la implementación. La especificación publicada — la cláusula 4.15.2:40 de la especificación de Ferrocene, que comprobé literalmente — ya exige que esta asignación se rechace. El compilador lleva fallando en aplicar una regla que el lenguaje ya tiene.

¿Qué toolchain necesito para estar a salvo? Ninguno que exista aún. Confirmé la aceptación hasta la estable 1.99.0 y en un nightly fechado un día antes de que se abriera el PR del arreglo. Vigila el estado de fusión del #164277 en lugar del número de versión, y cuenta con una fase de avisos de compatibilidad futura antes de la aplicación estricta.

¿Por qué el arreglo tocó 38 archivos de test si son ocho líneas de código? Porque hubo que cambiar 20 expectativas .stderr existentes para tener en cuenta los nuevos diagnósticos, y esos archivos estaban registrando el comportamiento antiguo como correcto. El recuento es la medida más directa disponible de cuánto dependía la propia suite de tests de rustc de la comprobación ausente. También es la evidencia más limpia para la pregunta de «¿esto romperá crates?»: dentro del compilador, ya rompe veinte archivos de referencia.

¿Es exacta la cifra de «11 años»? Sí, con una frontera precisa, y vale la pena enunciarla con precisión porque normalmente se enuncia a la ligera. La primera versión estable que acepta el reproductor con el límite explícito 'a: 'c es la 1.4.0, etiquetada el 2015-10-29T18:02:23Z; el issue se abrió el 2026-10-13, es decir 4.001 días después. Sin el límite explícito, el punto de entrada es la 1.31.0, etiquetada el 2018-12-06, o 2.867 días. Entre la 1.4.0 y la 1.99.0 hubo 96 versiones menores estables.

¿El equipo del compilador ya lo sabía? Tenían en la mano otra instancia de la misma invariante ausente: el #148854, etiquetado I-unsound y P-high, cerrado como arreglado por el PR #149389 el 2026-02-14. Ese arreglo comprobaba locales con parámetros de tipo sin instanciar. Los reproductores del #164250 son instanciaciones de solo lifetimes, que el arreglo de febrero no alcanza. Ambas afirmaciones están respaldadas por los cuerpos de los PR y la línea temporal del issue, y juntas son lo más importante que un responsable de ingeniería puede extraer de este incidente.

¿Dónde deja esto las promesas de seguridad de Rust? Intactas, y más concretas que antes. La promesa del Rust seguro la aplica un compilador que tiene fallos, y uno de ellos se encontró, se acotó, se reprodujo y se arregló en unas diez horas por un colaborador primerizo; dos herramientas dinámicas lo detectaban todo el tiempo; el arreglo son ocho líneas en la raíz. Lo que cambia es la advertencia honesta: «el Rust seguro no puede provocar comportamiento indefinido» es un objetivo con 140 tickets de excepción abiertos adjuntos, y el proyecto tiene declarado que arreglarlos todos es el plan.

La conclusión

Un struct de cuatro líneas con una relación de lifetime entre sus parámetros, más una implementación de Drop que cree que esa relación se cumple, ha compilado en todas las versiones estables de Rust desde la 1.4.0 — 4.001 días, 96 versiones menores — y sigue compilando en la estable 1.99.0 y en el nightly del 2026-10-12. Cuando lo ejecutas, obtienes una referencia colgante, bytes corruptos del heap y un String cuyo campo de longitud es mayor que su asignación. Miri reporta use-after-free y ASan reporta stack-use-after-scope en la primera demo. En ningún punto hay unsafe.

El arreglo son ocho líneas, abierto diez horas y dos minutos después del informe, y no se ha distribuido. La factura en tests es de veinte archivos de expectativas reescritos entre 38 modificados, que es la medida más honesta disponible de hasta dónde llegaba la comprobación ausente. Y la misma invariante se declaró arreglada en febrero de 2026 para el caso de parámetros de tipo, se cerró y se celebró — mientras el caso de solo lifetimes seguía abierto otros 241 días.

Si te llevas una sola cosa de esto, que sea los 241 días y no los 4.001. Los fallos longevos son un hecho del trabajo con compiladores y normalmente indican que el código de nadie los toca. Un caso hermano que sobrevive a una etiqueta de «arreglado» es una señal de proceso, y es la que sí puedes accionar: cuando cierres un fallo de soundness, deja escrito qué instanciaciones demostraste y cuáles no.