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
1 comentarios
Opiniones en Hacker News
Como mantenedor de GNU Coreutils, leí el artículo con interés, pero en el Rust que he usado un poco, era demasiado fácil crear una race TOCTOU con
std::fsOjalá eventualmente entre a la biblioteca estándar una API parecida a
openatY tampoco estoy de acuerdo con la regla de resolver la ruta antes de compararla
En general, es mejor llamar a
fstaty compararst_devyst_ino, y el artículo sí mencionaba algo de esoUn efecto secundario menos considerado es el costo en rendimiento
Como ejemplo real, en una ruta de directorio muy profunda
cptardó 0.010 segundos, mientras queuu_cptardó 12.857 segundosPuede que en la práctica rara vez se creen rutas así a propósito, pero el software GNU se esfuerza muchísimo por evitar límites arbitrarios
https://www.gnu.org/prep/standards/standards.html#Semantics
Y en el artículo dicen que la reescritura en Rust tuvo cero bugs de seguridad de memoria durante un periodo similar, pero eso no es cierto :)
https://github.com/advisories/GHSA-w9vv-q986-vj7x
Sí,
std::fstiene un problema de lowest common denominatorHabía que meter algo en Rust 1.0 y, por desgracia, ese estado quedó congelado por mucho tiempo
Creo que
uutilses un buen lugar para intentar diseñar una API alternativa a std::fs en la que sea más difícil equivocarseGracias por explicar este punto de vista de forma tan concisa desde el otro lado
Quisiera preguntar qué se debería aprender aquí
Para ser una publicación de internet, lo pregunto de forma deliberadamente algo agresiva, porque con contraste se ven más claramente las diferencias y los errores
Claro, no tienes ninguna obligación de gastar tiempo ni energía mental en responder
Me intriga por qué siempre terminan apareciendo juntos velocidad, rendimiento, race conditions y
st_inoParece que cosas como la latencia, escribir realmente al dispositivo de almacenamiento, la atomicidad, ACID y la velocidad finita de transmisión de información convergen al final en una esencia parecida
Da la impresión de que sistemas de alta confiabilidad, como la contabilidad, al final tienen que irse por ACID, mientras que los sistemas poco confiables se olvidan tan rápido que casi parece que las diferencias entre computadoras no importan tanto
También me pregunto si en aplicaciones cotidianas el throughput de verdad importa más que la latency
Y entiendo por la historia de C, los Unix-like y GNU coreutils por qué se pone el foco en los números de inode,
pero me pregunto qué pasaría si uno mira un ejemplo muy básico como lograr que una memoria USB simplemente funcione bien para guardar archivos
sin evitar complejidades como el buffering de I/O en
libc,fflush, el buffering del kernel, multicore, time-sharing y múltiples aplicaciones corriendo al mismo tiempoSoy totalmente principiante, pero me preguntaba por qué no simplemente hacían
cddirecto con$(yes a/ | head -n $((32 * 1024)) | tr -d '\n')en vez de necesitar un buclewhileEdit: ya entendí. Era por
-bash: cd: a/a/a/....../a/a/: File name too longNo sé si ya lo viste, pero hay una demo para convertir automáticamente utilidades GNU como
wgeta un subconjunto seguro de C++https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget
Como reemplaza casi 1:1 los elementos peligrosos de C por elementos seguros de C++ con comportamiento correspondiente, parece menos probable que introduzca nuevos bugs y nuevas diferencias de comportamiento que una reescritura
Si se ordena un poco el código original, la conversión podría automatizarse por completo, así que en la etapa de build se podría generar desde el código C original un ejecutable un poco más lento, pero seguro en memoria
Puede que sea una pregunta tonta, pero me pregunto si del lado de GNU Coreutils han considerado o planean una reescritura propia en Rust
Tal vez sabían usar Rust, pero no estaban lo bastante familiarizados con la API de Unix y su semántica y trampas
La mayoría de esos errores entran en una categoría bastante de principiante desde la perspectiva de desarrolladores veteranos de GNU coreutils, BSD o Solaris
Muchos de esos problemas ya salieron a la luz y se depuraron hace décadas, y aunque en los codebases existentes todavía quedan correcciones de cola larga, ahora por lo general solo siguen entrando en pequeñas cantidades
Leí ese hilo de Canonical y de verdad me dejó pasmado
El mensaje era más o menos: “Rust es más seguro, la seguridad es prioridad máxima, así que distribuir urgentemente una reescritura completa de coreutils es necesario. Si algo se rompe, no importa, se arregla después”
Yo no quiero correr en mi máquina código hecho por gente que piensa así
Yo también estoy a favor de Rust, pero que Rust sea más seguro solo aplica si todo lo demás es igual
Aquí todo lo demás no era igual en absoluto
Una reescritura inevitablemente va a tener muchísimos más bugs y vulnerabilidades que código mantenido por décadas, así que el argumento de seguridad puede tener sentido como estrategia de transición a largo plazo, pero no sirve como justificación para un despliegue apresurado
Minimizar el impacto en usuarios después del despliegue, o decir “así salen a la luz los bugs” o “los coreutils existentes tampoco tenían buenas pruebas”, es demasiado irresponsable
Los usuarios no son conejillos de indias
Creo que los mantenedores tienen la responsabilidad moral de no dañar la confiabilidad de los sistemas de sus usuarios
Más fundamentalmente que eso, parece que la biblioteca estándar de Rust empuja a los desarrolladores hacia una API limpia en el nivel de abstracción equivocado
Por ejemplo, hacia operaciones basadas en rutas en vez de operaciones de archivo basadas en handles
Ojalá me equivoque
Para mí, la gracia de Rust está en hacer que no tengas que preocuparte por las trampas más grandes y más fáciles de pisar
Y parece que el punto central de este artículo es que las API del sistema de archivos deberían cumplir ese mismo papel
Alguien acuñó una expresión parecida: disassembler rage
La idea es que, si miras algo lo bastante de cerca, cualquier error parece amateur
También viene de esa actitud de mirar solo el desensamblador y reprocharle a un programador de alto nivel por qué usó
ifen vez deswitchdentro de una función 100 frames abajo en el call stackAhora mismo solo estamos viendo unas pocas cosas en las que se equivocaron, y casi no vemos las miles de líneas de código correctamente escrito que las rodean
Que en utilidades así haya
panicya es un error bastante amateur, incluso bajo los estándares de RustSi fuera algo como un error de alloc imposible de manejar, bueno, pero
expectyunwrapson difíciles de justificar salvo que estés garantizando de forma realmente estricta un invariante que haga imposible recorrer ese camino de códigoUna de las cosas difíciles al reescribir código es que el original se fue deformando gradualmente para responder a problemas que solo aparecieron en entornos operativos reales
Las lecciones aprendidas en ese proceso se infiltran silenciosamente en el código y, si no están documentadas, hay una enorme cantidad de trabajo oculto antes de poder llegar a un nivel equivalente
El texto original muestra muy bien justamente ese tipo de lista
Aun así, antes de llamar amateur a alguien de inmediato, también hay que ver que esto es uno de los fenómenos más propios del software
Si no ignoraron documentación técnica realmente buena y pruebas que cubrieran esos casos en coreutils, esto era casi inevitable
Un buen ejemplo del artículo es el CVE de chroot + NSS
Que NSS sea dinámico y que dentro de
chrootcargue librerías condlopenno es algo que esté escrito de forma visible en ninguna parteEs más bien una de esas cosas que los administradores de sistemas aprendieron a golpes por más de 25 años, y una reescritura clean-room normalmente vuelve a aprenderlo como un CVE nuevo
Si portas el mismo código con un LLM, la situación sería parecida
Puedes leer las firmas de función, pero lo que de verdad necesitas son las heridas y cicatrices que quedaron en ese código
Si además haces este trabajo tratando de evitar la GPL sin siquiera leer el código fuente original, se vuelve más difícil todavía
En mi opinión,
uutilshabría estado mucho mejor si hubiera sido GPL y hubiera podido inspirarse directamente en el código fuente original de coreutilsTambién hay que dejar claro que no documentar estas lecciones, o al menos los bugs y vulnerabilidades que se intentaban evitar, es una mala práctica
Claro que es difícil dejar por escrito todos los bugs que se evitan implícitamente solo por haber escrito bien el código, pero
para futuros lectores importa dejar explicaciones como “aquí se usa
fooen vez debarporque con la condición ABC, usarbargenera el peligrosobazpor XYZ”Aunque parezca un pequeño desperdicio de tiempo y espacio en la documentación, me parece mejor así
Siento que muchas de las cosas señaladas en este artículo, sobre todo al compararlas con el código fuente de GNU coreutils, deberían haber sido detectadas por cualquier unit test razonable o revisión manual
Reescribir coreutils parece una idea terrible
https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
y parece que se hizo de la forma equivocada, sin lograr incorporar suficiente conocimiento acumulado del software anterior
Si vas a reescribir, tienes que entender por completo y aprender del trabajo previo
Si no, solo vas a repetir los mismos errores y, francamente, da bastante pena ajena
Para que quede claro, me gusta Rust, lo uso en varios proyectos y es excelente
Pero Rust no te salva de la mala ingeniería
Curiosamente,
uutilssí usa la suite de pruebas de GNU coreutilsAdemás, tienen explícito que no aceptarán contribuciones escritas a partir de leer el código GPL
Si viene de la gente que hizo
unity,upstartysnap, esto entra totalmente dentro de lo esperableA los nuevos programadores de sistemas habría que darles la bienvenida así:
Unix está roto, al final te toca escribir rodeos feos y nada educativos, y además hacer pruebas empíricas
Así es como funcionan el software confiable y la buena ingeniería de software
Me pregunto por qué el differential fuzzing no detectó estos bugs
https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz
El patrón de verificar una ruta con una syscall y luego volver a hacer syscall sobre esa misma ruta para operar sobre ella siempre trae el mismo problema
Un atacante con permisos de escritura sobre el directorio padre puede sustituir entre ambas llamadas componentes de la ruta por enlaces simbólicos, y el kernel volverá a resolver la ruta desde cero en la segunda llamada, haciendo que la operación privilegiada termine sobre el objetivo elegido por el atacante
Un atacante con permisos de escritura sobre el directorio padre también puede hacer maniobras con hardlinks
Incluso si solo puede tocar archivos regulares, en realidad casi no hay mitigaciones decentes
Como ejemplo, ver https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
Parece que la causa raíz de varios bugs es que las API de Unix son demasiado opacas
Por ejemplo, que
get_user_by_namecargue shared libraries dentro del nuevo sistema de archivos raíz para resolver un nombre de usuario, y que por eso un atacante que puede plantar archivos dentro delchroottermine ejecutando código como uid 0, se siente casi como una booby trapQue una función para obtener datos de usuario de pronto también cargue shared libraries parece un diseño con responsabilidades mezcladas
Creo que la consulta de datos de usuario y la carga de librerías deberían separarse a nivel de función, o al menos el nombre debería dejar claro que eso pasa
Puede que en parte sí, pero si decidiste reescribir coreutils desde cero, entender la API de POSIX es literalmente parte central del trabajo
Además, si el código que verificaba si una ruta apuntaba a la raíz del sistema de archivos era
file == Path::new("/"), entonces ese no es un problema de APIQuien escribió eso casi no parece apto para participar en este proyecto
Más bien, creo que usar un lenguaje seguro funcional puede hacer que la gente imagine erróneamente que los datos que manipula también son inmutables
Pero en un sistema operativo muchísimas cosas están cambiando todo el tiempo
Hasta que no aparecieron sistemas de archivos con snapshots, había que volver a comprobar todo constantemente
Al final lo que se necesita es una API que, si recibe una entrada, devuelva un resultado exitoso o un fallo, pero no una API que devuelva una de tres cosas: éxito, fallo o error
Sí,
musl libcjustamente elimina una de esas partesCreo que la causa raíz no es tanto la opacidad de las API de Unix, sino no haber pensado bien el caso de hacer chroot como root hacia un directorio que no controlas
Cualquier cosa a la que le hagas
chrootqueda bajo el control de quien armó ese chroot, y si no entiendes eso, no deberías usarchroot()get_user_by_namepuede sentirse como una trampa, pero en realidad hay muy poca diferencia práctica entre usarnewroot/etc/passwdy usarnewroot/usr/lib/x86_64-linux-gnu/libnss_compat.so,newroot/bin/shy cosas asíPor eso creo que
/usr/sbin/chrootnunca debería tener motivo para consultar IDs de usuariotoybox chrootno lo haceAl final, el bug no fue la forma en que se hizo algo mal, sino haber hecho esa operación desde el principio
Unix y POSIX están llenos de trampas como un fractal, sin importar por dónde los cortes
Aunque la gente de Rust hubiera reescrito coreutils sin experiencia en Linux, lo que menos entiendo es cómo Ubuntu terminó aceptándolo en mainline
Ubuntu parece tener la política de cambiar en casi cada release alguna pieza base del sistema por un experimento incompleto y mal hecho
El punto importante aquí no es “vaya, había bugs en código Rust”, sino justamente eso
El original tiene licencia GPL y la reescritura tiene licencia MIT
Si es verdad eso de que “estos bugs salieron de código Rust realmente desplegado y sus autores sabían lo que hacían”,
me pregunto si significa que las utilidades originales no tenían test harness y que la reescritura tampoco empezó por construir uno
Incluso si hay muchos edge cases, parecería posible abstraer hasta cierto punto el OS y el FS para verificar cosas como que
rm .//realmente no borre el directorio actual cuando no debeMás que un problema de código sucio o una crítica al lenguaje, esto parece otra vez esa vieja actitud de que en programación de sistemas no se hacen pruebas
En cambio, si las utilidades originales sí tenían pruebas y aun así había tantos huecos, quizá la suite de pruebas original en sí era bastante insuficiente
Eso creo
Pero no estoy tan convencido de que se pueda abstraer el OS y el FS lo suficiente como para verificarlo
porque la gente lleva intentándolo desde antes de que yo naciera y no parece haberlo logrado todavía
Por ejemplo, ya desde decidir cuántos
/vas a poner en una prueba la cosa se vuelve ambiguaY más allá de eso, si asumimos que
rmse niega a borrar un archivo si sus primeros 9 bytes sonimportant,cuesta imaginar cómo diseñarías una prueba que detecte ese comportamiento sin saber antes que existe esa cadena
Y si esa palabra mágica ni siquiera está en el diccionario, peor todavía
Casi nunca he visto a alguien decir en serio que “en programación de sistemas no se hacen pruebas”
Lo que sí escuché muchas veces es que las pruebas no siempre cumplen el papel que la gente espera de ellas
Según entiendo, durante el desarrollo de
uutilssí hubo pruebas extensivas de comparación de comportamiento con las utilidades originales, e incluso intentaron preservar hasta los bugsPor una de estas razones, Windows desactiva por defecto los symlinks
No los resuelve con una abstracción, sino prácticamente eliminando la función
En el mundo Unix eso no se puede hacer porque durante décadas ha habido demasiado software que depende de symlinks
MacOS tiene una respuesta parecida
Por ejemplo, el bug de
chroot()en la práctica no suele ser gran problema con la configuración por defecto, porque MacOS bloqueachroot()por defectoPara usarlo tienes que desactivar system integrity protection
El problema de fondo está en las aristas filosas de la API POSIX, y la solución está menos en abstraerlas que en eliminarlas
Me parece bien que la gente experimente e intente cosas torpemente
Así es como se aprende y se crece
Lo que de verdad me da curiosidad es cómo se rompió la cadena de toma de decisiones en Ubuntu para que algo así llegara a producción