> any safety checks put into the competing language will have a runtime cost, which often is unacceptable.
And what is the runtime cost of all the mitigations put in place because we don't use a memory safe language? Stack canaries, safe stacks, ASLR, control flow integrity, code pointer integrity, runtime attestation, library re-linking and randomization. Not to mention sandboxing techniques and other system level mitigations.
I suppose I should thank the C language for job security as a security engineer.
And who says that faster runtime trumps all other considerations? I would much rather have my computer run slower than have to continuously deal with security vulnerabilities. My time is much more valuable than CPU time.
> I would much rather have my computer run slower than have to continuously deal with security vulnerabilities. My time is much more valuable than CPU time.
To be devil’s advocate and take the other side of this argument: I would rather the CPU I paid for spend its cycles performing actual application logic. Every cycle spent executing all this “safety code” overhead feels like me having to pay for a developer’s sloppiness.
I feel end users pay a high cost for all this runtime checking and all these frameworks and levels of abstraction.
First, not all language safety features have to manifest themselves as run-time checks. A properly designed language will allow many safety checks to be done at compile-time.
And second, would you really rather deal with security holes on an on-going basis?
The problem with C is not that you can write unsafe code in it, it is that the design of the language -- and pointer aliasing in particular -- makes it impossible to perform even basic sanity checks at compile time [EDIT: or even at run-time for that matter]. For example:
int x[10];
...
int y = x[11];
In any sane language, that would be a trivially-checkable compile-time error. But in C it is not because there are all kinds of things that could legally happen where the ellipses are that would make this code not violate any of C's safety constraints. That is the problem with C, and it is a fundamental problem with C's design, that it conflates pointers and arrays. C was designed for a totally different world, and it is broken beyond repair.
I'm not here to defend C, but to point out a problem that many advocates make when they cherry pick examples of how their language is both safer and more performant than C. Simply put, that example is irrelevant when the size of the array is not known at compile time. The moment that you have a dynamically allocated array, you are either dropping safety (e.g. expecting the developer to perform bounds checks when necessary) or performance (e.g. the compiler inserts bounds checks at runtime).
It is also worth considering that there is nothing preventing C compilers from inserting compile time checks to address examples such as yours. I just tried to compile a similar example in gcc and it does catch the unsafe code with warnings and the optimizer turned on.
> The moment that you have a dynamically allocated array, you are either dropping safety (e.g. expecting the developer to perform bounds checks when necessary) or performance (e.g. the compiler inserts bounds checks at runtime).
Yes, that's true. So? The original claim is that it is self-evident that this tradeoff should be resolved in favor of performance in the design of the language, and it just isn't (self-evident). If anything, it is self-evident to me that the tradeoff should be resolved in favor of safety in today's world.
There are patterns, though, that can help. C compilers are capable of recognising and optimising many forms of iteration, but being able to tell the compiler explicitly that you're iterating over a collection gives you that much more confidence that the compiler will do the right thing.
Especially when to get the compiler to do the right thing safely you need to add manual bounds checking with the expectation that the compiler will optimise it away, but without any mechanism to ensure that it actually happens.
It depends greatly on the problem at hand, but there are definitely cases where even with a dynamic array size we can unroll our loop such that we check bounds less than once per iteration.
Bad example. There's nothing[0] you could put in the ellipsis to make that code valid, and both gcc and clang will warn about it (clang on defaults, gcc with -Wall).
[0] Ok, I guess you could #define x something, but that's not interesting from a static analysis perspective.
Warning is one thing, but crashing is better. That's possible to do in a C compiler too of course, because in this example the array hasn't decayed to a pointer and its size can be recovered.
The issue is when you pass the array to another function, it can't track the size without changing the ABI.
If the choice is between a compile time warning and a runtime crash, I will take the warning every single time: much closer to the actual error. You're probably asking for a compile time error instead.
Indeed the `-Werror` option is often a good thing to have (though I don't set it by default on my free software projects, because other people might use other compilers with different warnings, and I don't want to block them outright).
-Werror is an interesting case -- it's an example of a key difference between C and Rust.
Rust's compiler will reject programs unless it can prove them to be valid. C compilers will accept programs unless they can prove them to be invalid. But then C warnings can lead to an indeterminate state: code that looks iffy may be rejected, but we've not necessarily proven that the code is wrong. We're still trusting the programmers' claim that code which may exhibit undefined behaviour with certain inputs won't ever receive those inputs.
I meant a crash. Obviously both at once is best, but you can detect the possibility of the crash at compile time (disassemble your program and see the bounds check), and it turns a possible security issue into a predictable crash so that’s safer.
I don’t really love forcing errors; when a program is “under construction” you should be able to act like it is and not have to clean up all the incomplete parts. It also annoys people testing new compilers against your code.
> Indeed the `-Werror` option is often a good thing to have (though I don't set it by default on my free software projects, because other people might use other compilers with different warnings, and I don't want to block them outright).
Another problem with C. There's way too much implementation-dependent behavior.
Not with Monocypher. That project has one implementation defined behaviour (right shifts of negative integers), and it's one where all platforms all behave exactly the same (they propagate the sign bit). In over 5 years, I haven't got a single report of a platform behaving differently (which would result in public key crypto not working at all).
However I do get spurious warnings, such as mixing arithmetic and bitwise operations even in cases where that's intended. Pleasing every compiler is not trivial.
What do you suppose you could put in the ellipsis that would make your second statement defined behavior other than preprocesor stuff or creating a new scope with a different variable named x? (Neither of which would frustrate a compiler wanting to give you a warning)
The real overhead is not safety code but inefficient design and unnecessary (and often detrimental to the user) features. The added 10% of safety checks is nothing compared to orders of magnitude of bloat modern software has. Moreover, a lot of C codebases have the same safety checks manually implemented into them, sacrificing performance in order to have less bugs. And when it comes to designing a modern C alternative, it's not just a performance/safety tradeoff, the are parts of C that are simply bad, like null-terminated strings, and are long due to be replaced with something better.
I'm suspicious that people arguing that safety checks making things slow are basing that on the very stale now idea that CPU's work sequentially. Which isn't true for super scalar machines at all. And also forgetting compilers will optimize away a lot of them.
There is an asymmetrical risk here. On one hand the compile might emit checks that aren't needed. On the other that the programmer will fail to insert a check that is needed.
Both yours and parent arguments are strong and reasonable. Compared with other overhead that developers deliberately add to their programs, language safety checks are probably a drop in the bucket. We're in a world where developers think it's reasonable to run a chat app on top of an entire browser framework on top of the platform SDK on top of the OS. The runtime performance of checking an array's bounds are the least of our concerns.
>I would rather the CPU I paid for spend its cycles performing actual application logic.
The problem is that for CPU programming errors are application logic too and they are run, cf. protected memory. You don't run everything in ring0 do you?
That's not the point. The point the parent is making is that yes, languages that include safety features are slower, but you will actually need those safety features in C as well, in the code. So your program will also be slower in C.
There is something backwards about how safety checks are done in C.
First it's up to the programmer to put them in.
Then hopefully the compiler will optimize away the unneeded ones. If the programmer though biffs and forgets then oops. And of course classically we blame the programmer not the system he's been forced to work under.
That seems like a reasonable thing in 1982. Which is 40 years ago.
It would be better if the compiler implemented the checks automagically and removed ones it knows it doesn't need. And bonus, if the programmer puts one in, leave it alone.
Yes, I think we're actually all in agreement here. The common point is that TFA's argument that we need to keep C because it's faster is invalid. The GP's point is that it is not in fact faster because of all the things you need to do to produce code that can actually be deployed in today's world. My point is that even if C were faster (which it actually isn't) that would not matter because C is unsafe even with all the extra stuff that people stick onto it to try to make it safe. So the original claim is really a judgement call that speed matters more than safety, and this is not something about which there is any kind of consensus.
What does slower actually mean? Slower in the face of safety, while computers continue to get faster and or cheaper (depending on how you spend your transistors/cost) is a fools paradise. Don't be baited into their flawed logic.
If your transistor/cost curve has a doubling time of 3 years, 156 weeks. A 5% difference is approximately 11 weeks.
If your transistor/cost curve has a doubling time of 2 years, a 5% difference is approximately 8 weeks.
How fast do we have to get before safety is table stakes? Focusing on raw unsafe speed wouldn't be a normalized metric in any other industry. I'll spend 8-11 weeks of performance gains on correctness.
If you remove the mechanically preventable bugs from being a consideration, by definition the only one you now need to focus on are the ones NOT prevented by mechanism.
How is this not a win? We only have so many decisions we can make per day.
So, let's take the most extreme example of pursuing safety, WUFFS.
Unlike general purpose languages WUFFS has a very specific purpose (Wrangling Untrusted File Formats Safely) so it doesn't need to worry that it can't solve some of your problems, which frees it to completely refuse to do stuff that's unsafe, while going real, real fast.
WUFFS gets to completely omit bounds checks, which you probably wouldn't have dared try in C because it's so obviously unsafe, but WUFFS already proved at compile time that it can't ever have bounds misses so it needn't do these checks. WUFFS gets to also omit integer overflow checks, because again it proved at compile time that your code is correct and cannot overflow for any input. And since WUFFS knows how big the data is at all times it gets to automatically generate loop unrolling and suchlike accelerations.
WUFFS isn't a general purpose language, it doesn't have strings, it doesn't have growable arrays (what C++ calls "vectors"), it doesn't have any dynamic memory allocation, but then we weren't talking about how much you love general purpose features, we were talking about safety preventing security bugs. Which is exactly what WUFFS does.
I can't respond to that unless you are more specific about what are "the ones that lead to really bad outcomes". There are a lot of arbitrary-code-execution attacks that are enabled by buffer overflows, and IMHO that's as bad as it gets. There is absolutely no legitimate excuse for a buffer overflow in today's world. Switching to safe(r) language won't prevent all attacks, but it will make a big dent.
It defeats some extremely important classes of exploits. And I'm not sure how they're not ones that lead to really bad outcomes since they lead to fun ones such as arbitrary code execution all the time. I can create a C program with hideous vulnerabilities in about five minutes without doing anything that isn't totally standard and normal (albeit obviously vulnerable so technically buggy). I'd have to actually look up how to make my code vulnerable in languages with more safety features.
And the latest trend, producing C Machines with hardware memory tagging, because none of those mitigations are actually working preventing all those CVE to take place.
You are the security engineer, so you certainly know better than me, but aren't those runtime mitigations aimed at malicious programs? Which is to say, even if a better-C was written that didn't allow people to write a program that would bump into those mitigations, the bad guys could still write their programs in assembly or C or whatever, right?
But the presence of a better language to write innocent programs in wouldn't protect the innocent programs from malicious programs written in C and assembly...
Think about web browsers. We're not really that worried about people running malicious web browsers. It's potentially a problem, but as long as people know not to run software that some random spammer sends them in an email then it's easy to avoid.
On the other hand what computer security people worry about a lot is that the web browsers made by reputable organizations and teams of competent programmers nevertheless contain security flaws that can be exploited by a maliciously-written website to cause those browsers to do unexpected and dangerous things.
Many of those security flaws in otherwise well-regarded software are due to memory management errors that just aren't present in safer languages, or they're due to type errors that wouldn't be present in more type-safe languages.
There are some implementation bugs that could be present in any language no matter how many safety features it has, but many security bugs aren't due to, say, an incorrectly specified algorithm, they're due to the programmer asking the computer to do something that's literally nonsense, like asking for the fourth element of a list that only has three elements, or recording that someone's age is apricot. Programming languages with powerful nonsense filters can remove a lot of those kinds of security bugs. (And powerful type systems often give programmers mechanisms to tell the compiler more about the program so that it can filter out more kinds of nonsense than it would otherwise.)
I don't understand this response. Nothing really stops that, regardless of implementation language. It's why we have an entire bodged and mostly ineffective AV industry, as well as a slightly less bodged and partially effective endpoint monitoring/detection industry.
Runtime mitigations exist to mitigate some of the latent risk associated with programming in unsafe programming languages. We use them because they're our best known approach to continuing to use those languages without letting script kiddies own us like it's 1993.
You don't seem to understand that those malicious programs have no way of running on someone's computer if they can't exploit some other program to get installed on the machine in the first place. If the system software on the target machine is written in a better language and has no exploits, then it doesn't matter what language the attacker uses for their software.
It would prevent or at least mitigate some classes of exploitation. Buffer overflows are very common attack vectors: https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=buffer+over... (386 results, 11 from prior years, so 375 from this year). 23 of those are in kernels, many leading to privilege escalation.
Yes, a better language would protect innocent programs. Take stack canaries. They are to protect stack corruption due to an application bug, e.g. unsafe input handling. Input handling is perfectly safe in many other languages, but C has a lot of footguns.
The language the innocent programs are written in can remove attack vectors that the malicious programs use; I don’t think it matters what the malicious programs are written in?
Sandboxing techniques can also be used for dealing with malicious programs, but most of those things listed are for programs to use to protect themselves, not to stop them from intentionally doing bad things.
I agree. I think of address space context switching overhead as the performance price we pay for not being able to run all our programs in a single address space, which we could safely do if we knew all the programs were emitted by a trusted compiler that disallows unsafe memory access. Imagine if system calls were just ordinary functions that can be called with no more than the normal function call overhead? What if they could even be inlined?
Obviously there's a lot of little details you'd have to work out. Like how to make such a trusted compiler in the first place, and how to sandbox unsafe code and legacy applications that were compiled by an untrusted compiler.
If this seems like it's far-fetched or too much work, consider what lengths high-performance hardware devices like HPC network interfaces go to avoid system calls at all costs, to the point where applications talk to the hardware directly. Is that really a sustainable practice long term? And how can anyone audit the security of such hardware devices?
This is what Microsoft Research's Singularity OS did, with the language being C#. Their argument was that the MMU's address space isolation was a 30% Unsafe Code Tax so even if C# was slower than C if they could get the slowdown to less than that it was still an overall performance win.
I'm pretty sure the discovery of Meltdown/Spectre and similar speculative execution attacks would completely wreck this model. The fix for those exploits has been to make the isolation barriers even stronger but if you don't have them at all you're wide open. If you had such an OS but then had to split it back into separate address spaces you've now lost the performance gains and just have a slower OS that is harder to develop drivers for.
Yeah, speculation attacks are a big problem. It seems like maybe if you're running a specific compiler you might be able to avoid speculation attacks by not emitting dangerous sequences of instructions, but I don't know what the state of the art is when it comes to Spectre mitigations and whether it's possible to have a compiler that can formally verify that a program is immune to any (known) speculation attacks.
> I think of address space context switching overhead as the performance price we pay for not being able to run all our programs in a single address space, which we could safely do if we knew all the programs were emitted by a trusted compiler that disallows unsafe memory access. Imagine if system calls were just ordinary functions that can be called with no more than the normal function call overhead?
That's pretty much how the Amiga worked, and that level of technology achievement is still unsurpassed today.
You already see this in PaaS and serverless for managed runtimes, I don't care if my Java and .NET code runs on bare metal, micro-kernel, unikernel, or whatever.
Rust will not replace anything because it’s impossible to write code in it. Everything you write is a syntax error that requires an exobrain to figure out.
GP has been posting the same thing in many threads when Rust comes up. Apparently they think it's funny, or they're really bad at writing Rust code and feel like venting.
And what is the runtime cost of all the mitigations put in place because we don't use a memory safe language? Stack canaries, safe stacks, ASLR, control flow integrity, code pointer integrity, runtime attestation, library re-linking and randomization. Not to mention sandboxing techniques and other system level mitigations.
I suppose I should thank the C language for job security as a security engineer.