Fomos: sistema operativo experimental construido con Rust
(github.com/Ruddle)- 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 deContextsin biblioteca estándar Contextes una estructura que contiene funciones y estado como registros, PID, framebuffer,calloc,cdalloc,storey 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
returny luego la funciónstartse 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 deContext - 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 requerirrust nightly,gccyqemucon flags de Virgl y SDL
1 comentarios
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.
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.
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.
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.
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.
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.
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.
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.
int main() { … }.Otro ejemplo hecho en Rust es https://github.com/hermit-os/hermit-rs.
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.
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_uringserí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.
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
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
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
execveTambién se puede crear un proceso sin un ejecutable mediante
clone3ofork; 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íficoUn 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
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
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
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
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
yielden los lugares necesariosNo 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
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?
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/
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.
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.
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/