wandres.dev
UNSAFE · cuándo y por qué

Los cinco superpoderes que solo unsafe desbloquea

unsafe no abre un mundo ilimitado: desbloquea exactamente cinco operaciones y ni una más. Desreferenciar punteros crudos, llamar funciones unsafe, acceder a static mut, implementar traits unsafe y leer campos de una union. Conocer la lista exacta es saber que la superficie del peligro es pequeña y numerable.

⏱ 18 min

La imaginación popular pinta unsafe como una compuerta a un abismo sin fondo: dentro, todo vale. La realidad es mucho más modesta y por eso mucho más tranquilizadora. unsafe desbloquea una lista cerrada y numerable de exactamente cinco capacidades adicionales, definidas con precisión en la referencia del lenguaje. Desreferenciar un puntero crudo, llamar a una función unsafe, leer o escribir un static mut, implementar un trait unsafe, y acceder a los campos de una union. Eso es todo. Ni una operación más se desbloquea. Todo lo demás dentro de un bloque unsafe es Rust idéntico al de fuera, con sus mismas reglas. Aprender esta lista de memoria no es pedantería académica: es la forma más rápida de desactivar el miedo, porque convierte una nube difusa de “aquí pasan cosas peligrosas” en cinco puntos concretos que un revisor puede buscar, contar y auditar.

🎯 Al terminar esta lección sabrás
  • Memorizar las cinco —y solo cinco— operaciones que unsafe desbloquea.
  • Escribir el ejemplo mínimo de cada superpoder y saber cuál es su contrato.
  • Distinguir crear un puntero crudo (seguro) de desreferenciarlo (superpoder).
  • Conocer los cambios de la edición 2024: unsafe extern y el rechazo de referencias a static mut.

Superpoderes uno y dos: punteros crudos y funciones unsafe

Desreferenciar un puntero crudo (*const T o *mut T) es el primero. Nota la asimetría clave: crear un puntero crudo y hacer aritmética con él es seguro; lo único que exige unsafe es desreferenciarlo, porque solo ahí puede ocurrir el desastre.

let x = 42;
let p: *const i32 = &x;          // crear el puntero: seguro
let q = p.wrapping_add(1);       // aritmetica: seguro
let valor = unsafe { *p };       // desreferenciar: superpoder

Llamar a una función unsafe es el segundo, e incluye las funciones extern de otro lenguaje (FFI). En la edición 2024 el propio bloque extern debe marcarse unsafe extern, subrayando que enlazar con símbolos externos es en sí una promesa:

unsafe extern "C" {
    fn abs(entrada: i32) -> i32;
}

let a = unsafe { abs(-5) };      // llamar a la funcion externa

Superpoderes tres, cuatro y cinco: static mut, traits unsafe y union

Acceder o modificar un static mut es el tercero. Una variable global mutable es una fuente perpetua de carreras de datos, así que cada acceso exige unsafe. En la edición 2024, además, crear referencias a un static mut es un error: debes usar los operadores de préstamo crudo &raw const y &raw mut.

static mut CONTADOR: u32 = 0;

unsafe {
    let p = &raw mut CONTADOR;   // no `&mut CONTADOR`: en 2024 es error
    *p += 1;
}
⚠️
static mut es el superpoder que casi nunca deberías ejercer

De los cinco, static mut es el más desaconsejado. Una global mutable en un programa con hilos es una carrera de datos esperando a ocurrir, y por eso la edición 2024 endureció su uso hasta prohibir las referencias directas. En código nuevo casi siempre hay una alternativa segura y mejor: un tipo atómico como AtomicU32, un Mutex, o un OnceLock para inicialización diferida. Reserva static mut para casos verdaderamente excepcionales —firmware, un símbolo que exige C— y aun así prefiere envolverlo.

Implementar un trait unsafe es el cuarto. Traits como Send y Sync son unsafe porque implementarlos es afirmar una propiedad que el compilador no puede verificar —que tu tipo es seguro de mover entre hilos, o de compartir por referencia—. La palabra unsafe en el impl es tu firma sobre esa afirmación.

struct MiPuntero(*mut u8);

// Afirmo que mover MiPuntero entre hilos es seguro. El compilador no lo sabe.
unsafe impl Send for MiPuntero {}

De los cinco, este es el que más gente ejerce sin verlo como tal: cada vez que envuelves un puntero en un tipo y quieres compartirlo entre hilos, tarde o temprano escribes una unsafe impl Send o unsafe impl Sync. Y es también el más traicionero, porque su contrato no es local —no basta mirar una línea— sino global: afirmas algo sobre todos los usos concurrentes posibles del tipo. Mentir aquí no produce un fallo en la línea del impl, sino una carrera de datos que aflora a kilómetros, en otro hilo, bajo carga.

Acceder a los campos de una union es el quinto. Una union guarda varios campos en el mismo espacio de memoria; leer un campo distinto del que se escribió reinterpreta los bits, y esa reinterpretación puede ser inválida. Por eso leer un campo es un superpoder (escribir uno de tipo Copy, no).

union Bits {
    entero: u32,
    flotante: f32,
}

let b = Bits { entero: 0x3f80_0000 };
let f = unsafe { b.flotante };   // reinterpreta los mismos bits como f32

Cinco y solo cinco. La referencia del lenguaje las enumera de forma exhaustiva, y esa exhaustividad es en sí una promesa: si una operación no figura en esta lista, entonces no necesita unsafe, porque el lenguaje ya demostró que no puede, por sí sola, provocar comportamiento indefinido. Sumar enteros, indexar un Vec, mover un valor, clonar, iterar: nada de eso aparece, porque nada de eso puede corromper la memoria sin pasar antes por una de las cinco puertas.

🎯

1 · Puntero crudo

Desreferenciar *const T o *mut T. Crearlos y sumarles es seguro; solo el acceso a memoria a través de ellos exige unsafe.

📞

2 · Función unsafe

Invocar cualquier unsafe fn, incluidas las unsafe extern de C. Saldas el contrato que su firma declaró.

🌍

3 · static mut

Leer o escribir una global mutable. En 2024, siempre vía &raw const o &raw mut, nunca por referencia directa.

✍️

4 · Trait unsafe

Implementar Send, Sync u otro trait unsafe: firmas una propiedad que el compilador no verifica.

🔀

5 · Campo de union

Leer un campo de una union: reinterpretas los mismos bits bajo otro tipo, con riesgo de valor inválido.

flowchart TD
U[La palabra unsafe] --> P1[Desreferenciar puntero crudo]
U --> P2[Llamar funcion unsafe o extern]
U --> P3[Acceder a static mut]
U --> P4[Implementar trait unsafe]
U --> P5[Leer campo de union]
U -.no desbloquea nada mas.-> X[El resto es Rust normal]
style U fill:#f38ba8,color:#11111b
style X fill:#a6e3a1,color:#11111b
⚠️
Crear no es desreferenciar; esa frontera es la clave

El error mental más común es creer que tocar un puntero crudo ya es peligroso. No lo es: let p = &x as *const i32; es seguro, y también p.add(3). Rust deja pasar toda la manipulación simbólica del puntero sin unsafe porque no accede a memoria. El único acto que puede corromper el proceso —leer o escribir a través del puntero— es el que queda tras la puerta. Interiorizar esta línea evita salpicar unsafe donde no hace falta y lo concentra donde de verdad importa.

Lo que queda fuera de la lista

Tan instructivo como saber qué desbloquea unsafe es ver qué no desbloquea, porque revela cuánto sigue siendo Rust normal. Ninguna de estas operaciones —todas potencialmente peligrosas en C— necesita unsafe en Rust, porque el lenguaje ya garantiza su seguridad:

let mut v = vec![1, 2, 3];
let x = v[1];                   // indexar: comprobado en ejecucion, sin unsafe
v.push(4);                      // realojar: Vec mantiene sus invariantes por ti
let suma: i32 = v.iter().sum(); // iterar y sumar: sin unsafe
let w = v.clone();              // clonar: sin unsafe

Indexar fuera de rango provoca un panic, no un acceso salvaje; realojar el Vec es correcto por construcción; iterar respeta las vidas. Que nada de esto figure entre los cinco superpoderes es la otra cara de la promesa: el peligro está acotado a una lista cerrada, y todo lo que queda fuera de ella el compilador lo sostiene por ti.

La lista es cerrada, y esa clausura es la diferencia con C

Detente en la propiedad más importante de esta lección: la lista tiene exactamente cinco elementos y no crece. En C, la inseguridad es ambiental —cualquier línea puede corromper la memoria: un índice, una suma que desborda, un puntero que apunta a un objeto ya liberado, una conversión temeraria—, de modo que auditar la seguridad de un programa C significa auditarlo entero, siempre, sin fin. Rust hace una jugada conceptual que lo cambia todo: enumera de forma exhaustiva las operaciones capaces de provocar comportamiento indefinido, las declara ejercibles solo tras la palabra unsafe, y demuestra que cualquier otra cosa —todo el resto del lenguaje— es incapaz de causar ese daño. La consecuencia práctica es asombrosa: para revisar la seguridad de memoria de un millón de líneas de Rust no lees un millón de líneas, lees únicamente los bloques unsafe, que en un código sano son una fracción minúscula. Has pasado de una responsabilidad global e inauditable a una local y numerable. Y cada uno de los cinco superpoderes corresponde a una vía concreta y bien entendida hacia el desastre: el puntero crudo abre el uso-tras-liberación y el fuera de rango; la función unsafe hereda contratos arbitrarios; el static mut invita a la carrera de datos; el trait unsafe deja afirmar propiedades falsas sobre hilos; la union permite fabricar valores inválidos reinterpretando bits. Conocerlos por su nombre no es erudición: es tener el mapa completo del único territorio donde Rust puede sangrar. El miedo difuso a unsafe se disuelve en cuanto compruebas que el peligro no es una niebla infinita sino cinco puertas con nombre, y que tú tienes la lista.

📝
Lo esencial de los cinco superpoderes

unsafe desbloquea exactamente cinco operaciones: desreferenciar un puntero crudo, llamar a una función unsafe o extern, acceder a un static mut, implementar un trait unsafe, y leer un campo de union. Todo lo demás dentro de unsafe es Rust normal. Crear y manipular punteros crudos es seguro; solo desreferenciarlos exige el poder. La edición 2024 obliga a unsafe extern y prohíbe referencias directas a static mut, forzando &raw const y &raw mut. Que la lista sea cerrada convierte la auditoría de seguridad de global e infinita en local y numerable.

⚔️ Ejerce los cinco, uno por uno
  1. Crea un *const i32, hazle aritmética con wrapping_add fuera de unsafe, y desreferéncialo dentro. Explica por qué solo el último paso lo exige.
  2. Declara un bloque unsafe extern "C" con abs y llámalo. ¿Qué obligación implica enlazar con un símbolo externo?
  3. Define un static mut e increméntalo usando &raw mut. Explica por qué la edición 2024 rechaza &mut CONTADOR.
  4. Escribe un tipo con un *mut u8 y una unsafe impl Send para él. Argumenta qué estás afirmando y por qué el compilador no puede comprobarlo.
  5. Define una union con un u32 y un f32, escribe uno y lee el otro. Explica por qué leer reinterpreta bits y podría producir un valor inválido.