1 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • 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 String o el uso de from_utf8_lossy, unwrap y expect puede 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::fs de Rust usa por defecto una reinterpretación basada en &Path, lo que facilita este tipo de errores
  • 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 de fs::remove_file(to)? venía File::create(to)?
    • Si entre el borrado y la creación to se cambia por un enlace simbólico que apunta a /etc/shadow o a otro objetivo, un proceso privilegiado puede sobrescribir ese archivo
  • La corrección cambió a OpenOptions::create_new(true) para que solo cree archivos nuevos
    • Según la documentación, create_new no permite ni archivos existentes ni dangling symlink en la ubicación de destino
  • 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 chmod también genera una breve ventana de exposición
    • Si se escribe fs::create_dir(&path)? y luego fs::set_permissions(&path, Permissions::from_mode(0o700))?, durante ese intervalo path existe con los permisos por defecto
    • Otros usuarios pueden hacer open() en esa ventana, y aunque luego se aplique chmod, los file descriptors ya obtenidos no se recuperan
  • Los permisos deben definirse al momento de crear
    • Hay que usar OpenOptions::mode() y DirBuilderExt::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

Comparar cadenas de ruta no equivale a igualdad en el sistema de archivos

  • La verificación inicial de --preserve-root en chmod solo hacía una comparación de cadenas
    • recursive && 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
    • canonicalize devuelve 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, rm rechazaba . 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

  • String y &str en 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_lossy reemplazan bytes inválidos por U+FFFD y corrompen datos en silencio
    • Las conversiones estrictas como unwrap o ? pueden rechazar la entrada o terminar el proceso
  • El CVE-2026-35346 de comm fue un caso donde la conversión con pérdida arruinó la salida
    • En src/uu/comm/src/comm.rs, los bytes de entrada ra y rb se convertían con String::from_utf8_lossy y luego se imprimían con print!
    • GNU comm conserva los bytes tal cual incluso en archivos binarios, pero uutils reemplazaba el UTF-8 inválido por U+FFFD, dañando la salida
    • La corrección fue usar BufWriter y write_all para escribir los bytes sin procesar directamente a stdout
  • print! obliga a un viaje de ida y vuelta por UTF-8 al pasar por Display, pero Write::write_all no
  • En código de sistemas Unix conviene usar el tipo adecuado para cada caso
    • Para rutas de archivo: Path, PathBuf
    • Para variables de entorno: OsString
    • Para contenido de streams: Vec<u8> o &[u8]
  • Si se pasa por String por 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 y from_utf8 pueden convertirse en puntos de DoS cuando el atacante controla la entrada
    • panic! 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-35348 de sort --files0-from abortaba 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 sort trata 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
  • En código que procesa entradas no confiables, unwrap, expect, indexación y casts con as deben verse como CVE potenciales
    • Conviene usar ?, get, checked_* y try_from, y propagar el error real al caller
  • También se proponen reglas de clippy para detectarlo en CI
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_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 -R y chown -R devolví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
  • dd llamaba Result::ok() sobre el resultado de set_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 Result con .ok(), .unwrap_or_default(), o let _ =, 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-35369 de kill -1
    • GNU interpreta -1 como 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
  • 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-35368 fue una ejecución local de código como root en chroot
  • 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 atacante
    • get_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 chroot resuelve al usuario antes de hacer chroot
    • 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_name pasa por NSS y puede hacer dlopen de módulos libnss_* 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
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N escritura de NUL fuera del heap buffer
    • sort lectura de 1 byte antes del heap buffer
    • split --line-bytes heap overwrite en CVE-2024-0684
    • b2sum --check lectura de memoria no asignada con entrada malformada
    • tail -f stack 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 clippy no 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
    • String en vez de OsStr
    • ? en vez de unwrap
    • 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

1 comentarios

 
GN⁺ 4 시간 전
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::fs
    Ojalá eventualmente entre a la biblioteca estándar una API parecida a openat

    Y tampoco estoy de acuerdo con la regla de resolver la ruta antes de compararla
    En general, es mejor llamar a fstat y comparar st_dev y st_ino, y el artículo sí mencionaba algo de eso

    Un efecto secundario menos considerado es el costo en rendimiento
    Como ejemplo real, en una ruta de directorio muy profunda cp tardó 0.010 segundos, mientras que uu_cp tardó 12.857 segundos

    Puede 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::fs tiene un problema de lowest common denominator
      Había que meter algo en Rust 1.0 y, por desgracia, ese estado quedó congelado por mucho tiempo

      Creo que uutils es un buen lugar para intentar diseñar una API alternativa a std::fs en la que sea más difícil equivocarse

    • Gracias 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_ino
      Parece 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 tiempo

    • Soy totalmente principiante, pero me preguntaba por qué no simplemente hacían cd directo con $(yes a/ | head -n $((32 * 1024)) | tr -d '\n') en vez de necesitar un bucle while

      Edit: ya entendí. Era por -bash: cd: a/a/a/....../a/a/: File name too long

    • No sé si ya lo viste, pero hay una demo para convertir automáticamente utilidades GNU como wget a 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ó if en vez de switch dentro de una función 100 frames abajo en el call stack

      Ahora 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 panic ya es un error bastante amateur, incluso bajo los estándares de Rust
      Si fuera algo como un error de alloc imposible de manejar, bueno, pero expect y unwrap son difíciles de justificar salvo que estés garantizando de forma realmente estricta un invariante que haga imposible recorrer ese camino de código

  • Una 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 chroot cargue librerías con dlopen no es algo que esté escrito de forma visible en ninguna parte

      Es 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, uutils habría estado mucho mejor si hubiera sido GPL y hubiera podido inspirarse directamente en el código fuente original de coreutils

    • Tambié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 foo en vez de bar porque con la condición ABC, usar bar genera el peligroso baz por 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, uutils sí usa la suite de pruebas de GNU coreutils

      Ademá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, upstart y snap, esto entra totalmente dentro de lo esperable

    • A 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

    • De hecho, es peor que eso
      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
    • Mmm… quizá haya alguna manera de poner un write lock al directorio, pero en cuanto se mezclan cosas como timeouts, parece que se volvería mucho más complicado muy rápido
  • Parece que la causa raíz de varios bugs es que las API de Unix son demasiado opacas

    Por ejemplo, que get_user_by_name cargue 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 del chroot termine ejecutando código como uid 0, se siente casi como una booby trap

    Que 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 API
      Quien 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 libc justamente elimina una de esas partes

    • Creo 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 chroot queda bajo el control de quien armó ese chroot, y si no entiendes eso, no deberías usar chroot()

      get_user_by_name puede sentirse como una trampa, pero en realidad hay muy poca diferencia práctica entre usar newroot/etc/passwd y usar newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so, newroot/bin/sh y cosas así

      Por eso creo que /usr/sbin/chroot nunca debería tener motivo para consultar IDs de usuario
      toybox chroot no lo hace
      Al 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 debe

    Má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 ambigua

      Y más allá de eso, si asumimos que rm se niega a borrar un archivo si sus primeros 9 bytes son important,
      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 uutils sí hubo pruebas extensivas de comparación de comportamiento con las utilidades originales, e incluso intentaron preservar hasta los bugs

    • Por 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 bloquea chroot() por defecto
      Para 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

    • A veces crecer solo significa hacerse más alto