4 points par GN⁺ 2023-08-31 | 1 commentaires | Partager sur WhatsApp
  • 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 via Context
  • Context est une structure qui contient des fonctionnalités et des états comme les logs, le PID, le framebuffer, calloc, cdalloc, store et 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 fonction start est 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 de Context permet 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.sh et peut nécessiter rust nightly, gcc et qemu avec les flags Virgl et SDL

1 commentaires

 
GN⁺ 2023-08-31
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 fait
    Du 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

    • C’est exact, et à mon avis la réfutation ressemble plutôt à une diversion
      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
    • Il ne faut pas voir les choses en tout ou rien
      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ème
      Par 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
    • Je suis l’auteur
      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
    • Je ne suis pas développeur d’OS, mais je pense que le nombre de cœurs change la donne
      Une boucle while(true) met à genoux un système monocœur, mais pas forcément un système multicœur
      Les 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
    • Si, après l’expérience du multitâche coopératif de Windows 3.1, le monde est passé au multitâche préemptif, ce n’est pas sans raison
  • 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

    • Dans Smalltalk, il s’agit plutôt de méthodes de classes que de fonctions indépendantes, mais c’est vrai dans le sens où tous les objets de l’image s’envoient des messages, c’est-à-dire appellent les méthodes les uns des autres
      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
    • Dire qu’une « application n’est qu’une fonction » ressemble à la malédiction du développement greenfield
      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
    • En pratique, je me demande en quoi c’est différent des autres OS où une application est une fonction int main() { … }
    • Cela semble correspondre à la définition d’un unikernel
      Un autre exemple en Rust est https://github.com/hermit-os/hermit-rs
    • Il suffit d’essayer un vieux Classic MacOS
  • 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

    • Je suis l’auteur
      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_uring seraient excellentes, mais ce modèle semble les exclure, ce qui pourrait rendre difficile l’obtention de performances correctes
    Ne 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

    • À vue de nez, les E/S asynchrones seraient implémentées en mettant à jour une requête d’E/S dans le Context puis en retournant, la fonction étant rappelée quand c’est prêt
      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

    • Dans le monde des navigateurs, tous les sites ouverts partagent la mémoire du tas du navigateur, mais n’interfèrent pas les uns avec les autres
      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
    • Cet exemple est encore plus artificiel, car il suppose que tous les systèmes ont un sandboxing médiocre comme Windows et Linux
      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 execve
    On peut créer un processus avec clone3 ou fork mê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

    • En y réfléchissant un peu plus, l’une des grandes erreurs me semble être l’homogénéisation des formats d’exécutables
      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
    • Je pensais que la force de Linux était plutôt les pilotes qu’un ABI stable
      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
    • Je me demande si vous avez regardé comment Zircon (Fuchsia) traite ça
      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

    • Je me souviens qu’au milieu des années 2000, Microsoft Research avait un prototype de système d’exploitation entièrement écrit en .NET
      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 à yield aux endroits nécessaires
      On 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
    • Dans les systèmes Smalltalk comme Squeak ou Pharo, lorsqu’un thread se bloque, l’utilisateur interrompt l’exécution avec un raccourci clavier
      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 ?

    • Theseus est un exemple de la seconde approche, implémentée en Rust sans bytecode.
      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/
    • Ce n’est qu’une supposition, mais tous les « programmes » pourraient partager un même espace d’adressage, tandis que la mémoire virtuelle limiterait la visibilité des pages accessibles à un instant donné.
      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.
    • https://en.wikipedia.org/wiki/Capability-based_addressing ?
  • 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.

    • Pour préciser, quand je disais que « le multitâche coopératif n’implique pas nécessairement de mauvaises performances », je parlais de mauvaises performances interactives.
      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/