Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Fomos: Experimental OS, built with Rust (github.com/ruddle)
382 points by sbt567 on Aug 30, 2023 | hide | past | favorite | 150 comments


Something I don't like:

> The argument that a cooperative scheduling is doomed to fail is overblown. Apps are already very much cooperative. For proof, run a version of that on your nice preemptive system: [...] Might fill your swap and crash unsaved work on other apps.

The difference is that in a preemptive system, a `while (true)` might slow down the system, but in a cooperative system, the machine effectively halts. Like for good. If you're caught in a loop and don't yield control back, you're done.

In terms of security, this would make denial of service attacks against such systems trivial. You could exploit any bug in any application and it would bubble up to the entire system.

Or maybe I'm all wrong. I'm not an OS dev, so please someone correct me.


You're completely right, the argument is a red herring. People did not move away from cooperative multitasking for operating systems because it solved every "run-away usage of resource" problem for all time. They did it specifically because, as you note, it makes incredibly simple application-level errors, whether they are design-level (UI thread blocked) or simple errors (accidental infinite loop) totally devastating to the entire state of the system. This is pretty bad whenever your system is designed to run arbitrary computer programs.


It's because you treat it as if it's all or nothing. You can effectively allow apps to cooperatively schedule themselves, but also make sure `while(true){}` doesn't infinitely loop the system. You can e.g. timeout each app, or if you feel very experimental, avant-garde etc, you could try detecting loops, and put a very generous timeout (since you'll miss some loops like `i=0;while(true){i+=1}` depending on the algo you use to detect loops). There are tons of other schemes, algorithms. Most of these ideas are implemented somewhere, but 99.9% of computer users using linux/OSX/Windows don't experience them.

For the programmer, it's most convenient when the isolation between unrelated programs is maximal but isolation between related programs is minimal; but for the user it's the most convenient when programs are maximally isolated in terms of computation, but not for other things such as storage/permissions etc. Practically, this requires a complex scheduling mechanism that is more preemptive than cooperative, but also cooperative a little. E.g. in linux, it's not particularly hard for a rogue program to bring the system to halt with something like a fork bomb (especially if swap is turned on). Even if you have a preemptive scheduler, if your program has 99% of the threads running in the system, it's effectively cooperative because chances are you're scheduled most of the time anyway.


The problem is that it really _is_ an all or nothing, there are assumptions that cooperative programs will make that break if they can be preempted at any point. Even having a "little" preemption means programs have to be written to assume it can happen at any time (unless preemption means killing the program entirely).


I get what you are saying but does every operating system have the goal of allowing unpredictable multiple user applications running? A cooperative system can easily be correct for something well managed like an IOT device with only certain tasks or something somewhat general purpose but still more restricted like a video game console.


> I get what you are saying but does every operating system have the goal of allowing unpredictable multiple user applications running?

Certainly not, cooperative multitasking is used all the time in userspace programs to great effect, and OSs can do the same thing if the limitations are understood.

For the OS that's the focus of this post though, I think it does have the aim of being general purpose and running whatever you want on it. At that point the limitations aren't really acceptable (which the author acknowledges), and that's why the whole question of "what happens when a program misbehaves?" has come up. If the answer is "preempt the process to run it later" then it's not actually a cooperative system and application developers need to keep that in mind.


Author here. Scheduling is a spectrum. Current OS are preemptive, but also a bit cooperative. An app can make the decision of yielding control.

The opposite could work: OS is cooperative, unless some threshold of resource usage is triggered (a timer interrupt of instance). It then context switch to enter a, hopefully rare, failure mode, thus turning preemptive. Kill the app, and get back into cooperative mode. Let's call it optimistically cooperative & pessimistically preemptive.


If you're serious about this: make it preemptive. Handle interrupts in the kernel and find a nice way to pass them on to user processes. Any kind of prioritization mechanism will require preemption. I've written an experimental OS myself and I know this is hard stuff but if you can't crack that this is likely to be DOA. If you just want to play around then that's fine of course, there is nothing to stop you from doing that but if you want to see any kind of external adoption it is more or less a must.

Also: this is more like a multi-threaded app itself than an actual OS, for it to be a proper operating system you'd expect at a minimum memory barriers between applications. In that sense the bar for what an OS is has been raised quite a bit since the times of CP/M where it was more of a 'handy library to do some I/O for me' (on mainframes and mini computers there were already proper operating systems back then but on micro computers we had to wait until OS/9 to have something similar).


> I've written an experimental OS myself

What was it called? Do you have more details?


It was called 'Unite', some of the details: QnX clone for x86/32 which at the time wasn't available (nor would Quantum commit to releasing something like that). Pre-emptive multi-tasking, multi-user micro kernel based on the QnX API calls. It worked quite well, we built a bunch of devices that used it internally as the firmware (mostly: internet bandwidth management in the context of a hosting facility). Some HN'er has been trying to revive it in a virtual environment.


Sounds interesting. Thanks for sharing!


You're welcome. One of these days I should try to revive it myself, it's been decades since I last worked on the code though. But I still have the installation media that I made (floppy images) so it should be possible to bring it back to life.


> some threshold of resource usage is triggered (a timer interrupt of instance)

That's literally the definition of preemptive scheduling.


Yes, but "preemptive" sounds so aggressive. We prefer the term "alternative cooperation".


It could also be cooperative scheduling with a watchdog with task killing abilities (like we have for memory usage, it's effectively cooperative + OOM)


No idea why you are downvoted for quoting the literal textbook definition.


I'm not an OS dev either but I think core count makes a difference here. In that a while(true) loop brings down your single core system but doesn't for a multi-core system. Could see the tradeoffs being different today than back when the fundamentals of the OSes we use today were built.


This is a reasonable point. With containerization and the cloud, this abstraction is taken even further.

In the end though, all this really does is perhaps change how aggressive a pre-emptive system needs to be. It could wait longer to pre-empt. Fundamentally though, the design would still need to be pre-emptive.


I don't think it needs to be system pre-emptable. If we reserve 1 core for the OS then the user can always interact with the system. The OS can tell them "App X has pegged the 15 userland cores. Do you wish to kill it?".

In practice, I'm not sure how relevant pre-empting is any more. Software is much better behaved than it used to and I rarely see these runaway processes anymore. And when they do it's usually pegging a single core at 100% while other processes share the remaining core.


There's a reason that after the cooperative multitasking experiment in Windows 3.1, the world went with preemptive multitasking.


It is true however that we're in a different world now with multicore processors. Back then preemption was about slicing up the usage of a single sequentially executing core.

For very specialized (probably embedded, but maybe other) use cases, I can see the value of specialized operating system which dispenses with time slice scheduling within a core and just assigns waiting tasks to a free core when/if it becomes available. On systems with very high core counts, and with lots of short lived tasks, there could be some value to this.

Even back then, on many machines and operating systems it's not like the whole world stopped when a process would not give up time slice. Things like mouse cursors, keyboard buffering, and often even some I/O were able to proceed because the hardware assisted in making that possible.

The thing is, I don't think going cooperative simplifies much.. you still have to handle concurrent access to shared resources, and re-entrancy etc. By the time you've made your operating system able to handle that (memory, VM subsystem, I/O, etc.) from multiple cores... you may as well just go implement timeslicing as well.


I'm not sure what the advantages of this would be for something like a renderer for instance. Would synchronization be easier this way? I'm sorry for the disclaimer, but I must admit I really don't know and if someone answers I would like to forward my thanks


It's interesting because these days many language runtimes provide their own internal "green threads" / "coroutines" / "async", in-process timeslicing anyways.


We've had an important change to computers since then in that essentially every processor you might be running an OS on will have multiple threads of execution available. As long as some thread returns to the OS and the OS there does a check for excess resource usage it looks like Fomos's model could be made to work. Except in the case where you have some task that uses more threads than the system has which then all try to grab the same lost mutex or something. But you could always just ensure that the OS has at least one thread at any given time, which could be pretty expensive depending on how many threads are available.


And let's not forget the Mac, which was a dead-end until 1997.


> Or maybe I'm all wrong. I'm not an OS dev, so please someone correct me.

Nope, you are completl right! A while (true) might slow down the system, but even that is not necessarily the case: I wrote a program to test this (based on the pseudocode in the readme) and my system is totally usable, with basically zero lag! This is the power of a well-written operating system that includes a mysterious concept called priorities. There is actually no discernable different when running the program (other than battery use and temperature going up). In fact, I am running that program as I right this because the difference is so negligable.

program: https://paste.sr.ht/~pitust/47dc80ff09243b4bf41a08ae9434a32c...

> In terms of security, this would make denial of service attacks against such systems trivial

Denial of service attacks where you have access to the target system are not super hard to mount AFAIK.


But understand that modern CPUs have hyperthreading, meaning more than one independent thread of execution. A while(true) on one doesn't affect the other(s). So you won't notice much - other apps keep running.

Not because it isn't a resource disaster. But because unless you measure something, you might not notice.

Oh! You did measure something. Battery use and temp. There you go. It's a disaster. Some kind of management of such irresponsible threads is definitely a good idea.


> But understand that modern CPUs have hyperthreading, meaning more than one independent thread of execution. I know. But spinning up threads in an infinite loop is going to get code running on all threads (and indeed, that can be confirmed on `htop`). Also, not all CPUs have hyperthreading: the apple M1 chip (which is indeed my CPU) does not have support for that.

> Oh! You did measure something. Battery use and temp. So I didn't do any scientific measurements, but the temperature difference doesn't seem to be very significant.

> It's a disaster. But cooperative scheduling isn't any better. In fact, it's even worse! Since if an app doesn't yield (and let's face it, no app is perfect), you can easily get a deadlock. Developing for such a system without a VM sounds like a nightmare too to be honest.

> Some kind of management of such irresponsible threads is definitely a good idea. What kind of managment do you propose? I mean, if there is nothing else running on the system, it's probably okay to just let them keep running (it's not like they are harming anything except battery life, and you might want something that can max all cpu cores, like if you are running a compilation or a video game).


This is so cool.

Particularly enjoyed:

    In Fomos, an app is really just a function. There is nothing else ! This is a huge claim. An executable for a Unix or Windows OS is extremely complex compared to a freestanding function.
I can't even begin to imagine how cool a kernel would be written this way.

Correct me if I'm wrong, but I think this is how smalltalk/squeak works?

I hope the author continues with this project. File system, task manager, safe memory stacks, nice resource sharing, ...

... and of course, running DOOM as a minimum proof of concept requirement! /s.


>Correct me if I'm wrong, but I think this is how smalltalk/squeak works?

In Smalltalk those would be classes' methods rather than freestanding functions, but yes: all objects within the image directly send messages to each other, i.e. invoke each other's methods. Lisp machine operating systems are somewhat closer, since initially they had no object system and used freestanding functions calling each other, though later they became generic functions specialized to their arguments' classes.


Flavors was used on the Lisp Machine to implement much of the OS since around 1980: https://apps.dtic.mil/sti/tr/pdf/ADA095523.pdf


Reminds me of TempleOS, where all programs run in the same address space. Is this the same concept here?


Ironically, there are so many pointers at this point that OSes often now do use a single address space for every program (with escape hatches in case you need aliasing).

Virtual memory and vtables are now more about access control than about managing the scarcity of pointers.


Can you provide any citations for this (extraordinary, if true) claim? My knowledge of operating systems (which is based on experience in designing toy ones, but also studying other operating systems) suggests this is not the case on at least windows, macOS and linux. Feel free to correct me if I am wrong however!


> *In Fomos, an app is really just a function. There is nothing else ! This is a huge claim.*

To me this just screams the curse of greenfield development, where the architects are yet to discover what led other OSes to require all things they missed.


My expectation: Either Fomos has no security boundaries, or the security boundary looks a lot like syscalls...


What’s the difference in practice compared to every other OS where app is just a “int main() { … }” function?


In a regular OS there would be a bunch of implicit things provided by the OS that the app can access, while in this OS those things are explicit in the context.

But I don’t think that’s really that significant… you could, e.g., enumerate all the implicit things the OS provides, express them in a structure, and now you’ve got your explicit context. The “fomos” context is only as simple as it is because the OS provides a small number of simple things. Expand those to full OS capabilities and the context will get full OS complex.

The odd thing about this “OS” is that a running app is just the start function called repeatedly in a loop. This makes one app call a single coherent slice of app execution. That’s kind of interesting, but makes this pretty limited. E.g., it looks like apps are entirely cooperative. It’s maybe more of a cooperative execution environment.

Maybe it would be good for some embedded uses?



My takeaway was that compared to a dynamically linked binary, you don't need to literally patch it to get the correct addresses.

So you get the benefits of simplicity of a static binary and the size benefit of a dynamically linked binary.

Well that's the way I understood it anyway.


I'd suggest reading the linked readme, it does a great job explaining the differences. In other operating systems apps do dynamic linking & syscalls. Those don't exist in Fomos.


Make two apps A and B with their respective main() functions. Add some function foo() to app A. Now try to call A's foo() from B's main(). How many hoops do you need to jump through in every other OS to do this?


Not many hoops, why?

    /* Create function pointer of appropriate type, initialised. */
    int(*foo)(int, int) = NULL;
    
    int main()
    {
        void * ha;
        
        if ((ha = dlopen("A", RTLD_LAZY|RTLD_NODELETE)) != NULL) {
            foo = dlsym(ha, "foo");
            dlclose(ha);
        }
        
        /* if "foo" is non-NULL you can call it now. */
        
        ...


If you control the development of both A and B apps, then not so many. But generally you are not expected to load symbols from another application, you are expected to either use various IPC mechanisms or load symbols from a shared library instead of touching another application at all.


The amount of (wonderfully) horrible doors this opens is awesome.


None, because this exists outside the scope of an operating system. This is the domain of an executable/linkage format and language compiler.


Maybe the complexity lies in that possessive qualifier "A's foo()" - if this foo() can manipulate the callee state in some other virtual memory domain then it might not be sufficient to link against it in B's application.


> What’s the difference in practice compared to every other OS where app is just a “int main() { … }” function?

Your premise is wrong: in, say, Windows or GNU/Linux an application is, say, a PE/COFF or ELF executable, both non-trivial file formats. The int main(...) function in C is just a very leaky abstraction of all this.


As opposed to what? Code that can be directly loaded into memory with no headers? That's how .COM files worked.

There is no such thing as "program is just a function" unless it's compiled with the OS, like on some microcontrollers. For programs residing on storage, the OS needs a way to load them into memory along with its resources which means it needs to read some kind of format, even if it's trivial.


But you can separate the file format of the executable from the loading process, i.e. implement "doing what the OS loader does" on your own. This way, you can define your completely own executable format instead of being forced to use the one that your OS wants you to use, or even decide that it doesn't have to be a file format (e.g. stream the data from a network stream).


I think you're making a wrong assumption about how this OS works, its programs are still loaded from ELF executables, the parser is here: https://github.com/Ruddle/Fomos/blob/91cf732f173a01cdec3df43...

The "app is a function" distinction is simply about the `Context` struct passed it, otherwise it loads and runs executables as expected (load ELf memory segments in, jump to entrypoint)

Edit: I guess I'd also add that on Linux or other OSs it's possible to construct and run process completely from memory with no on-disk executable file. JIT compilers do effectively this, the produce the executable code completely in memory and run it.


> I guess I'd also add that on Linux or other OSs it's possible to construct and run process completely from memory with no on-disk executable file.

This is rather likely true. But since writing such code is much harder than, say, writing FizzBuzz, I assume that somewhere in the OS code, a "wrong" abstraction is used which makes this task far too complicated.


Why do you assume it's complicated? It's not hard, it's actually very straight forward. The hard part is generating the executable code in the first place (if you're not reading it out of an executable file).

I'm not really sure what you're looking for or expecting unless you want the OS to compile your code for you (which this OS doesn't do either).


This seems to match the definition of a unikernel. Here is another one in Rust: https://github.com/hermit-os/hermit-rs


Run old school classic MacOS.


Oh, no, you're right. That's classic MacOS development. It calls you for each event, and you can't block. It sucked.


It was fun sometimes to “recover” a crashed environment by going into the hardware interrupt console and forcing the program counter to the entry point address of the Finder and then hoping for the best with your corrupted machine state.


One thing that appears to me would be that you could still 'pipe' output from one command to another, BUT the data would not be copied from one part of the pipeline to another.

I would imagine it could greatly speed up composable things like that...


FWIW, it is possible to pipe data faster than memcpy: https://codegolf.stackexchange.com/a/236630


Well it would have to be for memory safety purposes, unless it would specifically be designed to allow siblings application to share the memo... Ugh that would lead to a very unsafe system and that would defeat the purpose of writing safer code in Rust!


Some neat ideas, but

> As long as the OS stays compatible with the old stuff in the context, it can add new functionalities for other App by just appending to the context the new functions

Welcome to backwards compatibility hell. You're boxing yourself in this way because you're forbidding yourself to remove old / defunct entries from the Context struct.

I think a better approach would be to introduce some kind of semantic versioning between OS and app. The app declares, somehow, what version of the OS it was built against or depends on. The OS then can check if the app is compatible and can pass a version of the Context struct that fits. This doesn't get rid of most backwards compatibility problems, but you keep the Context struct clean by effectively having multiple structs in the kernel, one for each major / minor version.


Author here, that's a neat idea. However I also like the simplicity in having only one runtime interface. If the OS has to handle every version anyway, an app in the future could just use padding to feel like it is "clean".

  struct Context{
   padding: [u8;256], // old stuff
   ctx: ContextV42
  }
Just typing that felt a bit wrong though. On the other hand the app declaring what version it is feels like what executable formats (like elf) already solve. I am trying alternatives.


> How do you sleep, or wait asynchronously ? Just return;

This is a bit strange. I would think async I/O in the style of io_uring would be fantastic but this kind of model seems to rule out anything like that. That’ll make it hard to get reasonable perf. It’s also strange to not support async as it’s a natural suspension point to hook into but you would have to give up a lot of the design where your application state has to be explicitly saved / loaded via disk if I’m not mistaken. Seems pricy. Hopefully can be extended to support async properly.

In a similar vein, I suspect networking may become difficult to do (at least efficiently) for similar reasons but I’m not certain.


If I had to guess, something like async IO would be implemented by updating the context with the IO request and returning, and then your function would be called when it is ready. The function appears to me to be the end of an event loop that receives an arbitrary state in the parameter, so it should be able to generalize to anything an event loop would do.

You give up the language-level support for coroutines and async, though...


Author here. Your guess is correct. An app is its own callback. It might be possible to write an small app-level executor to get back those sweet language-level support for async though.


> The argument that a cooperative scheduling is doomed to fail is overblown. Apps are already very much cooperative. For proof, run a version of that on your nice preemptive system : [pathological example which creates tons of threads and files]

The example is just too contrived. On a preemptive OS, apps typically hang in ways that don't turn the whole thing cooperative (thread deadlock, infinite loop, etc.). Also, a preemptive system could kill an app if it creates too many threads, files, or uses too much RAM, long before it gets effectively cooperative. Our systems are just more permissive.

> [Sandboxing] comes free once you accept the premises.

and yet

> any app can casually check the ram of another app ^^. This is going to be a hard problem to solve.

So no, sandboxing doesn't come for free.

That said, it's a cool idea and I wish the author success!


In browser land, all open sites share the memory of the browser heap, and there’s no crosstalk at all. I think the way out of that particular issue is creating a closure around the function (application) that effectively acts like the application’s own context. What if an app could open an app? Or put another way, what if an app could be an OS to another app?


There’s no cross talk because you can’t peek / poke arbitrary pointers in javascript. But you can in Rust.

And even then, I think modern browsers still isolate each tab in a separate process just to be safe. I don’t think they share memory.


Have you seen the "Birth and Death of Javascript" talk? https://www.destroyallsoftware.com/talks/the-birth-and-death...

But the basic idea of using a managed language like Java or something to eliminate the need for hardware process security goes way back. Microsoft's Singularity project is I think the best developed effort at this.


The example is even more contrived because it assumes all systems have terrible sandboxing like Windows and Linux.

Any system competently designed for robust sandboxing would have limits for all resources and reject requests when the limit is reached.


> In Fomos, an app is really just a function. There is nothing else ! This is a huge claim. An executable for a Unix or Windows OS is extremely complex compared to a freestanding function.

I'm curious what Fomos uses as a distinction between "process" and "executable."

On Linux a "process" is the virtual address space (containing the argv/envp pointers, stacks, heap, signal masks, file handle table, signal handlers, and executable memory) as with some in-kernel data (uid, gid, etc) that determine what resources it is using and what resources it is allowed to use.

An "executable" is a file that contains enough bits for a loader to populate that address space when the execve syscall is performed.

One of the distinctions is that you do not need an executable to make a process (eg, you can call clone3 or fork just fine and start mucking with your address space as a new process) and while the kernel uses ELF and much of the userspace uses the RTLD loader from GLIBC you don't need to use either of these things to make a process in a given executable format.

And finally, a statically linked executable without position independent code is "just a function" in the assembler sense, with just enough metadata to tell the kernel's loader that's what it is. But without ASLR to actually resolve symbols at runtime, it's vulnerable to a lot of buffer overflow attacks if the addresses of dependency functions are known (return to libc is one of those, but it's not unique).

I'm the first to point out the flaws in glibc and want an alternative to the Posix model of processes (particularly in the world where the distinction between processes, threads, and fibers is really fuzzy and that is clear even within Linux and Windows at the syscall level), but I'm curious what is going on in Fomos. Most of the complexity in "executables" in Unix is inherent (resolving symbols at runtime is hard, but also super useful, allowing arbitrary interpreters seems annoying, but is one of the strengths of Linux over Windows and MacOS, providing the kernel interface through stable syscalls is actually the super power of Linux and a dynamic context either through libc or a vtable to do the same thing is not that great, etc).


Thinking about it for a few more minutes, one thing that I think is the great mistake is a homogenization of executable formats (everything on Mac is Mach-O, everything on Windows is PE, everything on Linux is ELF, etc). There's no reason we can't have a diverse ecosystem of executable/linkage formats - and an OS with a dirt simple model for loading in code is a great place for that.


What would be gained by making more different formats? Also #!/bin stuff is executable in sense and the most files in Windows that have default application set.

Dirt simple model works until it doesn't. Without address space separation there is no safety between executables and cooperative scheduling is the same. Running faulty binary or simply bit flip in ram can crash the whole system instead of just that process.


I mean there's value in being able to grok it without having read "Linking and Loading" and we don't want an Asimov scenario (I can't remember if this is Foundation or the Last Question) where we just truck on assuming things work without understanding how they work. And there's a lot of arcane knowledge in executable formats buried in mailing lists and aging/dying engineers on how and why decisions about these formats were designed.

"How does loading work" has a simple answer: you map the executable into virtual memory and jump to the entry point. But the "why does XYZ format do this to achieve that" has a lot of nuance and design decisions - none of which are documented. Particularly things like RTLD, which is designed heavily around the design of glibc and ELF, while the designer of the next generation of AOT or JIT compiled languages for operating systems with capability based models for security might want to understand before they design the executable format and process model that may deviate from POSIX.

There's space for design and research there, and a platform that makes that easy has a lot of value. While I would encourage the designer of such a platform to read the literature and understand why certain things are done the way they are, it's valuable to question if those reasons are still valid and whether or not there's a better way.


> I can't remember if this is Foundation or the Last Question

Foundation. The Galactic Empire makes use of atomic energy and other technologies that were created in the distant past and the technicians can only (sometimes) repair but not create. 'The Last Question' has a question that remains unanswered throughout human history, which isn't quite the same thing.


> providing the kernel interface through stable syscalls is actually the super power of Linux

I thought that it was drivers? Linux isn't particularly unique for having a stable abi (and the utility of such a decision is highly questionable). The driver support however is extraordinary and undeniable.


Did you take a look on how Zircon (Fuchsia) is handling this? It is quite interesting.


How can you achieve any level of security and safety with un-trusted cooperative apps? Any app can get hold of the CPU for an indefinite amount of time, possibly stalling the kernel and other apps. There's a reason we are using OS-es with preemptive scheduling - any misbehaving app can be interrupted without compromising the rest of the system.


I remember Microsoft Research had a prototype OS written entirely in .NET some time in mid 2000s; IIRC they used pre-emptive multitasking, but didn't enforce memory protection. Instead, the compiler was a system service: only executables produced (and signed?) by the system compiler were allowed to run, and the compiler promised to enforce memory protection at build time. This made syscalls/IPC extremely cheap!

I think a slightly more fancy compiler could do something similar to "enforce" coop-multitasking: insert yield calls into the code where it deems necessary. While the halting problem is proven to be unsolvable for the general case, there still exists a class of programs where static analysis can prove that the program terminates (or yields). Only programs that can't be proven to yield/halt need to be treated in such fashion.

You can also just set up a watchdog timer to automatically interrupt a misbehaving program.


You might be thinking of Midori. Joe Duffy has written a lot about it on his blog: <https://joeduffyblog.com/2015/11/03/blogging-about-midori/>

They forked C#/.Net, taking the async concept to its extreme and changed the exception/error model, among other things.

There are several other OS projects based on Rust, relying on the memory-safety of the language for memory-protection. Personally, I think the most interesting of those might be Theseus: <https://github.com/theseus-os/Theseus>


The second thing you described is common in user space green thread implementations in various languages. Pervasively using it in the entire OS is just taking to its logical conclusion.

For performance though, I don't think halting analysis is needed. Even if the compiler can prove a piece of code terminates, it doesn't help if the inserted yield points occur too infrequently. If a piece of code is calculating the Fibonacci sequence using the naïve way, you do not want the compiler to prove it terminates, because it will terminate too slowly.


A general-purpose OS has to be designed so you never have scenarios where code hangs or yields too infrequently. Best-effort insertion of yield points probably won't cut it. Cooperative multitasking in applications exists on a smaller scale with fewer untrusted qualities.


Just thinking loud here, would it be an option to load the task to marshal the memory access and cooperative multitasking to something equivalent to llvm? Then multiple different compiler and languages could dock to that.

As long as all of them have to use an authorised llvm equivalent and that one can enforce memory access and cooperation that could look from the outside like a quite normal user experience with many programming languages available?


On smalltalk systems like squeak or Pharo, the user interrupts the execution of a thread when it hangs with a keyboard shortcut. And people don't run untrusted code in their "main" image, they would run it in a throw away VM. The same type of model could be used here using an hypervisor. This said, no one uses exclusively a Smalltalk system, it needs some infrastructure.


Is it actually possible to implement security in this OS without a complete redesign and basically redoing what all other existing OS have already done?

I am aware of two ways to enforce security for applications running on the same hardware:

(1) at runtime. All current platforms do this by isolating processes using virtual memory.

(2) at loadtime. The loader verifies that the code does not do arbitrary memory accesses. Usually enforced by only allowing bytecode with a limited instruction set (e.g., no pointer arithmetic) for a virtual machine (JVM, Smalltalk) instead of binaries containing arbitrary machine code.

The author of Fomos doesn't want context switching, memory isolation, etc. And Rust compilers don't produce bytecode. Is there another way?


Theseus is an example of (2), in Rust, without bytecode. As far as I understand, done by enforcing no-unsafe etc rules in a blessed compiler, so basically source code becomes the equivalent of the byte code in your thinking. At a glance it's very similar to Midori, but the details of how it's done are quite different. In Theseus, drivers, applications etc are ELF objects, dynamically linked all together into one executable (which is also the kernel), with some neat tricks like hot upgrades.

https://github.com/theseus-os/Theseus

https://www.theseus-os.com/


Theseus is very cool, as are Singularity and Midori. I'm trying to think of how a general-purpose OS could incorporate ideas from these intralingual OSes while allowing other programming languages to be used, without sacrificing security. I haven't gotten anywhere yet because I'm inexperienced.


I think the Theseus design would accommodate this in 4 different ways:

1. native Rust modules

2. arbitrary code running in WASM sandbox

3. arbitrary code running in KVM virtualization, perhaps with a Linux kernel there to provide a backwards-compat ABI

4. one can compile the WASM+untrusted app into trustworthy machine code (still implementing the WASM sandboxing logic for the untrusted component); I recall Firefox did this with some image handling C++ code they didn't find trustworthy enough

I know the Theseus project is working toward WASM support.


Wild guess: you could have a single address space shared by all "programs" while using virtual memory to restreint visibility on which page is accessible at a certain time. Like, at runtime, resort on a segfault to check some types of security tokens the caller would to check they are allowed to access the page and make the call. No idea how practical this would be.


> which page is accessible at a certain time

Sounds like a context switch to me :) (at least for the MMU registers)



I’m terms of cooperative multitasking I suspect this is not the same as what we had in Classic MacOS in the sense that we now have a zillion cores, so presumably one or two non yielding processes don’t actually hold the whole system up.

I’d also assume that a function that is misbehaved (doesn’t return) could be terminated if the system runs out of cores.

My point is that cooperative multitasking doesn’t necessarily equate to poor performance. Time sharing was originally a way to distribute a huge monolithic CPU among multiple users. Now that single user, multi core CPUs are ubiquitous, it’s past time that we think about other ways to use them.

I’m really excited that this project exists.


Just adding to this:

> cooperative multitasking doesn’t necessarily equate to poor performance.

I meant to say “poor interactive performance”

The lack of context switches in this model is likely to actually improve performance. So now I’m curious what would happen if we turned the Linux timeslice up to something stupid like 10s :)


The lack of context switches comes largely from having no security... That's the context being switched, memory protection!


That doesn’t seem right to me. Of course a context switch includes the memory protection, but in a traditional OS there are also registers and other CPU state that need to be saved since a process can be interrupted just about anywhere.

I suppose that I’m idealising a bit here but ISTM that the structure of FOMOS means that the CPU state doesn’t need to be saved, so the context switch involves only memory protection, register resets, stack pointer reset and little else. You don’t even need to preserve the stack between invocations. And unlike preemptive multitasking, there seems to be little or no writing to memory needed, which would seem to obviate a bunch of contention. (Noting that it’s 30 years since I fiddled with operating systems at this level)

Frankly I find this really elegant and exciting.


The TLB flush is the only part of the context switch that's really slow, and it's the part that's needed for a security boundary.


Theseus OS has no context switches, while being even more secure than conventional OSes, all without any reliance on hardware-provided isolation.

  Theseus is a safe-language OS, in which everything runs in a single address space (SAS) and single privilege level (SPL). This includes everything from low-level kernel components to higher-level OS services, drivers, libraries, and more, all the way up to user applications. Protection and isolation are provided by means of compiler and language-ensured type safety and memory safety.
https://www.theseus-os.com/Theseus/book/design/design.html


Yes, but Theseus doesn't run arbitrary binaries.


It got Wasm support recently, and extending this idea of emulators/interpreters, it can run any arbitrary binary in the future.


Wouldn't that be somewhat outside of Theseus' design and add overhead more akin to modern OSes? Theseus relies on Rust's type system (ownership/borrowing and otherwise) to ensure that all binaries have many verified properties; arbitrary WASM program can't hook into that, yes?


The vast majority of programs running on a user's computer today are already running in a VM (JS and WASM), and the number is only increasing. I do not know of any argument in behalf of running everything in a VM (even when it's not a requirement) from a cybersecurity perspective, but I suspect there may be one.

You're right, but maybe this is the way to go and the tradeoff to accept. After all, the ideas behind Theseus feel so obviously correct, alike to those of Nix.


Would be interested to hear the plans for security in more detail.

But in general I think these types of experiments show that operating systems could be improved with greenfield designs.

Reminds me a tiny bit of Mirage OS. https://mirage.io/


It's great to see a hobby OS with some interesting ideas coming out of a developer's need to scratch their own itch, pointing to a real potential for improvement.

I'm not sure, but my guess is that the author might want to checkout out Barrelfish and that there might be ideas they might find worth stealing from there.

https://barrelfish.org/documentation.html


I'm not sure you can be an exokernel and cooperatively scheduled. The only thing an exokernel does enforce multiplextion boundaries on hardware resources. Yeah, it'll be six of one in a lot of IO bound workloads, but IO bound workloads aren't the only kind of CPU workloads.

And to be fair, exokernels are probably one of the least understood forms of kernel. I feel like professors/textbooks/papers are legally required to explain the concept poorly.


Redox is full fledged OS written in rust by Pop OS developer

https://github.com/redox-os/redox


It's also of another architecture. Bit more conventional although still differs from mainstream systems and having some interesting features.


So this is a bit like React on the OS level? Cool project, especially the cooperative scheduling part. Although I'm not sure why insist on a single function (if not for a "hold my beer" moment), because for any non-trivial program the single function will obviously act as the entry point of a state machine, where the state is stored in the context. I'm also not sure about redraws - is the window of a program redrawn every time the function runs?


Hmm all apps in a single loop? Sounds like Oberon a bit. But for Rust? Neat!


This looks very interesting, but it wasn't clear to me how to run this, in particular, how the demo at the top of the README was run.

Does this repo build a standalone OS the runs on a bare machine, or does it run in a VM like QEMU, or is it a Rust application program that runs hosted on a conventional OS?


I also do not know. Perhaps worth asking over at https://github.com/Ruddle/Fomos/issues


It's a cool idea, was just thinking if a program is just a function, can I just replace any function with a malicious code acting as dependency of another function? Or how would I require a specific function through signature? I guess me as a developer working on Fomos, would have to declare fully qualified dependencies in a manifest and then be able to call by function name my dependencies in my code? But then what about versioning? Do I have to bundle in my app all my dependencies? But cool idea


I'm not following. How does program-as-a-function matter in this regard? Isn't it a question of security and permissions? In most operating systems, compilation, linking, and run-time libraries determine dependencies. (I don't know how much of this will differ in something like Fomos.)


Probably you're right, it's about the same thing as it is now. I was thinking in this terms: If a program A depends on a function D, this function is another program D, when I run program A, it looks for function D, now normally in compile time I have a full path to dependency, now instead a system has a set of functions running, let's say that one of those is function D, how do I ensure as a user that function D is not mimicking something it is not? And I guess it's the same of right now, just make sure to don't run malicious software, but I was also thinking that probably it could be much harder to track programs as functions reusable from other functions(apps)


The specific mechanisms are key to understanding. Your question prompted me to review the details of how system calls and linking works in Linux and think about it in a broader context.

Programming languages also have considerable variation when it comes to dependency management. There is a menu of options w.r.t. symbol lookup and dispatch, ranging from static to dynamic.

I’ve mostly been an application level developer. Something interesting about OS development is system function calls behave differently when called in different contexts (privilege levels, capabilities). One mechanism in play is dynamic dispatch. The end effect is that the underlying details of a system call can be quite different for different callers.

(The Unison language has some innovative ideas IMO.)

If you come across a good resource that covers these topics as a “design menu” across different OS styles and/or a historical look at security mitigations (as opposed to only a summary of what is used now in Linux), could you share it?


Can someone explain to me what FB in the Context does?

it's... an array of pixels? does that imply all apps must draw themselves?


Sounds like it's a framebuffer, and yes the apps draw themselves.

Looking at the source code of the cursor app [0], we can see it draws the mouse cursor and skips the rest. The transparent console app [1] is doing something more complicated which I haven't tried to fully understand, but which definitely involves massaging pixels and even temporarily saves pixel data in ctx.store.b1 and ctx.store.b2 so it looks like some kind of double buffering.

  [0]: https://github.com/Ruddle/Fomos/blob/cba0460af59e63f46c7646f8a2f29d574ff0d722/app_cursor/src/main.rs#L75
  [1]: https://github.com/Ruddle/Fomos/blob/cba0460af59e63f46c7646f8a2f29d574ff0d722/app_console/src/main.rs#L384


> The transparent console app [1] is doing something more complicated which I haven't tried to fully understand, (..)

If apps draw themselves, and transparency is involved, this could need:

a) Save background which is overwritten ("damage areas" is a modern description, I think?).

b) Alpha blending - calculating a weighed average between background & what you're overwriting it with.

c) And maybe some kind of text-buffer -> bitmap conversion (if not done ahead of time).

Enough pixel massaging right there.


Does this mean that apps that don't want/need to be visual are still burdened with it?


or ask other apps to draw for them.


Theseus OS (https://www.theseus-os.com/) is also an OS written in Rust. It's a safe-language OS and I believe it's the future of the OSes due to its unique features.


It is impressive, but a major limitation is that programs currently need to be verified by the Rust compiler. I love Rust, but not everyone should have to for a general-purpose OS. There's always emulation/runtime layer, but that seems like a step backwards. Maybe it can be a decent compromise?


It's obvious that native binaries are replaced by web more and more every day. Web uses runtimes (Wasm and JS). We can definitely afford to run everything in runtimes and emulation. Btw, Theseus recently got Wasm support.

Games? Write them thar thangs in Rust.

The ideas of Theseus is akin to those of NixOS, one easily feels they are 'the one obviously correct way' of doing it. I recommend the founder's presentations on YouTube.


Rust is great, but "rewrite it in Rust" is a tired and unrealistic refrain. Aside from performance concerns, an emulation layer defeats the validations that Theseus incorporates as an intralingual codebase. It'll be like a mix of a microkernel and a monolithic kernel; drivers, file system managers, and whatnot can stay in the OS, but there'll have to be heavyweight userspace processes all the same. Theseus and Singularity/Midori are great proof of concepts, but I think we need a third (fourth? fifth? What number are we on?) path.


To clarify, there is no motivation towards RIIR except the fact that Rust’s guarantees are required for Theseus’s very existence.

I do not understand how the emulation layer can defeat validations. As long as the emulator is conformant to validations, it should be okay. I’m probably ignorant of something here.

Regarding performance, in a presentation, the Theseus dev mentioned one study which showed the performance tax of context switching with attack mitigations (Spectre etc.) can be as much as %15-%30 in modern conventional OSes.


To my understanding, any kind of emulation layer or runtime can't leverage Rust's type system to achieve the cheap context switches and other benefits that define Theseus today. It seems to me that programs running on the emulation layer will essentially be conventional heavyweight processes. There are still benefits over conventional monolithic OSes, but not as significant.


Can someone explain why a bezier function is used in the mouse app? I would guess it's to help give smoother and more natural movement to the mouse using predictions of where it's going based on last few points, but I can't read Rust well enough to make sure.


> Apps do not need a standard library, any OS functionality is given to the app through the Context.

Don't need I can follow. But some apps will have dependencies -- some might even be tantamount to a standard library in other OSes. The Context provides all that too?


Finally, someone that tries non-UNIX alternative concepts with their hobby OS...


The "sandbox" is just some compile time check. As soon as you play with raw pointers, everything breaks down.

A good experiment, but nothing useful. Maybe the author can generate some new ideas from this.


Now add DOM and HTML parsing and you'll have a web browser


So there is no "kernel space and user space"? Then how to protect apps, like prevent bad app read other app's user password with pointer calculation.


As per the readme, security is not implemented yet.


Well there is also that small issue that implementing security for this design is pretty much impossible without destroying performance and end result is quite standard operating system.

Polling style scheduling will just be so slow if calling each executable always involves context switch. And then cooperative scheduling isn't really possible if some process doesn't play nice.


The next logical step would be to only support wasm programs ;)


i guess, that since each app is a function, each app only has access to the parameters it was called with.


This can run very efficiently in a Virtual Machine.

So many virtual machines run complex kernels and security features, for just one application listening on some port.

While this is a toy operating system for some standard Desktop scenario, it is already promising for another scenario, with the move of services to managed containers (serverless) in the cloud, with huge overhead (relative to the tasks performed) and slow startup time.

Then, this can be the AWS Lambda runtime for Rust (add networking and database libraries), and it will be faster and more efficient than other Lambda implementations for the other languages.


from-the-page

''' Exo-kernels are interesting, but it is mostly a theory. '''

is that _true_ ? what is the difference between exokernel, and hypervisor ?


A hypervisor is something that might run an exokernel or a more traditional OS. And an exokernel can be run on bare metal rather than a hypervisor.


Xen is considered a "bare-metal hypervisor" because you don't run it as a virtual machine on a Windows/Mac/Linux/etc. host OS. sel4, a very minimal microkernel, can be considered a hypervisor. To my understanding, bare-metal hypervisors and exokernels are quite similar, as they are both a thin layer multiplexing hardware resources and providing security. I have read that the Exokernel exposes more of the hardware to applications.


Is it blazingly fast, though?


Fear of missing out system?


I'm glad I'm not the only one who read it like that.


Does it stand for Fear Of Missing an Operating System?


> Context gives any OS function necessary, ... That way, apps could be freestanding, and compatible on multiple OS

Really cool idea!


[flagged]


This is quite harsh for a repo explicitly created to experiment with non-Unix OS ideas. It is an experiment, it doesn't have to e useful.

Personally, I enjoy when people post things like this as it helps me learn.


See also:

- Experimental

- OS

Ain't no body going to use your OS generally speaking.


Apart from KUDOS for effort, this sentence sounds best for it:

...you've got to start with the customer experience and work backwards for the technology. You can't start with the technology and try to figure out where you're going to try to sell it....


So… we now need to consider CX for experiments?


With no offense to the OP, I genuinely don't understand the benefit of writing an experimental OS. Do these types of projects push the envelope in real world systems? What other benefits exist beyond a (hopefully fun) academic/intellectual exercise?


They are incredible learning exercises, often taught in higher level CS degrees at universities. If you want to understand how Linux (for example) works, it's really helpful to understand the problems that it's solving; one of the best ways to do that is to build it yourself.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: