ironic to post this right after java's biggest vuln to date (log4shell).
I think parent was pointing out that the biggest and costliest security exploit ever found had nothing to do with buffer overflows, memory management, etc.
Seems pretty clear to me - Heartbleed (and all the other serious memory exploits) required a great deal of skill and a lot of luck to exploit, and in return you either don't get a remote execution, or you get a very tiny chance of a remote execution.
In comparison, log4j is about as easy to exploit into an RCE as it is to use curl.
Log4j is a guaranteed remote-execution exploit just by filling in a user facing form with the correct URL, while memory exploits are not guaranteed to result in an RCE, requires more skill than simply typing into an input box or an email.
This is only true of systems that were using very outdated JVM versions... on the newer ones (we're talking 2016 or newer JDK releases, not like last month), you would need to pull off a serialization exploit to indirectly get RCE, which is quite a bit harder than sending a HTTP request.
Heartbleed wasn't about RCE at all. It was about memory disclosure -- memory that contained secret signing keys. The fallout was that keys needed to be revoked and rotated.
Reading out memory and extracting the secret keys was actually pretty simple. There were multiple POCs available.
https://queue.acm.org/detail.cfm?id=3212479
"The root cause of the Spectre and Meltdown vulnerabilities was that processor architects were trying to build not just fast processors, but fast processors that expose the same abstract machine as a PDP-11. This is essential because it allows C programmers to continue in the belief that their language is close to the underlying hardware."
Let's assume it's true: your argument would be "intel made a mistake, but since they would have only made that mistake when doing stuff that appeases C programmers (would they have?), it's actually because of C."
Now, I think this is a bit of a stretch.
ETA: or did you mean that it has to do with C for that reason? in which case, ok, I see how you mean.
This article is pretty funny actually:
> On a modern high-end core, the register rename engine is one of the largest consumers of die area and power. To make matters worse, it cannot be turned off or power gated while any instructions are running
Yea, let's just gate off the RAT. What's it for again?
Of course it makes sense to configure outbound white list, but there's no infrastructure in RHEL or Ubuntu and nobody's going to bother with custom scripts for that.
https://www.zdnet.com/article/microsoft-70-percent-of-all-se...
Another big chunk of bugs, including forgetting to escape strings, can often be reduced by building strongly-typed APIs that distinguish between "String", "Sql" and "Html" types.
Java actually does quite well by these metrics. It's memory safe, tries to eliminate undefined behaviors, and it has an adequate type system. However, the mere existence of runtime code-loading is a risk, as we saw with Log4j.
I write this tongue in cheek, obviously. I've seen dire, dire security bugs in Java in particular but also in terms of fitting parts together (lo, broken ACLs, useless AWS SGs, inter-process assumptions that don't hold, injection vulnerabilities of myriad types, etc.). The truth is, we are doomed, and not just because of the 70%.
Unless Microsoft claims to have identified all bugs. That would be a bold claim indeed.
If that is what your C code looks like, then yeah, you should probably not be writing C code.
But at the same time, C remains, and will probably always remain, the easiest language on which to tune and optimize. It's not going anywhere. Our grandkids will still be using systems with C firmware at their core.
No it doesn't. There is actually no logical way to infer from the parent statement that language choice doesn't matter for security.
> But in practice they work well enough to push C's foibles down into the noise floor.
This goes against findings from research that has been done into the sources of security vulnerabilities. Microsoft and the chrome dev teams have published that 70% of their security bugs are a result of memory safety.
If C is used in 100 years it will only be because of inertia. Today there are better choices in the domain of low level systems programming languages.
I'm not saying you're wrong, but there's effectively zero real kernel work done in anything other than c and c++. Tons of well-known open source Rust/microkernel stuff exists here in public, but behind closed doors is where almost all firmware work is happening. When someone at Microsoft, Apple, Qualcomm, Samsung, etc sits down to code firmware for billions of devices, it happens in c. I've never seen a serious proposal to switch to managed code at any of my jobs, either
I think we'll see more and more complex stuff move out of the kernel, but I don't think c is going the way of COBOL in the next 20 years at least. I'M definitely not going to start using Rust on my own, and it would take a pretty compelling case from management or a junior engineer to make me switch in the future.
Many of those companies are now in C++, actually.
Is this true? It seems implausible to me.
I think it just means that these languages all have elevated potential for security issues, but there are languages without pervasive gratuitous dynamism or memory problems.
> Static analysis tools and runtime hardening techniques don't "fix" C, exactly. But in practice they work well enough to push C's foibles down into the noise floor.
But that’s all additional effort to integrate these tools and practices onto a language which already has a very low iteration velocity (all of the time spent debugging memory issues, package issues, build system issues, etc which simply don’t exist in many modern languages).
> But at the same time, C remains, and will probably always remain, the easiest language on which to tune and optimize. It's not going anywhere. Our grandkids will still be using systems with C firmware at their core.
This sounds like a concession to me. Of course C will smolder on in obscure, legacy firmware long after it becomes obscure—so did COBOL, but we don’t pretend COBOL’s vestigial existence is owed to its merits rather than a quirk of history.
This is a fine and normal thing. C did it’s job for a time, but languages aren’t emerging which are better suited to modern computing requirements. This process will continue and these languages which are chipping away at C’s market share will be eroded themselves eventually.
Um. COBOL survived so long specifically because it did some things better than alternatives, mostly around how it handled numbers. Yes, also inertia and historical accident, but also because it was actually good at its job.
It's arguably a symptom of a larger problem the ecosystem's sheer size.
For Python, JavaScript, Ruby, etc it's not even a question.
The flaw was in log4j's use of the library which allowed remotely connecting and executing code by default based on connection strings which were possibly user-entered.
It was expensive, simply because that library is extremely popular.
On Github I see people rewriting simple UNIX utilties, e.g, cat, in some "safe" language, e.g., Rust. Yes, it is possible there could be some exploit based on an error in cat, maybe triggered by some malicious input that the user fails to detect, but this would not be a concern to the same degreee as errors in large, complex applications with network and other privileged access that are relatively new. For example, using C to write cat versus using C to write git.
Exposure to security vulnerabilities depends not only on the language used, but also when, where and how the application is used.
A big reason is that they are usually multi-threaded, while the C counterparts are not.
Writing (memory)safe and maintainable parallel code is much, much easier in Rust than in C.
The other thing mentioned in the post was eliminating overhead. My limited experience with Rust leads me to believe it is closer to C++ than C in this respect. That is to say there is a lot of higher level functionality that you can use and it is optimized for performance in the general case. On the other hand, that functionality is still for the general case, so a competent developer who has a thorough knowledge of what the program is trying to accomplish will be able to write more performant code.
(Is Fortran still relevant? Try compiling Tensorflow, to say nothing of serious numerical modeling stuff. And yes, I know about __restrict; it's more of a band-aid.)
Ha, I have some disgusting use of COMMON blocks from the 80's to show you. But that doesn't affect the optimizer's ability to assume that there's no aliasing, so you're totally correct about that.
So does Rust, in unsafe blocks. Is there anything that can be done in C that can’t be done in unsafe Rust?
Google even sponsored the work to clean the Linux kernel from them.
Duff's device <https://en.wikipedia.org/wiki/Duff's_device>.
I don't think they are.
> You can write Rust code with the same observable behavior as Duff's device in C.
Yes, turing complete languages can do that.
Why anyone would want that on purpose, I don't know.
In this particular case it's the `Sync` trait and the generell rules around sharing data.
I was specifically referring to the things you can do in `unsafe` blocks.
This program prints "12". The only difficulty is that Rust's designers have intentionally made this pattern difficult by marking raw pointers as !Send, but you can get around that by wrapping them in a Send type.
use std::{time::Duration, ops::{DerefMut, Deref}};
struct SendWrapper<T>(*mut T);
impl<T> Deref for SendWrapper<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
unsafe { self.0.as_ref() }.unwrap()
}
}
impl<T> DerefMut for SendWrapper<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { self.0.as_mut() }.unwrap()
}
}
unsafe impl<T> Send for SendWrapper<T> {}
fn main() {
let mut x = 10;
let p1: *mut usize = &mut x;
let p2: *mut usize = &mut x;
let mut s1 = SendWrapper(p1);
let mut s2 = SendWrapper(p2);
let j1 = std::thread::spawn(move || {
*s1.deref_mut() += 1;
});
let j2 = std::thread::spawn(move || {
std::thread::sleep(Duration::from_secs(1));
*s2.deref_mut() += 1;
});
j1.join().unwrap();
j2.join().unwrap();
println!("x: {}", x);
}I have some thoughts on how Rust performance compares with C!
I spent yesterday optimizing a sort routine in Rust. I need to sort records where the key length is known at compile-time, and the payload length is different every time sort is called. I expect the average sort call to involve at least 250 million rows and over 400 columns. Speed matters.
Here's what I've learned so far, about how Rust versus C question:
- Rust has generics with monomorphization. This makes it easy to compile two different versions of the sort routine to run on different length keys.
- The very fastest version of my recursive base case, a 20-item insertion sort, currently uses unsafe code. This allows me to eliminate a couple of bounds checks that LLVM isn't eliminating automatically. I may figure out how to do this with safe code before shipping. (In 5+ years of production Rust, I've never needed unsafe for performance. This may be the first time!)
- Performance often comes down to cache locality. Both C and Rust give me the fine-grained memory layout control that's essential. Also, this means that "detached key" sorts are almost always a bad choice, even when the alternative is repeatly moving large record payloads around.
- The Rust "criterion" benchmarking library makes it super-easy to run valid performance tests.
- The Rust "proptest" allows me to generate large amounts of test data and verify properties like "reckless_sort always produces output matching std sort."
- "cargo fuzz" will be useful when I try to break the finished code. Seriously, it's a super-nice fuzzer workflow.
- Rust's standard quicksort contains all sorts of funky optimizations: It detects semi-sorted arrays, it breaks patterns, it falls back to merge sort when things go wrong. It even marks "cold" paths for LLVM to improve instruction cache usage.
- If I get my sort working, the "rayon" library will make it utterly painless and safe to split up the recursive calls over multiple CPUs.
Overall, this has been a pretty fun experience. I'm not sure whether the finished product will use unsafe Rust in the inner loop. But the combination of generics, rayon, and tightly integrated test/bench/fuzz tooling has made this a very pleasant experience.
Verdict: I am entirely happy using Rust for hot loops. (Even if my current fastest inner loop does use "unsafe" to bypass a few bounds checks I'm not clever enough to eliminate otherwise.)
The log4j vulnerabilities and others like it where input validation specific to what you expect would not happen in properly paranoid systems.
It requires quite a lot of qualification from the development team, though.
Assuming a higher level language would not allow that without introducing any other security problems, and assuming the CVEs roughly represent the secureness of the software, then using a higher level language would be a pretty small marginal improvement in security.
Git seems to have an excellent security track record: https://www.cvedetails.com/vulnerability-list/vendor_id-4008... However, I think the right claim here is, "be very sure you have the chops to benefit from C's speed without compromising security."