Guía completa de Rust para desarrolladores: seguridad de memoria, FFI y errores comunes

rust Guía completa de Rust para desarrolladores: seguridad de memoria, FFI y errores comunes

Guía completa de Rust para desarrolladores: seguridad de memoria, FFI y errores comunes

Objetivo: entender por qué Rust es seguro por defecto, cuáles son los riesgos reales cuando usas unsafe o FFI, y aplicar prácticas y herramientas concretas para auditar y endurecer tu código.

1. Resumen rápido de garantías de Rust

  • Ownership y borrowing: enlazan tiempo de vida y acceso mutuo/compartido en tiempo de compilación.
  • Tipos Send y Sync: determinan qué puede cruzar hilos y cómo se sincroniza.
  • Sin data races en código seguro: las reglas del compilador evitan condiciones de carrera en código safe.
  • unsafe abre la puerta a UB: cualquier invariantes pueden romperse si no se documentan y verifican.

2. Vectores de riesgo más comunes

  • Uso de unsafe sin acotar el alcance ni documentar invariantes.
  • FFI: pasar ownership o referencias que incumplen lifetimes/alinamiento/representación.
  • Panics atravesando fronteras FFI (UB).
  • Mutable statics (static mut) y malas sincronizaciones.
  • Interior mutability mal usada (RefCell en entornos multi-hilo, UnsafeCell mal documentado).
  • Integer overflows asumidos: comportamiento en release puede ser wrapping.

3. Reglas prácticas: cómo escribir unsafe correctamente

  1. Mantén los bloques unsafe lo más pequeños posible.
  2. Documenta exactamente las invariantes que haces valer dentro del bloque (alineamiento, no-null, exclusividad, etc.).
  3. Proporciona APIs seguras que encapsulen unsafe y prueben las invariantes.
  4. Escribe tests que validen límites, condiciones de borde y comportamiento concurrente.
  5. Revisa unsafe con al menos otra persona y con herramientas automáticas.

4. FFI: reglas y plantillas seguras

Cuando te comunicas con C (o cualquier otra ABI), respeta estas reglas:

  • Usa #[repr(C)] en structs compartidos para garantizar layout.
  • No permitas que panics crucen la frontera: captura con std::panic::catch_unwind o configura abort.
  • Usa CString/CStr para cadenas; nunca pases &str directamente.
  • Define claramente ownership: quién libera memoria, cuándo y cómo.
  • Para callbacks, evita capturar referencias temporales; usa contexto con punteros crudos y validaciones.

Ejemplo minimal safe wrapper para FFI

// src/ffi.rs
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::panic::catch_unwind;

#[repr(C)]
pub struct RawPoint {
    x: i32,
    y: i32,
}

#[no_mangle]
pub extern "C" fn create_point(x: i32, y: i32) -> *mut RawPoint {
    Box::into_raw(Box::new(RawPoint { x, y }))
}

#[no_mangle]
pub extern "C" fn destroy_point(ptr: *mut RawPoint) {
    if ptr.is_null() { return; }
    unsafe { Box::from_raw(ptr); } // se droppea
}

// Función expuesta que captura panics y devuelve código de error
#[no_mangle]
pub extern "C" fn process_safe(s: *const c_char) -> i32 {
    let res = catch_unwind(|| {
        if s.is_null() { return -1; }
        let cstr = unsafe { CStr::from_ptr(s) };
        let rust_str = cstr.to_str().map_err(|_| -2)?;
        // ... lógica que puede panic
        println!("Procesando: {}", rust_str);
        Ok(0)
    });

    match res {
        Ok(Ok(code)) => code,
        Ok(Err(code)) => code,
        Err(_) => -100, // panic atrapado
    }
}

//Wrapper seguro para uso desde Rust
pub struct Point { raw: *mut RawPoint }
impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        let raw = create_point(x, y);
        Point { raw }
    }
    pub fn coords(&self) -> (i32, i32) {
        assert!(!self.raw.is_null());
        unsafe { let r = &*self.raw; (r.x, r.y) }
    }
}
impl Drop for Point {
    fn drop(&mut self) {
        if !self.raw.is_null() { destroy_point(self.raw); }
    }
}

Por qué esto es seguro: encapsulamos punteros crudos en un tipo Rust con Drop, validamos null, y capturamos panics para que no crucen la frontera ABI.

5. Concurrencia: Send, Sync y patrones seguros

  • Regla rápida: si un tipo es Send, puede moverse entre threads; si es Sync, puede ser referenciado desde múltiples threads.
  • Evita static mut; usa Mutex, RwLock, Atomic* o estructuras de sincronización correctas (Arc, parking_lot).
  • Para rendimiento: usa primitivas de concurrencia sin bloqueo donde tiene sentido (atomics, crossbeam).
  • Si implementas manualmente Send/Sync para un tipo unsafe, documenta y prueba la semántica de sincronización.

Memoria y ordenamiento

Con atomics, entiende al menos Relaxed, Acquire, Release y SeqCst. Empieza con SeqCst para corrección, luego optimiza si necesitas throughput.

6. Herramientas imprescindibles

  • Miri: detecta comportamientos indefinidos en tiempo de ejecución. Uso: cargo +nightly miri test.
  • Sanitizers (ASan/TSan): detectar use-after-free y data races. Ejemplo: RUSTFLAGS="-Z sanitizer=address" cargo +nightly test (requiere nightly y setup).
  • cargo-audit: escanea dependencias por vulnerabilidades conocidas: cargo install cargo-audit y cargo audit.
  • cargo-fuzz: fuzzing de APIs públicas. cargo install cargo-fuzz, escribir targets de fuzzing.
  • clippy: lints de calidad y seguridad: cargo clippy -- -D warnings.
  • bindgen: generar bindings C automáticamente, revisar el código generado.
  • cargo-deny y cargo-geiger para políticas y detección de crates con unsafe.

7. Checks y comandos prácticos

  • Revisar unsafe en el repo: grep -R "unsafe" src | sed -n '1,200p'
  • Miri: rustup toolchain install nightly && cargo +nightly miri test
  • ASan: RUSTFLAGS="-Z sanitizer=address" cargo +nightly test (configura linking con runtime de sanitizer)
  • Fuzz: cargo fuzz run my_target
  • Auditar dependencias: cargo audit

8. Errores que veo una y otra vez (y cómo evitarlos)

  • No documentar invariantes de unsafe: crea tests unitarios que verifiquen esas invariantes.
  • Permitir panics en callbacks C: atrapa panics y tradúcelos a códigos de error.
  • Asumir que Vec::into_raw_parts u otras APIs son triviales al cruzar FFI: define claramente ownership y tamaño/ capacidad.
  • Implementar Send/Sync manualmente sin pruebas: añade stress tests y fuzzing multihilo.

9. Estructura de proyecto recomendada

my-crate/
├─ src/
│  ├─ lib.rs        # exports safe API
│  ├─ ffi.rs        # funciones extern "C" (pequeñas, documentadas)
│  ├─ unsafe_helpers.rs # bloques unsafe bien acotados
│  └─ sync.rs       # abstracciones de concurrencia
├─ tests/           # integration tests, stress tests
├─ fuzz/            # targets para cargo-fuzz
├─ examples/        # pequeños ejemplos de uso
├─ CI/              # scripts CI para miri, sanitizers y cargo-audit
└─ README.md

10. Buenas prácticas rápidas (lista de verificación)

  • ¿Encapsulas todo unsafe detrás de APIs seguras? Sí / No
  • ¿Has documentado invariantes y precondiciones? Sí / No
  • ¿Has corrido Miri y sanitizers en CI? Sí / No
  • ¿Tienes fuzz targets para los límites de entrada? Sí / No
  • ¿Revisas dependencias con cargo-audit? Sí / No

11. Herramientas avanzadas y siguientes pasos

  • Considera Prusti o Creusot para comprobaciones formales sobre invariantes si tu dominio lo requiere.
  • Fuzzing intensivo (libfuzzer/cargo-fuzz) en parsers y deserializadores.
  • Auditorías externas para código unsafe crítico o librerías públicas.

Consejo avanzado: automatiza un pipeline de CI que ejecute clippy, cargo-audit, Miri y fuzzing básico en PRs que tocan código unsafe. Advertencia: los sanitizers y Miri pueden ser costosos en tiempo; haz que el pipeline tenga niveles (fast/slow) para PRs vs nightlies.

¿Quieres que te genere plantillas concretas (CI, un ejemplo completo de FFI con C y tests, o configuración de sanitizers paso a paso) para tu repositorio? Puedo generarlas y ajustarlas a tu flujo de trabajo.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión