The benefits and costs of writing a POSIX kernel in a high-level language
usenix.org
usenix.org
Lots of asterisks and caveats apply I'm sure, but it bolsters the argument that writing kernels and device drivers in a high-level safe language wouldn't impose the 2x or worse perf penalty detractors like to claim. I know I'd trade 10% perf for the elimination of whole classes of security vulnerabilities.
(For the same reason, I want the standards committee to produce a bounds-checked dialect of C where I can choose to pay some perf cost to get real dynamic and bounds-checked arrays)
Now, could one of these academic kernels have sheer engineering effort put into optimizing that 5-15% regression away? Probably.
So saying "just don't use GC" as a answer to improving throughput shows ignorance.
So what are the real tradeoffs?
GC is less work. Is easier to get right. And lets you handle complex self-referential data structures more easily.
Reference counting lets you handle real-time constraints more easily. Is simpler to understand. And results in faster freeing of potentially scarce external resources.
The basis on which the system works is easy to understand.
It could also mean they use Rust, memory pools, some kind of checker that catches those errors, or separation logic. Probably that order due to popularity.
As a counterpoint to "don't use GC," there's also low-latency or real-time implementations of RC/GC to consider as well. A lot of developers using RC/GC languages don't know they exist. Maybe we need a killer app that uses it to spread awareness far and wide. More open implementations of the concept, too.
These exist but tend to have poor throughput, frequently set a maximum amount of memory you're allowed to use, and typically don't perform well when compared to commonly used garbage collectors.
We constantly pay this cost for other security-related issues, but when it comes to a systemic, one-time penalty that almost entirely eliminates a class of bug we freeze up.
Obviously it's a lot of engineering effort to transition but we don't think twice about other security issues
In general, these types of decisions aren't uncommon.
In theory, it's possible to write bug-free code in C or assembly. In practice, it just doesn't happen. The rule of "worse is better" ensures that the rare projects that put in the necessary effort to manually eliminate the bugs that HLLs catch automatically will get steamrolled by projects that don't (witness the adoption rates of Linux versus OpenBSD).
That said: I think that tool developers have only scratched the surface of the possibilities for compile time checks. I'm always amazed how little the industry has learned from projects like Ada. There's an opportunity in there somewhere to have that cake and eat it too.
Ironic how its descendant didn't managed to replicate that feature.
Also Burroughs B5000, developed in 1961, already took security as major OS feature.
The problem is that UNIX designers never took security as serious as other OS designers were doing on their OS implementations.
Had UNIX been a commercial OS sold at the same price levels as VMS, OS/360 and others, instead of given to universities with a symbolic price due to Bell Labs commercial restrictions and history would have been quite different.
That said, nobody’s disputing that a “lot” of people care about performance. It’s worth taking a look at the last slide of the presentation, which states clearly that if you care about performance, you should use C:
Performance is paramount ⇒ use C (up to 15%)
Minimize memory use ⇒ use C (↓ mem. budget, ↑ GC cost)
Safety is paramount ⇒ use HLL (40 CVEs stopped)
Performance merely important ⇒ use HLL (pay 15%, memory)The conventional wisdom shared by many of today's software engineers calls for ignoring efficiency in the small; but I believe this is simply an overreaction to the abuses they see being practiced by penny-wise-and-pound-foolish programmers, who can't debug or maintain their "optimized" programs. In established engineering disciplines a 12% improvement, easily obtained, is never considered marginal; and I believe the same viewpoint should prevail in software engineering.
Donald Knuth. Structured Programming with go to Statements. (It's the same paper that the oft-misquoted "premature optimization" quote comes from.) http://www.cs.sjsu.edu/~mak/CS185C/KnuthStructuredProgrammin...
Every decade, hardware performance skyrockets upward, but developers adopt libraries/frameworks/languages/low standards/habits which give rise to woefully inefficient software. It's so atrocious that it seems to roughly counterbalance the hardware advances. Heavyweight single-page web applications seem to be the endgame of this trend, for now.
See Wirth's law: What Intel giveth, Microsoft taketh away.
That said, the 80-20 rule is a real thing. It's possible that you'll get better real performance by writing 80% of your code in Python and the performance-critical 20% in carefully optimised C, than by spending the same amount of time writing it all in Java.
That's right, but "improvement" doesn't necessarily mean performance. Adding a GC would yield more than 12% reduction in vulnerabilities and bug count, which are also "improvements".
Also when compared to Rust-style lifetimes? (Did Redox OS have any CVEs yet?)
The Checked C project is working on such a bounds-checked dialect of C: https://www.microsoft.com/en-us/research/project/checked-c/
Since nothing in the C language prohibits bounds-checking for arrays, we can retrofit this to existing insecure languages rather than forcing all programmers to rewrite their software in some other language, which realistically won't happen for a decade or longer.
I don't know if things would be better now with more optimizing compilers, but I remember being somewhat surprised by this at the time, since you'd expect most of the overhead from passing fat pointers around to get optimized away.
There are many other examples of people trying to band aid and improve C's security history.
The biggest problem is that the developers that are supposed to actually adopt those practices don't really care, because performance trumps security.
Those that actually care end up moving to C++, or some other language, so we end up with this selection bias.
Thankfully they are being forced to change, as being 100% connected to the Internet does wonders to security.
Secure by default with option to turn those features off for performance, has been the way other systems programming languages have been designed since 1961.
That C culture is now being forced to change, because fixing memory corruption CVEs in devices that are permanently under security exploits attacks does not scale.
Firstly, I would like to know if that's an average of 5-15% slowdown, and if that means some operations are multiple slower and some are close to equivalent. Average means nothing if your workload triggers the pathological cases.
Secondly, you might be giving up classes of security vulnerabilities and gaining classes of kernel DOS bugs. I don't write kernels, but my understanding (mostly from comments of people here that do), is that kernels rarely allocate or de-allocate after book, as they immediately reserve the memory needed for their limits and then work within those limits.
Believe me, I'm not a proponent of C. I'm critical whenever it comes up, as I think it's caused a huge amount of harm. That said, of the places where C has a well deserved reputation of being the right tool, I think kernel development is one of them. That isn't to say other languages don't have something to offer, but this is one area where you actually seem to need all the crazy unfettered access that C gives you.
So, I'm not sure C is the right tool today given anything it can do can be done in safer languages plus assembly or sometimes even invoking/extracting-to C. In the latter case, the C part becomes incidental to where history went with who wrote or optimized what in what language. The person developing uses as little of C as necessary in that case. Software can also be incrementally ported to something safer, more maintainable, and so on.
> People are also writing low-level, efficient software in Ada/SPARK, Rust, Oberon (Astrobe Oberon), Pascal/BASIC (MikroElektronika), Haskell-based DSL's (eg Ivory Language), and ATS.
I fully think other languages can be used to quite good effect. I was specifically thinking of Rust and Ada/SPARK, and for the latter one coming to my mind so quickly you can only blame yourself. ;)
> So, I'm not sure C is the right tool today given anything it can do can be done in safer languages plus assembly or sometimes even invoking/extracting-to C.
Yeah, I'm not so much advocating for C as a good choice, as much as excusing it as a somewhat adequate choice given the specific circumstances. A rigorous set of usage guidelines, which I think are likely easier to implement and stick too given the type of project, mean that C is only slightly more dangerous, but also slightly easier to work in given the needs, than some other options. Even this small advantage is quickly being eroded by advances in other languages though, so I doubt it will persist (even if only in my mind) much longer.
The authors would probably not call it "unoptimized". From the last paragraph in Section 8.8:
"It took effort to make Biscuit achieve this level of performance. Most of the work was in understanding why Linux was more efficient than Biscuit, and then implementing similar optimizations in Biscuit. These optimizations had little to do with the choice of language, but were for the most part standard kernel optimizations (e.g., avoiding lock contention, avoiding TLB flushes, using better data structures, adding caches)."
I agree that it's impressive, and better than I would have guessed.
This would turn into a massive research project on GC and language runtimes where the commodity server market already is and especially where it is going considering a dual socket AMD Rome will have 256 threads and a single socket POWER10 would have the same. To consider that project in terms of time scale and cost, one can look at the history of JVMs. Go's type system might make some things slightly easier but there is no implicit scalability codex they've unlocked beyond state of the art JVMs that have been evolving for 15 or more years.
This is why Rust brings something novel to the table for systems programming. With the borrow checker, memory safety can be added in many cases without runtime overhead and follow on contention.
An extended though, we need to be moving _toward_ sympathy with the on chip network and implicit message passing nature of cache coherency policy. This is mostly a library/ecosystem shift rather than language.
We would be further down this path if bigger companies really cared to push down this path no matter what, but politics and short term profits always win in the end.
There is therefore a trade off and obviously the lower level the code the less suited high level languages are.
Thanks to Internet and IoT it seems that we might be finally at the turning point where OS security starts to be taken seriously, so lets see how long that resistance keeps to uphold.
It was refreshing to see that a large set of Linux Security Summit 2018 talks were all about how to fix C related issues on the Linux kernel.
If you don't want to wait on the standards committee, the SaferCPlusPlus library provides memory-safe substitutes for commonly used unsafe C/C++ elements (including pointers). These memory-safe elements require a C++ compiler, but because they are largely compatible with their native counterparts, you can write programs that can switch between using memory-safe (C++) elements and (unsafe) straight C elements with a compile-time directive.
That is, you don't have to choose between safety and performance when authoring the code. You can make the choice at build-time. So you could, for example, provide both "memory-safe" and "non-memory-safe" builds of your software. There was even a (now long neglected) tool[1] to automatically replace arrays/buffers in C source code with (optionally) memory-safe substitutes. (Not just bounds-checked, but safe from use-after-free.)
> I know I'd trade 10% perf for the elimination of whole classes of security vulnerabilities.
Full memory safety may sometimes cost a bit more than that [2]. But a nice thing about this approach is that you can choose to use (faster) unsafe code in critical inner loops, and memory-safe code everywhere else.
[1] Shameless plug: https://github.com/duneroadrunner/SaferCPlusPlus-AutoTransla...
[2] https://github.com/duneroadrunner/SaferCPlusPlus-BenchmarksG...
Given that Bryan Cantrill is gung-ho about rust, and he's got quite a lot of knowledge in this area, maybe someone can woo him to help (even if in an advisory role).
At work, I try to be a good boy and write proper commit messages, always.
On personal projects, I often just use "." or something equally meaningless. Sometimes I can't be bothered explaining myself, and on a personal project nobody is going to be burned by it but me.
https://github.com/mit-pdos/biscuit/commit/b262c40b1baf1fb1a...
?
Is that a fair way to make this comparison?
Also, ”This results in heap bounds of less than 500kB for all system calls but rename and fork (1MB and 641kB, respectively).”
How does that compare to the C-based kernel?
Finally, how does this system hold up after days of uptime?
Or are there go features that make it inherently slower than C for kernel programming, even when ignoring GC?
1. Your skepticism for using Rust cites [26], the PLOS paper from the same people who did [27]. They've essentially recanted, in that their PLOS talk was right after Niko's keynote, and he then talked them through how interior mutability works in Rust. Other than [26], are there other lingering concerns? I could imagine so, but it would be great to take stock.
2. You don't seem to page to disk, and do OOM shootdowns instead. If you did enable disk paging, how comically bad an idea does a tracing GC become? :D Naively, I could imagine RC-GC being less horrible, but perhaps that's my built-in (and absolutely for real) bias.
2. GC performance would be basically unaffected by paging, since kernels don't page their own heaps out to disk and thus a kernel heap access would never have to wait for a disk IO. Or maybe you had something else in mind?
Re 2., I assumed (incorrectly, it seems) that virtual memory mappings can eventually spill to disk. If you grab 1/32 of RAM For the kernel, then you could only virtually map 16x RAM if the page tables have 512 entries. Admittedly, I took my OS class in the previous millennium, so I might be out of date here...
https://github.com/google/gvisor
They use it in production for App Engine sandboxing.
I'd be keen to see what the smallest binaries the reference implementations of these languages can produce are.
If you really want to get extreme with this, http://mainisusuallyafunction.blogspot.com/2015/01/151-byte-..., while a bit old, talks about various things that are still true, though some stuff has changed a bit. There's also https://www.rust-lang.org/en-US/faq.html#why-do-rust-program...
Additionally, some people have done more work since then; the smallest binary rustc has ever produced is now 145 bytes: https://github.com/tormol/tiny-rust-executable
The 145 byte example also not being a fair comparison, since you've left out the standard library! Obviously C can do that too, but it's pretty neat to see the progress rust is making.
(anyway, I'm going off topic since we're discussing kernel programming here).
https://blog.erratasec.com/2015/03/x86-is-high-level-languag...
It's like how you can implement Java, C++, python, Haskell and any other (practical) language in C, despite C having no support for e.g. classes or lambdas. Or how humans can design and build combustion engines, despite us not having the capability to ingest gasoline in order to rotate an axle at 6000rpm ourselves.
But yes, I understand that this noodling has little to do with what the authors were going for, nor really with current reality.
Definitely a cool--amazing even--project.
If this approach took off, maybe it would light a fire under the chip industry to put a little more thought into hardware-assisted garbage collection.