Unsafe Rust: raw pointers, bloques unsafe y cuándo (y cuándo no) usarlo

Rust divide el código en dos categorías: seguro (safe) e inseguro (unsafe). El código safe es el que escribe el 99% del tiempo, con todas las garantías del compilador. El código unsafe no desactiva el compilador ni convierte Rust en C: solo habilita exactamente cinco operaciones adicionales que el código safe no puede hacer. Entender cuáles son —y cuándo son necesarias— es lo que separa el uso correcto del abuso.

Las cinco operaciones unsafe

  1. Desreferenciar raw pointers (*const T y *mut T).
  2. Llamar a funciones unsafe (incluidas funciones de C por FFI).
  3. Acceder o modificar una variable estática mutable.
  4. Implementar un unsafe trait.
  5. Acceder a campos de una union.

Raw pointers

fn main() {
    let mut valor = 42i32;

    // Crear raw pointers es safe; desreferenciarlos no
    let ptr_inmutable: *const i32 = &valor;
    let ptr_mutable:   *mut i32   = &mut valor;

    unsafe {
        println!("Valor via ptr: {}", *ptr_inmutable);
        *ptr_mutable = 100;
        println!("Modificado:    {}", *ptr_inmutable);
    }

    // Los raw pointers pueden ser nulos y apuntar a memoria inválida
    // El compilador no lo comprueba: TÚ eres el responsable
    let ptr_nulo: *const i32 = std::ptr::null();
    // unsafe { println!("{}", *ptr_nulo); } // UB: puntero nulo
}

Funciones unsafe

// Declarar una función unsafe: quien la llame asume la responsabilidad
unsafe fn desplazar_puntero(ptr: *mut i32, offset: isize) -> *mut i32 {
    ptr.offset(offset)
}

// Crear una abstracción safe sobre código unsafe
pub fn dividir_slice(slice: &[i32], indice: usize) -> (&[i32], &[i32]) {
    assert!(indice <= slice.len());
    let ptr = slice.as_ptr();
    unsafe {
        (
            std::slice::from_raw_parts(ptr, indice),
            std::slice::from_raw_parts(ptr.add(indice), slice.len() - indice),
        )
    }
}

fn main() {
    let datos = [1, 2, 3, 4, 5];
    let (izq, der) = dividir_slice(&datos, 3);
    println!("{izq:?} | {der:?}"); // [1, 2, 3] | [4, 5]
}

FFI: llamar a funciones de C

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

extern "C" {
    fn strlen(s: *const c_char) -> usize;
}

fn main() {
    let cadena = CString::new("hola mundo").unwrap();

    unsafe {
        let longitud = strlen(cadena.as_ptr());
        println!("strlen: {longitud}"); // 10
    }

    // Convertir *const c_char de C a &str de Rust
    let ptr: *const c_char = cadena.as_ptr();
    unsafe {
        let s = CStr::from_ptr(ptr).to_str().unwrap();
        println!("Desde C: {s}");
    }
}

Static mutable: uso correcto

static mut CONTADOR: u32 = 0;

// INCORRECTO en código real: sin sincronización, UB en multihilo
unsafe fn incrementar() {
    CONTADOR += 1;
}

// CORRECTO: usar AtomicU32 para statics mutables
use std::sync::atomic::{AtomicU32, Ordering};
static CONTADOR_ATOMICO: AtomicU32 = AtomicU32::new(0);

fn incrementar_safe() {
    CONTADOR_ATOMICO.fetch_add(1, Ordering::Relaxed);
}

fn main() {
    incrementar_safe();
    incrementar_safe();
    println!("{}", CONTADOR_ATOMICO.load(Ordering::Relaxed)); // 2
}

Miri: detectar UB en código unsafe

// Instalar y ejecutar Miri:
// rustup component add miri
// cargo miri test

// Miri detecta:
// - Desreferenciación de punteros inválidos o nulos
// - Acceso fuera de bounds
// - Uso de memoria sin inicializar
// - Data races (con -Zmiri-track-raw-pointers)

// Ejemplo de UB que Miri detecta:
unsafe fn mal_uso() {
    let datos = vec![1, 2, 3];
    let ptr = datos.as_ptr();
    drop(datos); // libera la memoria
    // println!("{}", *ptr); // use-after-free: Miri lo detecta
}

Cuándo (y cuándo no) usar unsafe

  • : implementar estructuras de datos que la stdlib no cubre (listas intrusivas, arenas).
  • : llamar a librerías C con bindings FFI.
  • : optimizaciones de bajo nivel donde el análisis manual garantiza la corrección.
  • No: como forma de "saltarse" el borrow checker cuando el diseño puede corregirse.
  • No: traducción literal de código C sin revisar los invariantes.

Resumen

  • Un bloque unsafe habilita exactamente cinco capacidades; el resto de las garantías del compilador permanecen intactas.
  • Los raw pointers se crean en código safe pero solo se desreferencian en unsafe.
  • Una función unsafe exige al caller que documente y cumpla los invariantes de seguridad.
  • Para statics mutables, prefiere AtomicU32 u otros tipos atómicos en lugar de static mut desnudo.
  • Usa Miri en tu suite de tests para detectar comportamiento indefinido en bloques unsafe.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP