On some level, yes, it's the best we can do. Unless you want to add hardware semantics into your language. Unless you do, writing two bytes to 0xB8000 is going to be an arbitrary memory access. At least, that's my take.
On some level, yes, it's the best we can do. Unless you want to add hardware semantics into your language. Unless you do, writing two bytes to 0xB8000 is going to be an arbitrary memory access. At least, that's my take.
The two places of unsafe are barely just the minimum.
When interacting with UEFI you have to include a bunch more unsafe clauses since you'll be FFI'ing to PE-calling convention C code to wrap the UEFI methods. You'll have to allocate memory without being able to malloc which means passing a raw memory block, allocate in C, to rust and tell the frame allocator to use that.
Setting up a syscall is unsafe, you have to use a naked function with internal unsafeness and the entire construct is unsafe because a syscall can easily crash the system if it doesn't return properly.
You will have to dereference null pointers because on kernel level a null pointer is a valid address (my UEFI places a memory region at 0x00 for about 30 pages which is a great starting region) unless you waste a buffer region. But Rust and LLVM don't allow that safely at all.
The very nature of a kernel is unsafe. It has to do very unsafe things in an unsafe way to enable software running in Ring 3 to make safe assumptions about it's environment as long as it gets scheduled.
Of course, Rust is somewhat better than C since it's type system allows one to express things without the entire construct blowing up every 2 minutes unless you're developing a kernel for years.
Yes, this is only one subsystem. Obviously, as you say, there are more than that in a realer kernel. But even larger ones have still shown that the vast majority of their stuff works in safe code.
> Setting up a syscall is unsafe
Fun detail, this isn't unsafe anymore on x86! You don't need naked functions; you can
extern "x86-interrupt" fn some_handler()
and LLVM knows how to properly do the codegen! This is kinda emblematic of what I mean: you can do more with safe code than you'd think.On x86, not x86-64, which has the syscall instruction, replacing interrupts for syscalls (technically x86 had sysenter)
The syscall instruction requires writing the address of the syscall function into the LSTAR MSR. Additionally, the syscall entry is purely naked, only the EIP and Segments are preserved and in a way that requires the caller to handle it.
Sysret is also particularly nasty in being unsafe in various ways, including being buggy on all Intel CPUs.
Atleast it's faster than interrupts and you don't technically need the -mno-redzone for it if it's not part of your interrupt handling code.
However, those two calls to unsafe rely on an invariant maintained by the rest of the code that is not marked unsafe. Effectively, the entire module has to be treated as unsafe code by the programmer.
The question is, how much code is in this state, vs being purely safe? My experience and that of others shows that it's generally a smaller amount than many people not experienced with Rust assume.
Basically, I find the "there's always unsafe code somewhere at the bottom and therefore it's the same as 100% unsafe" to be overly reductive.
I totally agree; it's not the same. However, I think there is disagreement about whether saying only "there's always unsafe code somewhere at the bottom" is better or worse than saying "it's 100% unsafe".
I tend to write some sketchy unsafe code, and the only thing I can say with a straight face is "please don't use this if you care about safety", i.e. treat it as if "it's 100% unsafe". I'm slightly weirded out that Rust has a keyword that amounts to "I understand UB really well; tell others to trust me on it transitively".
I'll be less weirded out when the UB story gets shaken out more, or if cargo gets an "audit-unsafe" option that shows you all unsafe blocks you transitively depend on (does that exist yet?). Still like Rust, but letting randoms write unsafe code is scary.
Are you saying that Rust doesn't have a better memory safety story than C++? In practice, that hasn't been our experience.
I don't use C++ anymore, but I am not clear on how Rust has better memory safety guarantees than C++. I do like its story more, for sure, and I could see how they might turn in to guarantees (but I could also believe that someone could identify a similarly safe fragment of modern C++, and don't want to start that).
Edit: will rust-lang.org take a PR changing "guaranteed memory safety" to "memory safety story"? ;)
I hope not. The value proposition of Rust is that if you stick to writing code without unsafe, then you have a guarantee that you will not fall victim to memory unsafety unless there is a bug in the language or in some unsafe block in a dependency you're using.
Personally, I find your quibbling in this thread pretty strange. Rust very clearly provides more memory safety guarantees than C++. For starters, you could not take the guarantee I just stated above and apply it to C++ because there is no such thing as "C++ without unsafe blocks."
I have used other memory safe languages, and I find their guarantees qualitatively different from Rust. I would say of them "you will not fall victim to memory unsafety unless there is a bug in the language or in the standard library" with no mention of other dependencies. The standard library is then fairly thoroughly audited.
By comparison, you have e.g. the `memmap` crate, which I like and use, written by serious people known to you, where `unsafe` is used in construction of the memory map. It is then on the user to ensure that there are no concurrent writes to the backing file for the lifetime of the map, at the risk of (as I understand it) UB. Would you describe such a program as "safe"? Or at least, can you understand why I might not? If a concurrent modification happens, is the bug to be found in the `unsafe { MmapMut::map_mut(&file)? }` block, or perhaps farther away where you safely open and modify the file before dropping the map?
I'm sorry if this comes off as quibbling; I do actually care about getting these things right, as I want to rely on them and do more interesting things (e.g. de-abomonate mapped memory).
What I should have written is: "you will not fall victim to memory unsafety unless there is a bug in the language or in the standard library, or you explicitly opt out of safety". Using `unsafe` in C# is opting out of its safety guarantees (and requires the `/unsafe` flag); I'm pretty sure none of the C# team would say "guaranteed memory safety" of unsafe C#. I expect the same is true of Java, and have no clue about Python.
To the extent that we are talking about unsafe C#, I think you are right; it's mostly the same as Rust. But, no one would claim that unsafe C# has "guaranteed memory safety" where the guarantee is "as long as your code and all the code you bring in don't have memory safety bugs". At least, I hope not.
My original point was just meant to be that there is the risk of harm when you try and quantify a language as "more safe" because there are fewer regions where unsafe operations are permitted. People might read "guaranteed memory safety" and misunderstand what it means, as evidenced by me not actually knowing what it means.
"Rust is not perfect, so it's basically the same as C"
The fact that "american fuzzy lop" and "syzkaller" have gone through the kernel and found dozens of use-after-free's (see https://github.com/google/syzkaller/blob/4bd70f/docs/linux/f...), shows that even incredibly carefully programmed C messes up in totally rote ways.
The basic cleanup code which is able to be exploited here would not be in an unsafe block if this were rust. In fact, because rust abstracts memory management out better, most of the cleanup code wouldn't exist at all.
Rust also allows better abstractions. For example, Rc<T> in rust is much easier to use without screwing up than the manually handled reference counters littered throughout the kernel. It seems obvious to me that numerous deadlocks in the kernel could be avoided by not hand-rolling refcounting everywhere.
You're right that there's still the potential with rust for some code to compromise the safety of the entire kernel.
But with rust that's a subset of code, not practically every single line of the whole thing.
It seems to me like memory corruption/use-after-free/etc only being able to happen in a small fraction of code is better than it being able to happen anywhere.
Rc requires memory allocation. When you boot a kernel you don't have that. Until you set it up, you can't use anything that requires a heap, only stack values.
The best solution I found is to use C code to allocate raw memory and then go through the elaborate process of setting up kernel address space, after which you setup a frame allocator, after which you can setup interrupts and finally use pagefaults to normally use kernel heap.
But wait there is more! During a pagefault interrupt you can't use the kernel heap in a writeful manner that is not inplace. Since a page fault during a page fault is a double fault and thusly your only option is to crash the kernel. You can only read from the kernel heap, modify in place in the heap and write to pagetables.
Same goes for some other interrupts. Which btw, violate a lot of ways Rust code likes to work. An interrupt is like a coroutine but you don't have a nice runtime that abstracts it for you, instead the IRQ just smashes into the active execution and let's hope you haven't held any locks to important kernel stuff at that moment because you will have to crack that lock open.
Sure, the kernel has reusable things like its embedded linked list etc, but they're not type-safe, and they can't represent ownership.
With rust's type-system, a generic rc-like thing could be created which is suitable for the world of the kernel, even if it's not the stdlib Rc.
Without any memory management, things get hard fast, especially considering you still need to parse the memory layout to find out which parts of memory are even usable.
That's heap vs. heap. It comes after the very brief stage you're mentioning.
I tend to think that the driver and network level, which is where most problems in the real world tend to lie, is similar to userland in how often unsafe code gets used and the impact of unsafe code on the correctness of the kernel. In fact, the fact that you can write some drivers in userland on certain OS's--and they can crash without bringing down the whole OS--is strong evidence of this.
You can write some device drivers in userspace. But you can't handle page faults in userspace. Those need to be handled by the kernel and will be 100% unsafe because any mistake will lead to a certain and quick double or triple fault.
Kernel Level Programming is very very different to normal user-level programming. There is no malloc. There is no segmentation. Nothing prevents you from dereferencing a null pointer. In fact, in kernel-land a null pointer is valid memory. You don't have any of the forgiving properties of Ring3 assembly code in kernel-land.
Once you have VMM and Paging setup, preventing a null pointer deref is not hard (simply unpage 0x0)
You don't have to go all that way to be better than C or C++. Rust can give you a submodule A whose correctness doesn't impact the safety of any uses of submodule B.
That's true, but the property I described can still help you track down the root cause and limit the number of places that interact with it.