So compiler optimizers get to take advantage of UB for that last mile optimization, which in a language like C always expect the developer to be a ISO C expert, and when they aren't or are too tired trying to meet project deadlines, surprises happen.
"Oh, it was quite a while ago. I kind of stopped when C came out. That was a big blow. We were making so much good progress on optimizations and transformations. We were getting rid of just one nice problem after another. When C came out, at one of the SIGPLAN compiler conferences, there was a debate between Steve Johnson from Bell Labs, who was supporting C, and one of our people, Bill Harrison, who was working on a project that I had at that time supporting automatic optimization...The nubbin of the debate was Steve's defense of not having to build optimizers anymore because the programmer would take care of it. That it was really a programmer's issue.... Seibel: Do you think C is a reasonable language if they had restricted its use to operating-system kernels? Allen: Oh, yeah. That would have been fine. And, in fact, you need to have something like that, something where experts can really fine-tune without big bottlenecks because those are key problems to solve. By 1960, we had a long list of amazing languages: Lisp, APL, Fortran, COBOL, Algol 60. These are higher-level than C. We have seriously regressed, since C developed. C has destroyed our ability to advance the state of the art in automatic optimization, automatic parallelization, automatic mapping of a high-level language to the machine. This is one of the reasons compilers are ... basically not taught much anymore in the colleges and universities."
-- Fran Allen interview, Excerpted from: Peter Seibel. Coders at Work: Reflections on the Craft of Programming
This can be the case now, but then later someone adds new code that you did need that null check.
> If you think you do need them then it's a sign you've screwed up somewhere and should fix it!
Hm, so instead of
> Wouldn't it be better to have compile errors if the compiler figures out that it can remove a null check?
you wanted the opposite: warn when a null check is there and was actually required :).
Not sure if this is a working solution either. Maybe if it was behind a macro, JUST_CHECKING_IF_NULL(x)..
I mean obviously the solution is to use a sane language but you know...
Rust would certainly not help in this instance, because nobody is writing pty handling code in Rust. I.e., using Rust in place B does not help with bugs in place A. Any expectation that Linux would get more secure if some new code were Rust is optimistic to the point of fantasy.
The best possible outcome of allowing Rust in kernel code is that the kernel would not become even more insecure as a result of the added Rust code. That would be good, by itself, even if not what we really want. But whether even that would be achieved in practice is still to be demonstrated.
But I am waiting for the Rust-OS to complete -- if one is under construction now. We can check how that stands when released.
Citation needed, since all evidence points to the contrary.
Could you please point us to a Rust application (there are hundreds of thousands at this point) that gets noticeably faster when disabling bound checks?
In servo, a whole web browser written in Rust, the cost of doing this was negligible, to the point that it was barely measurable (1-3%, noise levels for such a big app).
Same for Firefox which has a substantial amount of Rust.
Go ahead and give Fuchsia a try. You can enable bound checks for a substantial part of Android's user space and not really notice it.
Same for Redox, or any operating system kernel written in Rust.
You have many large applications to choose from, so please, just point us to 1 for which this is the case.
---
Compared with other mitigations already in the kernel, that can cost you up to 50% perf, and that people seem to be ok with, bound checking all the array accesses seems like a no brainer, given that ~70% of CVE are caused by memory issues.
When most people think about bound checking all array accesses, they think, for some "i can only think inside the box" reason, that this happens on hardware, for every memory access.
But that is not how Rust works. Rust adds bound checks "in Rust", and the Rust compiler and LLVM are really good at removing duplicates, hoisting many bound checks out of a loop into a single bound check at the beginning of the loop, etc.
People also think that this is an all or nothing approach, but Rust allows you to manually access without bound checks and do the hoisting manually. So if you find a function in which this makes a big difference, you can just fix the performance issue there manually.
So, in general, the idiomatic Rust "twiddle all these doodads" compiles to the same machine code as the idiomatic C++ for that problem, even though Rust bounds checked it and C++ didn't care. Lots of Rust checks are like this, they compile away to nothing, so long as what you did is necessarily correct. The Option<NonZeroU64> stuff a few days ago is another example. Same machine code as a C++ long integer using zero as a signal value, but with type safety.
Naturally this only kind of works when everyone on the team goes safety first.
Doing some C style coding will just bork it, similarly to any unsafe block or FFI call in other better suited languages.
But in the subject of making juice with lemons, is way better than plain C.
This is, after all, why Godbolt was first invented as I suspect you know (Matt Godbolt wondered if C++ iterators really do produce the same machine code as a hand-rolled C-style for loop, and rather than just trust an expert he built the earliest Compiler Explorer to show that yes, with real C++ code you get the same machine code, any time spent hand-rolling such loops is time wasted)
By the way, this should be a nice update about the state of affairs on Android (I am yet to watch it).
"Improving Memory Safety in Android 12 Using MTE"
Memory tagging (which is what MTE is about) reminds me of ASLR and password entropy requirements. They're slightly raising the bar which is not something I have much time for. I prefer to put the effort in to solve problems permanently so I can worry about something else instead. Whether that's a practical opportunity here is unclear though, and I think Rust is a big part of finding out.
The bounds check wasn't being elided either. I checked and it was there in the assembly, so I figured that the function is so hot that an unchecked access might help things. Apparently not. The only thing I can think of is that the reduction in code-size for that function had an unintended effect elsewhere, either for the optimizer or that it resulted in a hot bit of code crossing a cache line?
The very few cases where it actually mattered for project delivery acceptance, were proven with a profiler, and fixed on those single cases only.
Most of the time it is just cargo cult taken from C into other languages.
Multics had a better DoD security profile assessment than UNIX, thanks to PL/I being bounds checked by default.
Mac OS used Object Pascal until they decided to switch to C++ around 1992, it also hardly impacted their sales, using a language with bounds checking.
I have not found this, at least in application code. There is usually at most a few percent different between v[i] and v.at(i) (the latter checks bounds) with C++ std::vector, for example. So I almost always use .at() these days, and it does catch bugs.
https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_macros...
And before STL was a thing, all the custom types I had were bounds checked by default.
spin_lock_irq(&tty->ctrl_lock);
put_pid(real_tty->pgrp);
real_tty->pgrp = get_pid(pgrp);
spin_unlock_irq(&tty->ctrl_lock);
rustifying this would be let mut tty_lock = tty.ctrl_lock();
put_pid(real_tty);
real_tty.pgrp = get_pid(pgrp);
std::mem::drop(tty_lock);
Which would give an error that you are not allowed to mutate real_tty.pgrp.Rust could prevent this issue by requiring that all non-exclusive accesses to the shared data acquire the mutex (and if you use Mutex<T> which wraps the data, you'll always acquire the right mutex). The & vs. &mut system can model exclusive access ("initialization/destruction functions that have exclusive access to the entire object and can access members without locking"). It doesn't help with RCU vs. refcounted references, or "non-RCU members are exclusively owned by thread performing teardown" or "RCU callback pending, non-RCU members are uninitialized" or "exclusive access to RCU-protected members granted to thread performing teardown, other members are uninitialized". And Rust is worse than C at working with uninitialized memory (Rust not using the same type for initialized vs. uninitialized memory/references is a feature, but uninitialized memory references are too awkward IMO).
my takeaway was essentially that you get sweet perf wins from semantics that are hard to replicate with a type system that's also making really strong guarantees without making the code SUPER gross.