4 puntos por GN⁺ 2023-08-31 | 1 comentarios | Compartir por WhatsApp
  • Fomos es un SO experimental construido con Rust y es un proyecto para entender ideas de sistemas operativos no Unix y los desafíos del patrón exokernel
  • Ofrece salida gráfica, asignación dinámica, carga y ejecución simultánea de apps, soporte para mouse y teclado Virtio, y planificación cooperativa
  • Las apps se tratan como una única función con la forma pub extern "C" fn _start(ctx: &mut Context) -> i32, y reciben las funciones del SO a través de Context sin biblioteca estándar
  • Context es una estructura que contiene funciones y estado como registros, PID, framebuffer, calloc, cdalloc, store y entrada, y mantiene compatibilidad con apps antiguas agregando nuevas funciones al final
  • No hay llamadas al sistema; las apps devuelven el control al SO con return y luego la función start se vuelve a llamar en un modelo de ejecución cooperativa
  • El estado de la app puede guardarse en Context.store, y el bucle del kernel tiene una estructura simplificada que recorre la lista de apps y llama a _start(Context::new(...)) de cada una
  • Como toda la funcionalidad y los efectos secundarios se pasan mediante Context, se parte de la idea de que el sandboxing, la instrumentación y la depuración pueden construirse reemplazando o envolviendo las funciones de Context
  • Actualmente no hay seguridad implementada, y las apps pueden inspeccionar la RAM de otras apps; hay planes para implementar seguridad de datos sin cambio de contexto ni pila de memoria virtual por app
  • Entre los elementos faltantes están el almacenamiento persistente, soporte de GPU, redes y abstracciones para compartir datos y funciones entre apps; Virgl está en desarrollo
  • La compilación se ejecuta con ./build.sh, y puede requerir rust nightly, gcc y qemu con flags de Virgl y SDL

1 comentarios

 
GN⁺ 2023-08-31
Opiniones de Hacker News
  • Lo que no me convence es que, en un sistema preventivo, while (true) puede hacer que el sistema se vuelva lento, pero en un sistema cooperativo, en el momento en que no se devuelve el control, la máquina básicamente se detiene.
    Desde el punto de vista de seguridad, un sistema así también hace que los ataques de denegación de servicio sean demasiado fáciles, y un solo bug en una app puede propagarse a todo el sistema.
    No soy desarrollador de sistemas operativos, así que corríjanme si me equivoco.

    • Es correcto, y creo que esa réplica se acerca a desviar el tema.
      La razón por la que los sistemas operativos abandonaron la multitarea cooperativa no fue que hubieran resuelto para siempre todos los problemas de “uso descontrolado de recursos”, sino que errores simples a nivel de app, como bloquear el hilo de la UI o un bucle infinito accidental, arruinaban el estado de todo el sistema.
      En un sistema diseñado para ejecutar programas arbitrarios, eso es bastante grave.
    • No hay que verlo como todo o nada.
      Se puede permitir que las apps se planifiquen de forma cooperativa y aun así impedir que while(true){} capture el sistema indefinidamente.
      Por ejemplo, se podría poner un límite de tiempo por app o, de forma más experimental, detectar bucles y aplicar un límite de tiempo generoso.
      Para los programadores es cómodo cuando el aislamiento entre programas no relacionados es máximo y el aislamiento entre programas relacionados es mínimo, pero para los usuarios es cómodo cuando los recursos de cómputo están fuertemente aislados y cosas como el almacenamiento o los permisos están menos bloqueados.
      En la práctica se necesita una planificación compleja, más cercana a la preventiva que a la cooperativa, pero con algo de cooperativa.
      Incluso en Linux, no es difícil que un programa malicioso como una bomba fork congele el sistema, sobre todo si el swap está activado; y aunque exista un planificador preventivo, si un programa ocupa el 99% de los hilos del sistema, en la práctica casi siempre se ejecutará ese programa.
    • Soy el autor.
      La planificación es un espectro, y los sistemas operativos actuales son preventivos, pero también son cooperativos en cierta medida.
      Una app puede decidir si cede el control.
      A la inversa, el sistema operativo podría mantenerse cooperativo, pero si se supera un umbral de uso de recursos o ocurre una interrupción de temporizador, cambiar de contexto en un modo de falla poco frecuente, volverse preventivo, terminar la app y luego volver al modo cooperativo.
      A eso se le podría llamar cooperativo optimista y preventivo pesimista.
    • No soy desarrollador de sistemas operativos, pero creo que la cantidad de núcleos marca la diferencia.
      Un bucle while(true) derriba un sistema de un solo núcleo, pero no necesariamente uno multinúcleo.
      Los compromisos pueden haber cambiado entre la época en que se creó la estructura básica de los sistemas operativos que usamos hoy y la actualidad.
    • Hay una razón por la que, después del experimento de multitarea cooperativa de Windows 3.1, el mundo pasó a la multitarea preventiva.
  • Me gustó especialmente la parte de “en Fomos, una app es solo una función”.
    Los ejecutables de Unix o Windows son muy complejos en comparación con funciones independientes, así que cuesta imaginar lo genial que podría ser un kernel escrito de esta manera.
    Me pregunto si Smalltalk/Squeak también funciona así, y espero que el autor siga con el sistema de archivos, el administrador de tareas, una pila de memoria segura y el intercambio de recursos.
    Por supuesto, como requisito mínimo de prueba de concepto, también hace falta ejecutar DOOM.

    • En Smalltalk no sería tanto una función independiente, sino un método de una clase, pero en el sentido de que todos los objetos dentro de la imagen se envían mensajes entre sí, es decir, llaman a los métodos de los demás, sí.
      El sistema operativo de las máquinas Lisp se acerca más: al principio, sin sistema de objetos, había funciones independientes que se llamaban entre sí, y más tarde pasaron a ser funciones genéricas especializadas según la clase de los argumentos.
    • Decir “una app es solo una función” suena como la maldición del desarrollo greenfield.
      Parece que los diseñadores todavía no han descubierto por qué otros sistemas operativos terminaron necesitando las cosas que ahora están dejando fuera.
    • En la práctica, me pregunto en qué se diferencia de otros sistemas operativos donde una app es una función int main() { … }.
    • Esto parece encajar con la definición de unikernel.
      Otro ejemplo hecho en Rust es https://github.com/hermit-os/hermit-rs.
    • Basta con ejecutar el viejo Classic MacOS.
  • La idea está bien, pero seguir agregando funciones nuevas al struct Context para mantener compatibilidad con elementos antiguos es un camino directo al infierno de la compatibilidad hacia atrás.
    Es como encerrarse a sí mismo e impedirse eliminar del struct Context elementos viejos u obsoletos.
    Creo que un mejor enfoque sería introducir versionado semántico entre el sistema operativo y las apps.
    Si una app declara para qué versión del sistema operativo fue compilada o de cuál depende, el sistema operativo puede comprobar la compatibilidad y pasarle la versión correspondiente del struct Context.
    La mayoría de los problemas de compatibilidad hacia atrás seguirían existiendo, pero se podría mantener limpio el struct Context teniendo dentro del kernel varias estructuras por versión mayor/menor.

    • Soy el autor.
      Es una buena idea, pero también me gusta la simplicidad de tener una sola interfaz en tiempo de ejecución.
      Si de todos modos el sistema operativo tiene que manejar todas las versiones, las apps futuras podrían usar padding para que se sienta “limpio”.
      struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }
      Aunque, ahora que lo escribo, también se siente un poco incorrecto.
      Declarar la versión de una app parece un problema que los formatos de ejecutables como ELF ya resuelven, así que estoy probando alternativas.
  • La parte de “¿cómo se hace sleep o se espera de forma asíncrona? simplemente se hace return” me parece un poco rara.
    La E/S asíncrona al estilo io_uring sería excelente, pero este modelo parece excluir algo así, por lo que podría dificultar lograr un rendimiento adecuado.
    También es raro no admitir async, porque podría conectarse con puntos naturales de suspensión.
    Sin embargo, para hacerlo parece que habría que renunciar a gran parte del diseño de guardar y cargar explícitamente el estado de la aplicación en disco, y el costo se ve alto.
    Creo que, por razones similares, el networking también podría volverse difícil, al menos si se quiere hacerlo de forma eficiente.

    • Si tuviera que adivinar, la E/S asíncrona se implementaría actualizando una solicitud de E/S en Context y devolviendo el control, y luego la función se volvería a invocar cuando esté lista.
      Esta función parece el extremo de un event loop que recibe un estado arbitrario como parámetro, así que en general debería ser posible generalizar lo que hace un event loop.
      Eso sí, se estaría renunciando al soporte de corrutinas y async a nivel de lenguaje.
  • El ejemplo presentado es demasiado artificial
    En un sistema operativo preventivo, una app normalmente se queda colgada de formas que no vuelven cooperativo a todo el sistema, como un interbloqueo de hilos o un bucle infinito
    Además, un sistema preventivo puede terminar una app si crea demasiados hilos o archivos, o si usa demasiada memoria, mucho antes de que en la práctica se vuelva cooperativo
    Nuestro sistema simplemente es más permisivo
    Encima dicen que “el sandboxing es gratis si aceptas las premisas”, pero también que “cualquier app puede inspeccionar fácilmente la RAM de otra app, y eso es un problema difícil de resolver”; entonces, el sandboxing no es gratis
    Aun así, es una idea genial y espero que al autor le vaya bien

    • En el mundo de los navegadores, todos los sitios abiertos comparten la memoria heap del navegador, pero no interfieren entre sí
      La solución a este problema podría ser crear una función, es decir, un closure que envuelva a la aplicación y actúe como el propio Context de la app
      Me pregunto qué pasaría si una app pudiera abrir otra app, o si una app pudiera ser el sistema operativo para otra app
    • Ese ejemplo es aún más artificial porque asume que todos los sistemas tienen un sandboxing tan malo como Windows y Linux
      Un sistema bien diseñado para un sandboxing sólido pondría límites a todos los recursos y rechazaría las solicitudes cuando se alcance el límite
  • Me da curiosidad cómo Fomos distingue entre procesos y ejecutables
    En Linux, un proceso es un conjunto de datos internos del kernel, como uid y gid, y un espacio de direcciones virtual que incluye punteros argv/envp, pila, heap, máscara de señales, tabla de handles de archivos, manejadores de señales y memoria ejecutable
    Un ejecutable es un archivo que contiene los bits suficientes para que el loader llene ese espacio de direcciones durante la llamada al sistema execve
    También se puede crear un proceso sin un ejecutable mediante clone3 o fork; el kernel usa ELF y la mayor parte del espacio de usuario usa el loader RTLD de GLIBC, pero no ambos son estrictamente necesarios para crear un proceso con un formato de ejecutable específico
    Un ejecutable estático enlazado sin código independiente de posición, desde el punto de vista del ensamblador, se parece más a “solo una función”, pero si resuelve símbolos en tiempo de ejecución sin ASLR, se vuelve vulnerable a ataques de desbordamiento de búfer cuando se conocen las direcciones de las funciones dependientes
    Quiero alternativas a las deficiencias de glibc y al modelo de procesos de Posix, pero creo que una buena parte de la complejidad de los ejecutables de Unix es esencial
    La resolución de símbolos en tiempo de ejecución es difícil pero útil; permitir intérpretes arbitrarios es molesto, pero es una de las partes donde Linux es más fuerte que Windows y MacOS; y ofrecer una interfaz de kernel mediante llamadas al sistema estables es una fortaleza de Linux

    • Pensándolo un poco más, creo que uno de los grandes errores es la homogeneización de los formatos de ejecutable
      Mac usa Mach-O, Windows usa PE, Linux usa ELF, y así, pero no hay razón para no tener un ecosistema variado de formatos de ejecución/enlace
      Un sistema operativo con un modelo muy simple para cargar código sería un buen lugar para ese tipo de experimentos
    • Pensaba que la fortaleza de Linux eran más bien los drivers, no una ABI estable
      Una ABI estable tampoco es algo exclusivo de Linux, y la utilidad de esa decisión me parece bastante cuestionable, pero el soporte de drivers es impresionante y difícil de negar
    • Me da curiosidad si viste cómo Zircon(Fuchsia) maneja esto
      Es bastante interesante
  • No sé cómo se puede lograr cierto grado de seguridad y protección con apps cooperativas no confiables
    Cualquier app puede acaparar la CPU indefinidamente y detener el kernel y las demás apps
    La razón por la que usamos sistemas operativos con planificación preventiva es que podemos detener una app que funciona mal sin arruinar el resto del sistema

    • Recuerdo que a mediados de los 2000 Microsoft Research tenía un prototipo de sistema operativo escrito solo en .NET
      Usaba multitarea preventiva, pero no imponía protección de memoria; en cambio, tenía el compilador como servicio del sistema, de modo que solo se podían ejecutar archivos ejecutables creados y firmados por el compilador del sistema
      Como el compilador garantizaba la protección de memoria en tiempo de compilación, las llamadas al sistema y la comunicación entre procesos se volvían muy baratas
      Con un compilador un poco más sofisticado, también se podría imponer de forma similar la multitarea cooperativa insertando llamadas a yield en los lugares necesarios
      No se puede resolver el problema general de la detención, pero aún existen clases de programas para los que el análisis estático puede demostrar que terminan o ceden el control
      Solo habría que tratar por separado los programas que no se puedan demostrar, y también se podrían detener automáticamente los programas defectuosos con un temporizador de vigilancia
    • En sistemas Smalltalk como Squeak o Pharo, si un hilo se cuelga, el usuario interrumpe la ejecución con un atajo de teclado
      El código no confiable no se ejecuta en la imagen “principal”, sino en una VM descartable
      Aquí también se podría aplicar el mismo modelo usando un hipervisor, pero nadie usa únicamente un sistema Smalltalk por sí solo, y se necesita cierta infraestructura
  • Me pregunto si se puede implementar seguridad en este sistema operativo sin rediseñarlo por completo; en la práctica, sin volver a hacer lo que los sistemas operativos existentes ya hicieron.
    Conozco dos formas de imponer la seguridad de las aplicaciones que se ejecutan en el mismo hardware.
    Una es aislar los procesos en tiempo de ejecución con memoria virtual, y la otra es que, al momento de carga, el cargador verifique si el código realiza accesos arbitrarios a la memoria.
    Esta última normalmente se impone con una máquina virtual que solo permite bytecode de un conjunto de instrucciones restringido, sin aritmética de punteros, como la JVM o Smalltalk.
    El autor de Fomos no quiere cambios de contexto ni aislamiento de memoria, y el compilador de Rust no genera bytecode; ¿habrá otra forma?

    • Theseus es un ejemplo del segundo enfoque implementado en Rust, sin bytecode.
      Según entiendo, en un compilador certificado se imponen reglas como prohibir unsafe, así que aquí el código fuente actúa, en la práctica, como bytecode.
      A primera vista se parece mucho a Midori, pero los detalles de implementación son bastante distintos.
      En Theseus, los drivers, las aplicaciones, etc., son objetos ELF, y todos se enlazan dinámicamente en un único ejecutable, es decir, el kernel; también hay técnicas interesantes como hot upgrades.
      https://github.com/theseus-os/Theseus
      https://www.theseus-os.com/
    • Es una suposición, pero parece que todos los “programas” podrían compartir un único espacio de direcciones, mientras que con memoria virtual se limita la visibilidad de las páginas accesibles en un momento dado.
      Por ejemplo, si en tiempo de ejecución ocurre una falla de segmentación, se podría verificar algo como un token de seguridad para determinar si el llamador tiene permiso para acceder a esa página y llamarla.
      No sé qué tan práctico sería realmente.
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • Aunque sea multitarea cooperativa, hoy en día hay muchísimos núcleos, así que no creo que sea igual que en la época de Classic MacOS.
    Uno o dos procesos que no cedan el control no necesariamente tendrían que bloquear todo el sistema.
    Si una función se comporta mal y no retorna, el sistema podría finalizarla cuando haya usado todos los núcleos.
    La multitarea cooperativa no necesariamente implica mal rendimiento.
    El tiempo compartido originalmente era una forma de repartir una enorme CPU única entre varios usuarios, pero ahora que las CPU multinúcleo de un solo usuario son comunes, ya pasó el momento de pensar en otras formas de usar los núcleos.
    Que este proyecto exista me entusiasma mucho.

    • Para agregar algo: cuando dije que “la multitarea cooperativa no necesariamente implica mal rendimiento”, me refería a mal rendimiento interactivo.
      En este modelo no hay cambios de contexto, así que incluso podría mejorar el rendimiento.
      Por eso me da curiosidad qué pasaría si se subiera el timeslice de Linux a un valor absurdo, como 10 segundos.
  • Me gustaría escuchar con más detalle el plan de seguridad.
    En general, creo que este tipo de experimentos muestra que los sistemas operativos pueden mejorar mediante un diseño greenfield.
    Me recuerda un poco a Mirage OS: https://mirage.io/