Los bugs que Rust no puede detectar
(corrode.dev)- La seguridad de memoria mejora mucho, pero incluso en código Rust de producción siguen existiendo problemas en el manejo de límites del sistema, que pueden terminar en vulnerabilidades
- Los flujos que vuelven a interpretar la misma ruta en varios syscalls, los métodos que cambian permisos después de crear un archivo y la comparación de rutas basada en cadenas facilitan problemas como TOCTOU y exposición de privilegios
- En Unix, las rutas, variables de entorno y datos de streams circulan como bytes sin procesar, por lo que un manejo centrado en
Stringo el uso defrom_utf8_lossy,unwrapyexpectpuede causar corrupción de datos o DoS - Si se descartan errores, un fallo puede parecer éxito, y las diferencias de comportamiento con GNU coreutils pueden convertirse de inmediato en problemas de seguridad en scripts de shell y herramientas con privilegios
- En esta auditoría no aparecieron bugs de seguridad de memoria como buffer overflow, use-after-free o double-free; el riesgo principal restante estaba concentrado no dentro de Rust, sino en los límites que lo conectan con el mundo exterior
Los límites de Rust que reveló la auditoría
- Los 44 CVE de uutils publicados por Canonical muestran que incluso en código Rust de producción pueden quedar vulnerabilidades que borrow checker, clippy y cargo audit no detectan
- El centro del problema no estuvo en la seguridad de memoria, sino en el manejo de límites del sistema
- Había una brecha de tiempo entre la ruta y el syscall
- Los datos en bytes de Unix y las cadenas UTF-8 no coincidían
- Había diferencias de comportamiento con la herramienta original
- Faltaba manejo de errores y había terminaciones por
panic!
- Esta lista de CVE muestra de forma condensada dónde termina la seguridad en el código de sistemas escrito en Rust
Interpretar una ruta dos veces genera TOCTOU
- Si se verifica la misma ruta en un syscall y luego se vuelve a operar sobre ella en el siguiente, es fácil terminar en una vulnerabilidad TOCTOU
- Entre ambas llamadas, un atacante con permiso de escritura en el directorio padre puede cambiar componentes de la ruta por un enlace simbólico
- En la segunda llamada, el kernel vuelve a interpretar la ruta desde cero y una operación privilegiada puede dirigirse al objetivo elegido por el atacante
- La API
std::fsde Rust usa por defecto una reinterpretación basada en&Path, lo que facilita este tipo de erroresfs::metadata,File::create,fs::remove_fileyfs::set_permissionsvuelven a interpretar la ruta en cada llamada- En herramientas privilegiadas que deben bloquear a atacantes locales, este comportamiento por defecto se vuelve peligroso
- En
CVE-2026-35355, se explotó un flujo que borra un archivo y luego crea uno nuevo en la misma ruta- En
src/uu/install/src/install.rs, después defs::remove_file(to)?veníaFile::create(to)? - Si entre el borrado y la creación
tose cambia por un enlace simbólico que apunta a/etc/shadowo a otro objetivo, un proceso privilegiado puede sobrescribir ese archivo
- En
- La corrección cambió a
OpenOptions::create_new(true)para que solo cree archivos nuevos- Según la documentación,
create_newno permite ni archivos existentes ni dangling symlink en la ubicación de destino
- Según la documentación,
- Si se necesita operar dos veces sobre la misma ruta, es más seguro anclar la operación al file descriptor
- Fuera de la creación de archivos nuevos, lo correcto suele ser abrir una vez el directorio padre y trabajar con rutas relativas respecto a ese handle
- Si una misma ruta se usa dos veces, debe asumirse que hay TOCTOU hasta que se demuestre lo contrario
Los permisos no deben modificarse después de crear, sino definirse al crear
- Crear un directorio o archivo con permisos por defecto y luego aplicar
chmodtambién genera una breve ventana de exposición- Si se escribe
fs::create_dir(&path)?y luegofs::set_permissions(&path, Permissions::from_mode(0o700))?, durante ese intervalopathexiste con los permisos por defecto - Otros usuarios pueden hacer
open()en esa ventana, y aunque luego se apliquechmod, los file descriptors ya obtenidos no se recuperan
- Si se escribe
- Los permisos deben definirse al momento de crear
- Hay que usar
OpenOptions::mode()yDirBuilderExt::mode()para que nazcan con los permisos deseados - El kernel además aplica
umask, así que si ese efecto también importa, debe manejarse explícitamente
- Hay que usar
Comparar cadenas de ruta no equivale a igualdad en el sistema de archivos
- La verificación inicial de
--preserve-rootenchmodsolo hacía una comparación de cadenasrecursive && preserve_root && file == Path::new("/")- Entradas que en realidad apuntan a la raíz, como
/../,/./,/usr/..o un enlace simbólico hacia/, pero que no son literalmente/, podían saltarse la verificación
- La corrección cambió a comparar después de resolver la ruta real absoluta con
fs::canonicalize- PR de corrección
canonicalizedevuelve la ruta real tras resolver..,.y enlaces simbólicos
- En el caso de
--preserve-root, este método funciona porque/no tiene directorio padre - Para comparar en general si dos rutas arbitrarias apuntan al mismo objeto del sistema de archivos, no se deben comparar cadenas, sino
(dev, inode)- GNU coreutils también usa este enfoque
- En
CVE-2026-35363,rmrechazaba.y.., pero permitía./y.///, lo que hacía posible borrar el directorio actual- Si las diferencias de entrada se manejan solo a nivel de cadena, es fácil esquivar la verificación
En los límites de Unix, los bytes deben tener prioridad sobre las cadenas
Stringy&stren Rust siempre son UTF-8, pero en Unix las rutas, variables de entorno, argumentos y datos de streams viven en un mundo de bytes sin procesar- Elegir mal al cruzar ese límite lleva a dos tipos de bugs
- Las conversiones con pérdida como
from_utf8_lossyreemplazan bytes inválidos porU+FFFDy corrompen datos en silencio - Las conversiones estrictas como
unwrapo?pueden rechazar la entrada o terminar el proceso
- Las conversiones con pérdida como
- El
CVE-2026-35346decommfue un caso donde la conversión con pérdida arruinó la salida- En
src/uu/comm/src/comm.rs, los bytes de entradarayrbse convertían conString::from_utf8_lossyy luego se imprimían conprint! - GNU
commconserva los bytes tal cual incluso en archivos binarios, pero uutils reemplazaba el UTF-8 inválido porU+FFFD, dañando la salida - La corrección fue usar
BufWriterywrite_allpara escribir los bytes sin procesar directamente astdout
- En
print!obliga a un viaje de ida y vuelta por UTF-8 al pasar porDisplay, peroWrite::write_allno- En código de sistemas Unix conviene usar el tipo adecuado para cada caso
- Si se pasa por
Stringpor comodidad de formateo, es fácil introducir corrupción de datos
Todo panic puede convertirse en una denegación de servicio
- En una CLI,
unwrap,expect, indexación de slices, aritmética sin comprobaciones yfrom_utf8pueden convertirse en puntos de DoS cuando el atacante controla la entradapanic!hace unwind del stack y detiene el proceso- Si corre dentro de un cron job, CI pipeline o script de shell, puede detener toda la tarea
- En entornos de ejecución repetida, incluso puede provocar un crash loop que inutilice el sistema completo
- El
CVE-2026-35348desort --files0-fromabortaba al encontrar nombres de archivo no UTF-8 en una lista separada por NUL- El parser llamaba
std::str::from_utf8(bytes).expect(...)sobre los bytes de cada nombre - GNU
sorttrata los nombres de archivo como bytes sin procesar, igual que el kernel, pero uutils forzaba UTF-8 y detenía el proceso completo al primer path no UTF-8
- El parser llamaba
- En código que procesa entradas no confiables,
unwrap,expect, indexación y casts conasdeben verse como CVE potenciales- Conviene usar
?,get,checked_*ytry_from, y propagar el error real al caller
- Conviene usar
- También se proponen reglas de clippy para detectarlo en CI
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_side_effects
- En código de pruebas, estas advertencias pueden ser excesivas, así que es razonable limitarlas al ámbito
cfg(test)
Si se descartan errores, un fallo puede parecer éxito
- Algunos CVE surgieron por ignorar errores o por flujos donde la información del error se pierde
chmod -Rychown -Rdevolvían solo el código de salida del último archivo de toda la operación- Aunque fallaran muchos archivos antes, si el último salía bien, el proceso podía terminar con
0 - El script interpretaría erróneamente que todo el trabajo terminó sin problemas
- Aunque fallaran muchos archivos antes, si el último salía bien, el proceso podía terminar con
ddllamabaResult::ok()sobre el resultado deset_len()para imitar el comportamiento de GNU sobre/dev/null- La intención era descartar el error solo en una situación limitada, pero el mismo código terminó aplicándose también a archivos normales
- Incluso con el disco lleno, podía quedar silenciosamente un archivo de destino escrito a medias
- Si se descarta un
Resultcon.ok(),.unwrap_or_default(), olet _ =, se pierden causas de fallo importantes - Aunque no se aborte en el primer error, hay que recordar el código de error más grave y terminar con él
- Si de verdad se debe descartar un
Result, hay que dejar en el código la razón por la que ese fallo puede ignorarse con seguridad
La compatibilidad exacta con la herramienta original también es una función de seguridad
- Varios CVE no aparecieron porque el código hiciera operaciones peligrosas, sino porque se comportaba distinto a GNU
- Los scripts de shell reales dependen del comportamiento original de GNU, así que una diferencia de significado puede convertirse en un problema de seguridad
- Un ejemplo representativo es el
CVE-2026-35369dekill -1- GNU interpreta
-1como signal 1 y exige un PID - uutils lo interpretaba como enviar la señal por defecto al PID -1
- En Linux, PID -1 significa todos los procesos visibles, así que un simple error tipográfico podía terminar matando el sistema completo
- GNU interpreta
- En herramientas reimplementadas, la compatibilidad bug-for-bug funciona como una barrera de seguridad que incluye códigos de salida, mensajes de error, edge cases y significado de opciones
- En cada punto donde el comportamiento difiere de GNU, aumenta la posibilidad de que un script de shell tome una decisión equivocada
- Ahora uutils también ejecuta en CI la suite de pruebas upstream de GNU coreutils
- Parece una defensa del tamaño adecuado para bloquear este tipo de diferencias
Hay que resolver antes de cruzar el límite de confianza
- El
CVE-2026-35368fue una ejecución local de código como root enchroot - El patrón del problema estuvo en hacer
chroot(new_root)?y luego resolver nombres de usuario dentro de la nueva raíz controlada por el atacanteget_user_by_name(name)?terminaba leyendo bibliotecas compartidas del sistema de archivos de la nueva raíz para resolver el nombre de usuario- Si el atacante colocaba archivos dentro del chroot, eso podía llevar a ejecución de código con uid 0
- GNU
chrootresuelve al usuario antes de hacerchroot- La corrección también cambió a ese mismo orden
- Una vez que se cruza un límite de confianza, cada llamada de biblioteca puede terminar ejecutando código del atacante
- El enlace estático tampoco evita este problema
get_user_by_namepasa por NSS y puede hacerdlopende móduloslibnss_*en tiempo de ejecución
Los bugs que Rust sí evitó en la práctica
- También es claro qué tipos de bugs no se encontraron en esta auditoría
- No hubo buffer overflow
- Tampoco use-after-free
- Tampoco double-free
- Tampoco data races por estado mutable compartido
- Tampoco null-pointer dereference
- Tampoco uninitialized memory read
- Aunque las herramientas tenían bugs, en los resultados de la auditoría no apareció ninguno explotable como lectura arbitraria de memoria
- GNU coreutils sí ha seguido teniendo en los últimos años CVE de seguridad de memoria de este tipo
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nescritura de NUL fuera del heap buffersortlectura de 1 byte antes del heap buffersplit --line-bytesheap overwrite en CVE-2024-0684b2sum --checklectura de memoria no asignada con entrada malformadatail -fstack buffer overrun
- En la comparación para el mismo período, la reimplementación en Rust mantuvo 0 bugs de esa categoría
- Eso sí, la auditoría no demuestra la ausencia de bugs de seguridad de memoria; solo significa que no se encontraron
- Los problemas restantes aparecen sobre todo en los límites que conectan con el mundo exterior, más que dentro de Rust
- rutas
- bytes y cadenas
- syscall
- diferencia temporal y cambios de estado del sistema de archivos
Rust correcto también es Rust idiomático
- El Rust idiomático no consiste solo en pasar el borrow checker y tener un código donde
clippyno se queje - La corrección también debe ser parte de lo idiomático
- Porque las formas de código que sobreviven en el mundo real se consolidan a partir de la experiencia de la comunidad
- Un sistema robusto no debe ocultar el desorden del mundo real, sino reflejarlo tal cual
- file descriptors en vez de rutas
Stringen vez deOsStr?en vez deunwrap- compatibilidad bug-for-bug con el original antes que una semántica que solo se vea más limpia
- El sistema de tipos puede expresar muchas cosas, pero no condiciones fuera de su control, como el paso del tiempo entre dos syscalls
- El Rust idiomático debe hacer que los tipos, nombres y flujo de control del código expongan la verdad del entorno de ejecución
- Aunque sea menos bonito que el código elegante de pizarrón, hace falta una forma más honesta
Material de referencia
- An update on rust-coreutils: publicación de los resultados de la auditoría
- Patterns for Defensive Programming in Rust: patrones de Rust defensivo para leer junto con esto
- Pitfalls of Safe Rust: errores comunes que pueden aparecer incluso en safe Rust
- Sharp Edges In The Rust Standard Library: comportamientos inesperados de
std - uutils/coreutils on GitHub: GNU coreutils reimplementado en Rust
Aún no hay comentarios.