Fomos : un système d’exploitation expérimental construit en Rust
(github.com/Ruddle)- Fomos est un OS expérimental construit en Rust, un projet destiné à comprendre les idées de systèmes Non-Unix et les défis du modèle exo-kernel
- Il fournit une sortie graphique, l’allocation dynamique, le chargement et l’exécution simultanés d’apps, la prise en charge des souris et claviers Virtio, ainsi que l’ordonnancement coopératif
- Les apps sont traitées comme une fonction unique de la forme
pub extern "C" fn _start(ctx: &mut Context) -> i32, sans bibliothèque standard, les fonctionnalités de l’OS étant reçues viaContext Contextest une structure qui contient des fonctionnalités et des états comme les logs, le PID, le framebuffer,calloc,cdalloc,storeet les entrées ; les nouvelles fonctionnalités sont ajoutées à la fin afin de préserver la compatibilité avec les anciennes apps- Il n’y a pas d’appels système ; les apps rendent le contrôle à l’OS via
return, puis la fonctionstartest rappelée ensuite, dans un modèle d’exécution coopératif - L’état des apps peut être stocké dans
Context.store, et la boucle du kernel suit une structure simplifiée qui parcourt la liste des apps et appelle_start(Context::new(...))pour chacune - Comme toutes les fonctionnalités et tous les effets de bord passent par
Context, le remplacement ou l’encapsulation des fonctions deContextpermet en principe de mettre en place sandboxing, instrumentation et débogage - La sécurité n’est pas encore implémentée ; les apps peuvent actuellement inspecter la RAM d’autres apps, et il est prévu d’implémenter la sécurité des données sans changement de contexte ni pile de mémoire virtuelle propre à chaque app
- Les éléments manquants sont le stockage persistant, la prise en charge du GPU, le networking, ainsi que des abstractions pour partager données et fonctionnalités entre apps ; Virgl est en cours de développement
- La compilation se lance avec
./build.shet peut nécessiterrust nightly,gccetqemuavec les flags Virgl et SDL
1 commentaires
Avis sur Hacker News
Ce qui me dérange, c’est que dans un système préemptif,
while (true)peut ralentir le système, mais dans un système coopératif, dès qu’on ne rend pas la main, la machine s’arrête de faitDu point de vue de la sécurité aussi, ce type de système rend les attaques par déni de service beaucoup trop faciles, et un simple bug dans une application peut se propager à tout le système
Je ne suis pas développeur d’OS, donc corrigez-moi si je me trompe
Si les systèmes d’exploitation ont abandonné le multitâche coopératif, ce n’est pas parce qu’ils avaient résolu pour toujours tous les problèmes de « consommation incontrôlée de ressources », mais parce que de simples erreurs au niveau applicatif, comme le blocage du thread UI ou une boucle infinie accidentelle, peuvent compromettre l’état de tout le système
Pour un système conçu pour exécuter des programmes arbitraires, c’est assez rédhibitoire
On peut permettre aux applications d’être planifiées de façon coopérative tout en empêchant
while(true){}de monopoliser indéfiniment le systèmePar exemple, on peut fixer une limite de temps par application, ou, de façon plus expérimentale, détecter les boucles et appliquer une limite de temps généreuse
Pour les programmeurs, c’est confortable quand l’isolation est maximale entre programmes sans rapport et minimale entre programmes liés ; pour les utilisateurs, c’est confortable quand les ressources de calcul sont fortement isolées, tandis que le stockage ou les permissions sont moins verrouillés
En pratique, il faut une planification complexe, plus proche du préemptif que du coopératif, mais avec une part de coopératif
Même sous Linux, un programme malveillant comme une fork bomb peut assez facilement bloquer le système, surtout si le swap est activé, et même avec un ordonnanceur préemptif, si un programme occupe 99 % des threads système, c’est essentiellement lui qui s’exécute la plupart du temps
La planification est un spectre, et les OS actuels sont préemptifs, mais aussi coopératifs dans une certaine mesure
Une application peut décider de céder la main
À l’inverse, on peut garder l’OS coopératif, mais lorsqu’un seuil d’utilisation des ressources est dépassé ou qu’une interruption de timer survient, basculer rarement, en mode d’échec, vers un changement de contexte préemptif, terminer l’application, puis revenir au mode coopératif
On pourrait appeler cela coopératif optimiste, préemptif pessimiste
Une boucle
while(true)met à genoux un système monocœur, mais pas forcément un système multicœurLes compromis ont peut-être changé entre l’époque où l’architecture de base des OS que nous utilisons aujourd’hui a été conçue et maintenant
J’ai particulièrement aimé la partie disant que « dans Fomos, une application n’est qu’une fonction »
Les exécutables Unix ou Windows sont très complexes par rapport à des fonctions indépendantes, donc il est difficile d’imaginer à quel point un noyau écrit de cette manière pourrait être élégant
Je me demande si Smalltalk/Squeak fonctionne aussi ainsi, et j’espère que l’auteur continuera jusqu’au système de fichiers, au gestionnaire de tâches, à une pile mémoire sûre et au partage des ressources
Bien sûr, comme exigence minimale de preuve de concept, il faudra aussi faire tourner DOOM
Les OS des machines Lisp en sont plus proches : au début, des fonctions indépendantes s’appelaient entre elles, sans système d’objets, puis elles sont devenues des fonctions génériques spécialisées selon les classes des arguments
On dirait que les concepteurs n’ont pas encore découvert pourquoi les autres OS ont fini par avoir besoin de ce qu’ils omettent aujourd’hui
int main() { … }Un autre exemple en Rust est https://github.com/hermit-os/hermit-rs
L’idée est intéressante, mais continuer à ajouter de nouvelles fonctions à la structure Context pour rester compatible avec les anciens éléments, c’est la voie vers l’enfer de la rétrocompatibilité
C’est se piéger soi-même en s’interdisant de retirer de la structure Context des éléments anciens ou abandonnés
Une meilleure approche serait d’introduire du versionnage sémantique entre l’OS et les applications
Si une application déclare pour quelle version de l’OS elle a été construite, ou de quelle version elle dépend, l’OS peut vérifier la compatibilité et lui passer la version correspondante de la structure Context
La plupart des problèmes de rétrocompatibilité restent, mais on peut garder la structure Context propre en conservant dans le noyau plusieurs structures par version majeure/mineure
C’est une bonne idée, mais j’aime aussi la simplicité d’une seule interface d’exécution
Si l’OS doit de toute façon gérer toutes les versions, les futures applications pourraient utiliser du padding pour donner une impression de « propreté »
struct Context{ padding: [u8;256], // old stuff ctx: ContextV42 }Cela dit, en l’écrivant ainsi, ça me semble un peu bancal
Faire déclarer sa version à l’application me paraît être un problème déjà résolu par les formats d’exécutables comme ELF, donc j’essaie une autre approche
La partie « comment faire sleep ou attendre de façon asynchrone ? Il suffit de return » me paraît un peu étrange
Des E/S asynchrones à la
io_uringseraient excellentes, mais ce modèle semble les exclure, ce qui pourrait rendre difficile l’obtention de performances correctesNe pas prendre en charge l’asynchrone est aussi étrange, car cela peut correspondre à des points d’interruption naturels
Cela dit, pour faire ça, il faudrait probablement abandonner une bonne partie de la conception qui consiste à enregistrer et recharger explicitement l’état de l’application sur disque, et le coût semble élevé
Pour des raisons similaires, je pense que le réseau pourrait aussi devenir difficile, du moins à faire efficacement
Cette fonction ressemble à l’extrémité d’une boucle d’événements qui reçoit un état arbitraire en paramètre, donc ce que fait une boucle d’événements devrait pouvoir être largement généralisé
En revanche, cela ferait renoncer aux coroutines et au support async au niveau du langage
Les exemples donnés sont trop artificiels
Dans un système d’exploitation préemptif, une appli se bloque généralement d’une manière qui ne rend pas tout le système coopératif, comme un interblocage de threads ou une boucle infinie
De plus, un système préemptif peut arrêter une appli bien avant qu’elle ne devienne de fait coopérative si elle crée trop de threads ou de fichiers, ou consomme trop de mémoire
Notre système est simplement plus permissif
En plus, dire que « le sandboxing est gratuit dès lors qu’on accepte les prémisses », tout en disant que « n’importe quelle appli peut facilement inspecter la RAM d’une autre, et c’est un problème difficile à résoudre », signifie que le sandboxing n’est pas gratuit
Cela dit, l’idée est intéressante et j’espère que l’auteur réussira
La solution à ce problème pourrait être de créer une fonction, c’est-à-dire une closure qui enveloppe l’application et se comporte comme le Context propre de l’appli
Je me demande ce que ça donnerait si une appli pouvait ouvrir une autre appli, ou si une appli pouvait devenir le système d’exploitation d’une autre appli
Un système correctement conçu pour un sandboxing robuste imposerait des limites à toutes les ressources et refuserait les requêtes lorsque ces limites sont atteintes
Je me demande comment Fomos distingue processus et exécutable
Sous Linux, un processus correspond à des données internes du noyau comme uid et gid, ainsi qu’à un espace d’adressage virtuel contenant les pointeurs argv/envp, la pile, le tas, le masque de signaux, la table des descripteurs de fichiers, les gestionnaires de signaux et la mémoire exécutable
Un exécutable est un fichier contenant suffisamment de bits pour que le chargeur remplisse cet espace d’adressage lors de l’appel système
execveOn peut créer un processus avec
clone3ouforkmême sans exécutable ; le noyau utilise ELF et la majeure partie de l’espace utilisateur utilise le chargeur RTLD de GLIBC, mais ni l’un ni l’autre n’est strictement nécessaire pour créer un processus à partir d’un format d’exécutable donnéUn exécutable lié statiquement sans code indépendant de la position ressemble davantage à « juste une fonction » du point de vue de l’assembleur, mais résoudre des symboles à l’exécution sans ASLR le rend vulnérable aux attaques par dépassement de tampon lorsque l’adresse des fonctions dépendantes est connue
Je veux bien des alternatives aux défauts de glibc et au modèle de processus Posix, mais je pense qu’une grande partie de la complexité des exécutables Unix est intrinsèque
La résolution de symboles à l’exécution est difficile mais utile, autoriser des interpréteurs arbitraires est pénible mais c’est un domaine où Linux est plus fort que Windows et MacOS, et exposer l’interface du noyau via des appels système stables est l’un des atouts de Linux
Mac a Mach-O, Windows a PE, Linux a ELF, etc., mais il n’y a aucune raison de ne pas avoir un écosystème varié de formats d’exécution et de liaison
Un système d’exploitation avec un modèle de chargement de code très simple serait un bon terrain pour ce genre d’expérimentation
Un ABI stable n’a rien de particulièrement unique à Linux, et l’utilité d’un tel choix est assez discutable, mais la prise en charge des pilotes est remarquable et difficile à nier
C’est assez intéressant
Je ne vois pas comment on peut obtenir un certain niveau de sécurité et de sûreté avec des applis coopératives non fiables
N’importe quelle appli peut monopoliser le CPU indéfiniment et bloquer le noyau ainsi que les autres applis
Si nous utilisons des systèmes d’exploitation à ordonnancement préemptif, c’est parce qu’ils permettent d’arrêter une appli défaillante sans compromettre le reste du système
Il utilisait le multitâche préemptif, mais n’imposait pas de protection mémoire ; à la place, le compilateur était un service système, de sorte que seuls les exécutables produits et signés par le compilateur du système pouvaient être lancés
Comme le compilateur garantissait la protection mémoire au moment du build, les appels système et la communication interprocessus devenaient très peu coûteux
Avec un compilateur un peu plus sophistiqué, on pourrait aussi imposer de façon similaire le multitâche coopératif en insérant des appels à
yieldaux endroits nécessairesOn ne peut pas résoudre le problème de l’arrêt en général, mais il existe tout de même des classes de programmes dont l’analyse statique peut prouver qu’ils se terminent ou cèdent la main
Il suffirait de traiter séparément les programmes pour lesquels on ne peut pas le prouver, et un timer de surveillance pourrait aussi arrêter automatiquement les programmes défaillants
Le code non fiable ne s’exécute pas dans l’image « principale », mais dans une VM jetable
On pourrait probablement appliquer le même modèle ici avec un hyperviseur, mais personne n’utilise uniquement un système Smalltalk tout seul, et il faut un minimum d’infrastructure
Je me demande s’il est possible de mettre en œuvre la sécurité dans ce système d’exploitation sans refonte complète, et sans en réalité refaire ce que les systèmes d’exploitation existants ont déjà fait.
Je connais deux façons d’imposer la sécurité aux applications exécutées sur le même matériel.
L’une consiste à isoler les processus à l’exécution avec de la mémoire virtuelle, l’autre à faire vérifier par le chargeur, au moment du chargement, que le code n’effectue pas d’accès arbitraires à la mémoire.
La seconde est généralement imposée par une machine virtuelle qui n’autorise que le bytecode d’un jeu d’instructions restreint sans arithmétique de pointeurs, comme la JVM ou Smalltalk.
L’auteur de Fomos ne veut pas de changements de contexte ni d’isolation mémoire, et le compilateur Rust ne produit pas de bytecode : y a-t-il une autre méthode ?
D’après ce que j’ai compris, un compilateur certifié impose des règles comme l’interdiction de
unsafe, si bien qu’ici le code source tient en fait lieu de bytecode.À première vue, cela ressemble beaucoup à Midori, mais les détails d’implémentation sont assez différents.
Dans Theseus, les pilotes, applications, etc. sont des objets ELF, tous liés dynamiquement en un seul exécutable, c’est-à-dire le noyau, avec aussi des techniques intéressantes comme la mise à niveau à chaud.
https://github.com/theseus-os/Theseus
https://www.theseus-os.com/
En cas d’erreur de segmentation à l’exécution, on vérifierait par exemple un jeton de sécurité pour savoir si l’appelant a le droit d’accéder à cette page et de l’appeler.
Je ne sais pas à quel point ce serait vraiment pratique.
Même avec du multitâche coopératif, je ne pense pas que ce soit comme à l’époque de Classic MacOS, dans la mesure où il y a aujourd’hui énormément de cœurs.
Un ou deux processus qui ne cèdent pas la main ne bloqueraient pas forcément tout le système.
Si une fonction se comporte mal et ne retourne pas, le système pourrait sans doute l’arrêter quand tous les cœurs seraient occupés.
Le multitâche coopératif n’implique pas nécessairement de mauvaises performances.
Le partage temporel servait à l’origine à répartir un énorme CPU unique entre plusieurs utilisateurs, mais maintenant que les CPU multicœurs mono-utilisateur sont courants, il est grand temps d’envisager d’autres façons d’utiliser les cœurs.
Le simple fait que ce projet existe est vraiment enthousiasmant.
Dans ce modèle, il n’y a pas de changement de contexte, donc les performances pourraient au contraire s’améliorer.
Du coup, je me demande ce qui se passerait si l’on augmentait le timeslice de Linux à une valeur absurde comme 10 secondes.
J’aimerais en savoir plus sur le plan de sécurité.
Dans l’ensemble, je pense que ce type d’expérimentation montre qu’un système d’exploitation peut être amélioré par une conception greenfield.
Cela me fait un peu penser à Mirage OS : https://mirage.io/