Qué es unsafe y por qué existe
Rust seguro es un verificador conservador: solo acepta lo que puede demostrar. Existen programas seguros que no sabe probar, y para ese margen existe unsafe. No apaga comprobaciones: te deja hacer cosas cuya seguridad tú garantizas, trasladando la carga de la prueba de la máquina a ti.
El compilador de Rust es, en el fondo, un verificador estático: no ejecuta tu programa, sino que intenta demostrar que jamás incurrirá en comportamiento indefinido. Y como todo verificador honesto, cuando no logra la prueba, rechaza. Esto lo hace conservador: existen programas perfectamente seguros que el borrow checker no sabe demostrar y por tanto prohíbe. unsafe es la puerta que Rust reserva para ese margen exacto: el conjunto de operaciones cuya seguridad tú puedes garantizar pero la máquina no. No es un interruptor que apaga la seguridad ni un permiso para “hacer C dentro de Rust”. Es un desplazamiento de la carga de la prueba: donde antes el compilador firmaba la corrección, ahora firmas tú. Entender esa transferencia de responsabilidad —qué obligas al contraerla, qué saldas al ejercerla— es entender por qué Rust necesita, en su mismísimo núcleo, una palabra que parece contradecir todo lo que promete.
- Comprender por qué el Rust seguro es conservador: rechaza programas seguros que no puede probar.
- Distinguir las dos caras de
unsafe: launsafe fnque contrae una obligación y el bloqueunsafeque la salda. - Ver que toda la biblioteca estándar —
Vec,Box,Arc— se construye sobreunsafeencapsulado. - Situar la edición 2024 y su exigencia de bloques
unsafeexplícitos dentro de unaunsafe fn.
El verificador es conservador por diseño
Recupera una intuición del borrow checker: acepta un programa solo si puede demostrar que respeta las reglas de aliasing y de vidas. Pero demostrar es más difícil que ser verdad. Hay código cuya seguridad es evidente para un humano y aun así el análisis, por grueso, no la captura. El caso canónico es partir un slice mutable en dos mitades disjuntas:
fn partir_en_dos(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len();
let ptr = slice.as_mut_ptr();
assert!(mid <= len);
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Las dos mitades no se solapan: es provablemente seguro tener un &mut a cada una. Pero el borrow checker solo ve dos préstamos mutables derivados del mismo slice y, fiel a su regla de aliasing exclusivo, los rechazaría. La verdad excede a la prueba. unsafe es el mecanismo con el que le dices al compilador: “aquí hay una garantía que yo puedo sostener y tú no sabes verificar; déjame ejercerla”. Por eso split_at_mut, la versión real de esta función en std, está construida exactamente así.
Fíjate en lo que no está pasando: no estás engañando al compilador ni desactivando nada. Estás usando unsafe para afirmar un hecho —que estas dos mitades son disjuntas— que es verdadero y que el análisis, por su granularidad, no sabe deducir. La assert! de la primera línea no es decorativa: convierte tu afirmación en algo que se comprueba en ejecución, de modo que si alguien llama con un mid imposible, el programa aborta limpiamente en vez de fabricar dos slices solapados. Esa combinación —una verdad que tú conoces más una comprobación que la respalda— es el molde de casi todo unsafe bien escrito.
El slice partido no es un caso aislado. La misma tensión reaparece en una lista doblemente enlazada —donde cada nodo apunta a su vecino y el vecino de vuelta a él, un aliasing que el modelo de propiedad no expresa—, en un grafo con ciclos, o en una arena de nodos. Todas son estructuras seguras y útiles que el borrow checker, por su naturaleza conservadora, no sabe aprobar. unsafe existe para que Rust no tenga que renunciar a ellas ni empujarte de vuelta a un lenguaje sin garantías.
Todo verificador estático vive atrapado entre dos fracasos: aceptar un programa peligroso —inadmisible— o rechazar uno seguro —irritante—. Rust jamás cede en el primero, así que toda la fricción cae en el segundo. unsafe no elimina la brecha; te da una salida controlada para los casos concretos en que sabes que tu programa cae del lado seguro aunque la prueba automática no llegue.
Las dos caras: contraer una deuda y saldarla
La palabra unsafe aparece en dos posiciones que la gente confunde, y son casi opuestas.
Una unsafe fn declara una obligación: anuncia que la función tiene un contrato que quien la llame debe cumplir, o habrá comportamiento indefinido. Es una deuda que la firma contrae y traslada a la persona que llama.
/// # Seguridad
/// `idx` debe ser estrictamente menor que `self.len()`.
/// Llamarla fuera de rango es comportamiento indefinido.
unsafe fn obtener_sin_comprobar(&self, idx: usize) -> &T {
// ...
}
Un bloque unsafe hace lo contrario: salda una obligación. Es tu afirmación —“he verificado a mano que las condiciones se cumplen aquí”— que autoriza a ejercer un superpoder o a llamar a una unsafe fn. La deuda que la unsafe fn contrajo, el bloque unsafe la paga en el punto de uso.
let x = coleccion.obtener_sin_comprobar(3); // no compila: deuda impaga
let y = unsafe { coleccion.obtener_sin_comprobar(3) }; // saldada: yo garantizo 3 < len
unsafe fn · contraer
La palabra en la firma declara una deuda: quien llame debe cumplir un contrato o habrá comportamiento indefinido. La responsabilidad viaja hacia quien usa.
bloque unsafe · saldar
La palabra en el cuerpo paga la deuda: afirmas que aquí, en este punto exacto, las condiciones del contrato se cumplen. La responsabilidad la asumes tú.
Antes, el cuerpo entero de una unsafe fn era implícitamente un bloque unsafe. Desde la edición 2024 el lint unsafe_op_in_unsafe_fn es aviso por defecto: dentro de una unsafe fn sigues necesitando bloques unsafe explícitos para ejercer superpoderes. La razón es profunda: contraer una obligación en la firma y saldar otras al usar operaciones internas son actos distintos, y mezclarlos ocultaba en qué línea exacta descansa cada garantía.
Toda la seguridad descansa sobre unsafe
Aquí está la paradoja fundacional. Vec, Box, Rc, Arc, RefCell, Mutex: cada abstracción segura que usas a diario está implementada con unsafe por dentro. Vec maneja un puntero crudo, memoria sin inicializar y llamadas al asignador; nada de eso lo puede expresar el Rust seguro. Lo que hace Vec es encapsular ese unsafe tras una API cuya propiedad clave es la solidez (soundness): ningún uso desde código seguro, por retorcido que sea, puede provocar comportamiento indefinido.
flowchart TD A[Codigo seguro del usuario] -->|solo llama a la API| B[Frontera segura y solida] B --> C[Interior con unsafe encapsulado] C --> D[Punteros crudos, memoria sin inicializar, asignador] E[El autor garantiza las invariantes] --> C style A fill:#a6e3a1,color:#11111b style B fill:#89b4fa,color:#11111b style C fill:#f38ba8,color:#11111b style E fill:#cba6f7,color:#11111b
La palabra clave es encapsular. Vec no te expone su puntero crudo ni te pide que mantengas sus invariantes: te da push, get, len, y por dentro traduce cada uno a operaciones sobre memoria cruda que él, y solo él, sabe mantener coherentes. Tú heredas la seguridad sin heredar la responsabilidad. Ese trato es lo que hace posible que millones de programas Rust jamás escriban unsafe y aun así se apoyen, en cada Vec que usan, sobre un núcleo que sí lo hace.
Esa es la división del trabajo de todo Rust idiomático: un núcleo pequeño de unsafe, auditado y encapsulado, sobre el que se levanta un océano de código seguro que hereda sus garantías gratis. unsafe no es lo contrario de la seguridad de Rust; es su cimiento.
Seguro, sólido y correcto son tres cosas distintas
El principiante funde en una sola idea tres adjetivos que conviene separar quirúrgicamente. Un fragmento es seguro si no ejerce ningún superpoder: el compilador garantiza por sí solo que no puede provocar comportamiento indefinido. Una abstracción es sólida (sound) si ninguna combinación de usos desde código seguro, por retorcida que sea, logra arrancarle un UB. Y un programa es correcto si hace lo que promete. Los tres son independientes entre sí:
fn suma(a: i32, b: i32) -> i32 {
a - b // SEGURA pero INCORRECTA: ningun compilador te salva de esto
}
Esta resta disfrazada de suma es perfectamente segura —no hay UB posible— y a la vez desastrosamente incorrecta. La lección es que “seguro” en Rust no significa “sin errores”: significa “sin comportamiento indefinido”, una frontera mucho más estrecha y mucho más grave. Por eso unsafe no habla jamás de corrección lógica —de eso se ocupan los tests—, sino de esa línea fina donde el programa deja de tener significado definido. Cuando construyes una abstracción con unsafe, tu deber no es que sea correcta, sino que sea sólida: que ni el usuario más retorcido pueda, sin teclear unsafe, empujarla al comportamiento indefinido.
La convención universal en Rust es preceder cada bloque unsafe con un comentario // SAFETY: que nombra la invariante concreta que ese bloque presupone y por qué se cumple aquí. No es decoración: es la prueba, escrita en prosa, que el compilador te delegó. Un bloque unsafe sin // SAFETY: es una firma sin contrato adjunto —nadie, ni tú mismo dentro de un mes, podrá revisar si la promesa se sostiene—. Volverás sobre esta disciplina en la lección de invariantes.
La lectura ingenua de unsafe —“el modo en que Rust deja de comprobar y te fías”— es exactamente la equivocación que envenena el diseño de quien la sostiene. Rust nunca deja de exigir seguridad; lo que cambia dentro de unsafe es quién produce la demostración. Fuera, el compilador es un asistente de pruebas que descarga cada obligación automáticamente y solo acepta lo que sabe probar. Dentro, ese asistente se topa con operaciones cuya corrección no puede derivar —desreferenciar un puntero crudo, confiar en que dos mitades no se solapan— y hace lo único honesto que puede hacer: te pasa el bolígrafo. “Yo no sé demostrar esto; fírmalo tú.” La invariante que sostiene la seguridad no desaparece —estaba ahí desde el principio, sujetando el lenguaje entero—; simplemente pasas a ser tú el responsable legal de mantenerla. Por eso unsafe es a la vez humilde y temible. Humilde, porque reconoce la brecha inevitable de todo verificador estático: siempre habrá programas seguros que la máquina no sabe probar, y negarse a ejecutarlos jamás significaría amputar la interoperación con C, las estructuras de datos que el borrow checker no expresa y el último uno por ciento de rendimiento medido. Temible, porque una firma falsa no la atrapa ninguna excepción: el optimizador tratará tu promesa como verdad de base y compilará sobre ella, de modo que si mentiste, no “gestiona” la mentira, construye encima. La grandeza de la palabra es que hace visible y localizada esa transferencia de confianza: en vez de diluir el peligro por todo el programa como en C, lo concentra en bloques marcados, pequeños y auditables, donde un revisor sabe exactamente dónde mirar. unsafe es el punto en que Rust deja de ser un lenguaje que te protege y pasa a ser uno que te cree. Toda la disciplina que sigue —minimizar la superficie, encapsular tras APIs sólidas, comentar cada invariante— existe para ser digno de esa credulidad.
El Rust seguro es un verificador conservador: solo acepta programas cuya seguridad puede demostrar, y rechaza algunos que son seguros pero no probables. unsafe cubre ese margen desplazando la carga de la prueba del compilador a ti. Tiene dos caras: la unsafe fn contrae una obligación que traslada a quien llama; el bloque unsafe la salda afirmando que las condiciones se cumplen. La edición 2024 exige bloques unsafe explícitos incluso dentro de una unsafe fn. Toda la biblioteca estándar se construye sobre unsafe encapsulado tras APIs sólidas: no es lo opuesto a la seguridad, es su cimiento.
- Escribe tu propia
partir_en_dosy confirma que la versión sinunsafe—devolver dos&muta mitades del mismo slice— es rechazada por el borrow checker. Explica qué regla cree violar. - Declara una
unsafe fn obtener_sin_comprobarcon su comentario# Seguridad. Llámala sin bloque y con bloque; describe qué obligación contrae la firma y cuál saldas tú. - En una
unsafe fnde la edición 2024, ejerce un superpoder sin bloqueunsafeinterno y observa el aviso deunsafe_op_in_unsafe_fn. Razona por qué la edición separa contraer de saldar. - Enumera tres tipos de
stdimplementados conunsafeinterno y explica qué significa que su API pública sea sólida. - Argumenta por qué un lenguaje que se niega a incluir
unsafesacrificaría capacidades reales, no solo peligro.