40 years ago, the case could be made. But C and C++ are not new languages, and the fact that just barely shy of no one can demonstrate the existence of secure C or C++ code bases without staggering levels of effort put into that process is data now, not just anecdote.
(And let me emphasize the effort as my yardstick. Writing truly secure code is arguably something nobody has ever done at scale in any language... but C and C++ are certainly unique in the sheer level of effort it takes poured in to them to even match what a number of other languages come with out of the box, let alone exceed them. If you aren't using some very high quality and fairly expensive tools like Coverity on a routine basis, you aren't even close.)
"Many years later we asked our customers whether they wished us to provide an option to switch off these checks in the interests of efficiency on production runs. Unanimously, they urged us not to--they already knew how frequently subscript errors occur on production runs where failure to detect them could be disastrous. I note with fear and horror that even in 1980, language designers and users have not learned this lesson. In any respectable branch of engineering, failure to observe such elementary precautions would have long been against the law."
While Intel's MPX extensions were a failure, Solaris makes use of ADI on SPARC, iOS uses PAC, Google is researching adopting ARM MTE into Android, ARM sponsors CHERI with Morello project, Azure Sphere has Plutonium, and so forth.
However these are all pontual efforts, and their large scale effects will take years or decades to start making a difference.
Agreed. That is why C and C++ should be replaced with more reliable alternatives.
Security is the process. It contains continuous risk assessment, penetration testing, fuzzing and using various other tools throughout the product development to eliminate attack vectors. Only then you could build a secured product. Just rewriting everything in Rust won't make it.
I sometimes work on the infosec side of the house. It's easy to point at vulnerabilities due to endless memory access problems in C code and fixate on that. And it's true, so it feels satisfying.
Just rewrite everything in not-C and this is fixed! And that's true as well. But remeber here the word "this" refers to the memory access bugs. It doesn't refer to vulnerabilities in that sentence.
Plenty of systems without a single line of C/C++ code and we have no shortage of ways to break in anyway. So in one sense, everything changed. But wearing the black hat, nothing really changed since I compromised the system anyway. A new language without all the engineering process parent post describes, won't magically get there.
For an existing system "just rewrite everything" will guarantee way more bugs for years to come simply because the old system has been battle-tested and reinforced for years. In the long haul the rewrite will converge to a better state but that haul is long (and assumes budget will remain in place long enough, which might not happen so you end up with a half-baked rewrite).
For new systems starting from scratch that may in any way ever be run in a security relevant context, sure, starting with not-C is a good idea today.
Unfortunately Rust seems to be the only alternative for use cases where C was actually needed (if it could be written in Java or Go or Python or ... then it didn't really need to be in C in the first place). And sadly rust is fairly user-hostile language so my guess is plenty of new projects will start with C for a long time due to lack of friendly alternatives. And tooling.
Could you elaborate on that? I started using Rust at the end of last year, and have found it to be one of the most user-friendly languages I've learned. There is clear documentation that is easy to generate with the standard toolchain, along with a very good language guide.
The compiler error messages are an absolute joy as somebody coming from C++. Pointing out exactly where the borrow checker found errors, or exactly what the type signature should be for a trait, is really useful.
For example, in the case of const generics, I've used the equivalent in C++ to make a class for a geometric vector in N dimensions. The size varies depending on the number of elements, but is known and can be checked at compile time. The existence of const generics is very closely matched to this particular use case.
Forcing it down to an overly simplistic setting, if I need to add 5 to a number I can increment 5 times, or add 2 and then 3, or just add 5. Which of those options should we remove?
Perhaps we mean, instead, that there should be one best way, but (sliding into metaphor, I hope) what about when we have a language without 5 and we are considering adding it? Is the use case already handled by 2+3? How are we to decide?
Language wise rust is better the python in every way except for compile times.
Language-wise, it feels like they are intended to solve different problems, so Python vs Rust is a weird comparison. Granted, that may because I started learning Rust as a replacement for C++, so that has been my point of comparison.
Rust seems to me to be user-frienndly, though for the cases where it's safety features are critical and at the fore, it front-loads pain that would be dealt with eventually, which can make it feel intimidating. But its forcing you to confront things that are likely to cause subtle bugs if dealt with sloppy (or overlooked), not actually adding unnecessary complexity.
Rewriting C or C++ applications in Rust won't fix all security problems. But it would be forward progress. It's just like wearing seat belts won't stop people from being injured in car crashes. You could say that "safety isn't a product but a process," which is fine, but if your safety testing process finds that wearing seatbelts reduces injuries substantially it seems pretty obvious that using a seatbelt product is the way forward. At least until someone invents a better replacement for seat belts, or supplements them with other features like airbags, automatic braking systems, self-driving, etc...
True, but if the process discovers that a lot of lives are lost because of the use of unsafe tools, it changes the tools to add safety features (https://en.wikipedia.org/wiki/Chainsaw_safety_features), or ditches them altogether (https://en.wikipedia.org/wiki/Hazard_substitution#Processes_...)
So, yes “rewrite in rust” isn’t the full answer, but that does not imply “don’t write it in C” isn’t part of the answer.
The simple classes of bugs enforced by rust, are also caught fairly quickly with any kind of memory sanitizer (valgrind?), combined with static analysis tools (coverty?), and automated code quality standards (misra). Run a CI/Code coverage monitor while looking for for these kinds of errors, and I would bet the results are actually better than plain rust due to the maturity of some of these tools.
About valgrind or sanitizers: they're runtime, so just like tests they can only show the presence of errors, not their absence. Like dynamic type checking.
IMHO, Rust simply isn't good enough at catching all types of bugs to justify rewrites at this point, and its likely when you look at some of the work being done at the processor manufacturing companies that they don't believe it either.
Consider: https://en.wikichip.org/wiki/arm/mte, https://en.wikipedia.org/wiki/Intel_MPX, and https://lwn.net/Articles/718888/
There are quite a number of these in the pipeline, which make some of what rust does redundant.
Valgrind might not catch 100% of errors, but at least what it catches are actual errors I care about.
If you're writing safety-critical software though, being forced to restructure your code to satisfy the type checker (which, in this case, is kind of a simple proof assistant) seems like a sane tradeoff.
Yes, but do the programming languages / tools we choose make the security process easier?
There is no perfect solution, but some programming languages / tools are better than others for preventing unexpected behavior that can lead to insecure programs.
(Caveat: Obviously 'unsafe' is relevant to this discussion, but at least you only have very small areas of code you need to check extra carefully.)
https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
UndefinedBehaviorSanitizer (UBSan) is a fast undefined behavior detector. UBSan modifies the program at compile-time to catch various kinds of undefined behavior during program execution, for example:
- Using misaligned or null pointer
- Signed integer overflow
- Conversion to, from, or between floating-point types which would overflow the destination
GCC has similar features.
Which makes it unusable with binary libraries.
Anecdotally, I haven't seen an open source C++ code base that is uses type safety to that extent but it is certainly possible.
The lack of formal verification features and easy-to-stray-into logic errors in Rust, Zig and Ada make it easy to create code that is difficult to fully grasp how it will behave, especially when large projects are concerned. The Rust, Zig and Ada code runs really fast and is usually memory safe but there are hidden dangers lurking.
I don't doubt that Rust, Zig and Ada will become more popular in time to come, but formally verifiable languages such as Verifiable C or Spark are actually "safe" in some meaningful sense of the term instead of giving everyone a false sense of "safety" in the form of memory safety features as if memory safety errors were somehow the only class of security critical error.
Anything can be secure (and conversely anything can be insecure). The theoretical potential doesn't matter because real life is never the theoretical best case. What matters is the overall risk (is liklihood * how bad < benefit?)
Exactly; real life C and C++ code that does real work tends to be insecure.
So you can easily make it secure by doing something not easy?
Something that requires discipline, willingness and unconventional practice to be secure is by definition not secure. A secure language is the opposite: The default behavior and conventions are mostly secure and you need to go out of your way to make it unsecure.
Otherwise, with enough time and enough people you are guaranteed to be doomed.
Is the kernel Linux not a real-world use case?
Now show us the list of replacement kernels written in Rust, Ada, Lisp, or whatever that are:
1) free software (or open source as a second best)
2) as widely distributed and as portable as Linux
3) supports as much hardware as Linux
4) supports the wide range of applications that Linux does
5) free of memory safety related CVE's as well
There is always a series of tradeoffs - Linux has adopted a classic but unsafe programming language as its basis which has in turn allowed it to cover an enormous amount of ground in a (relatively) short time frame, support massive amounts of new employment and accelerate the development of the web.
It is not clear that an alternative design or implementation in another (safer) language would have moved the world as far forward as Linux has in the same time frame, or whether its even possible, as nobody has demonstrated this.
Apparently they are still stuck at the starting line, discussing endlessly how perfect their system will be when they finally choose the right language to implement it in.
As a result, anyone working on Redox, Fuschia, or whatever popular contender now have a massive library of driver source code to refer to when working in their languages of choice. This is not a resource that the Linux and 386BSD communities had when starting out.
Rust isn't as standardized or at critical mass yet, as C was when Linux was launched. The language still lacks comparative mindshare, no matter how popular it is here on HN. That's why there aren't enough developers.
Do you honestly believe that there is a widely-deployed battle-tested free software operating system implemented in a memory safe language?
I assumed the answer was "no" from your original comment, but all of your replies can only be interpreted as if the answer is "yes"
I never said "Linux is bad and insecure" either, and when making blanket assertions like that, it's helpful to have a point of reference.
So I ask again, which free software, widely deployed, battle tested and highly secure operating system are you comparing Linux to? That was the very obvious point of my comment, which you have apparently missed (ignored?) twice now.
I think this is a good question, and I was wondering if someone would ask that. To be more specific, I think points (1), (3) and (4) are of primary importance, so I would address those specifically:
1) commercial, proprietary and expensive systems were very fragmented and incompatible with each other, especially UNIX, and VMS, Windows etc. Nobody seems to want to go back to that. I think this is the biggest hurdle for any new commercial system to overcome, otherwise Linux (or other, better free systems) will continue to dominate until a better replacement comes along. Look what happened to Solaris. Better than Linux in some respects, worse in some, but it failed to retain its market share. Windows is not hugely used as a server platform either like it was in the early 2000's outside the infrastructure required to support Active Directory, Exchange and Sharepoint.
3) obviously popular server hardware needs to be supported by the system, eg. HP, Cisco UCS, DELL etc. This should be evident by the amount of effort that vendors like Redhat put into patching their kernel to ensure compatibility with hardware from major server vendors. If the proposed alternatives don't support these hardware platforms then they are going to find it hard to gain traction.
4) you aren't going to migrate people away en-masse from a C-based system like Linux without being able to support the major languages and ecosystems that rely on C and/or C++ for their runtimes. People will still need to run their Tomcat applications, Ruby on Rails, Python, NodeJS etc. all of which currently depend on C or C++ to some extent. I think at least Redox is addressing this somewhat with their "relibc" implementation, which would allow "legacy" C codebases to be hosted there in the future, but let's be pragmatic and admit that it's probably a long way off.
As somebody who builds/manages emergency services communications platform based on Linux that supports over 70,000 front line emergency services workers, who in turn provide criticial services to over 6 million people, those are the reasons why I see no currently viable platform worth migrating to in the short to medium term _for our particular application_. In addition to self-hosted environments like ours, Linux currently also has a very particular grip within cloud hosted environments as well (which aren't particularly relevant to me at this point in time).
In order for non-C based platforms to take off, you either need to support the C runtimes, or be prepared to wait a long time as other languages that don't depend on a C runtime become popular and the ecosystem grows enough.
There were no free software memory-safe languages suitable for implementing an operating system in during the 90s when a lot of the initial Linux development happened.
> In order for non-C based platforms to take off, you either need to support the C runtimes, or be prepared to wait a long time as other languages that don't depend on a C runtime become popular and the ecosystem grows enough.
C runtimes are everywhere at least partly because C runtimes are easy to implement. Most of the Lisp machines in the 80s had full support for C. And those were non-memory protected systems! Running a C program in a separate a process would let you use any existing C runtime; the C runtime doesn't care what language the system-calls are implemented in.
It is already starting with MIT/BSD/Apache OSes for IoT.
Even if I indulge your fantasy for a moment - Linux (amongst other systems) has provided myself and many others with a solid career for the better part of the last 25 years, daily security patches from vendors and all, and I don't see any solid evidence of that changing before I retire.
Further I suggest most Linux people would be able to adapt if the world were to move on to an improved competitor with enough momentum to be adopted en masse - which hasn't happened.
The security implications of undefined behavior are poorly understood by software developers: https://arc.aiaa.org/doi/pdf/10.2514/1.I010699
No, undefined behaviour is a serious problem, especially regarding security.
There's a long history of serious security problems in C and C++ codebases due to unintended invocation of undefined behaviour. These issues continue to arise even in well-resourced C/C++ projects with highly skilled developers, such as the Linux kernel and Chromium.
I admit I don't have hard numbers to hand, and sadly it's rather rare to see decent rigorous comparisons, but I don't think C and C++ have that much of a performance advantage over, say, Ada or Safe Rust.
My inner pedant feels it's necessary to note that Ada is still an unsafe language, but still, it's much less unsafe than C. It also has excellent support for optionally disabling all sorts of runtime safety checks, whereas the design of C and C++ make it extremely difficult to implement such checks as 'opt-in' features in a compiler.
One example is that undefined signed overflow is important when the compiler might need to rearrange a loop index - PPC prefers to count down, x86 doesn't care.
Anyway, I think undefined behavior + ubsan is better than defining all behavior. If something's undefined you know it's a bug every time you see it. If it's defined, how do you know it's wrong?
> One example is that undefined signed overflow is important when the compiler might need to rearrange a loop index - PPC prefers to count down, x86 doesn't care.
Signed overflow is undefined behaviour regardless of the target hardware architecture. The compiler is permitted to assume the absence of signed overflow and to optimise accordingly. Unless the compiler documentation specifically says its ok, you still have an undefined behaviour problem. It might happen to work fine, sure, but if you're serious about writing correct programs you should be aiming to deliver a program which is correct-by-definition rather than correct-by-coincidence.
> I think undefined behavior + ubsan is better than defining all behavior
It isn't. That's why Rust makes such a big deal of its Safe Rust subset. It's also a major selling point of SPARK Ada for safety-critical software. It's tremendously valuable to be able to categorically close the door on a whole family of potentially serious and difficult to detect bugs.
> If something's undefined you know it's a bug every time you see it.
No, you absolutely don't.
I already mentioned that high-profile projects like Chromium and the Linux kernel continue to face security vulnerabilities arising from unintended invocation of undefined behaviour. Section 7 of the paper discusses undefined behaviour but doesn't really explore its full consequences, so instead I suggest reading [0] and [1].
Undefined behaviour means exactly that: if undefined behaviour has been invoked at runtime, the behaviour of the program is not constrained by the C/C++ standard. The program is not required to explode loudly, it can do anything. It isn't required to behave the same way each time. Hopefully it will explode loudly, but it's possible everything will seem to be fine. In the worst case the undefined behaviour leads to a serious safety issue or security vulnerability.
Undefined behaviour is even permitted to 'time travel'. [0][2]
> If it's defined, how do you know it's wrong?
You use exceptions or some other well-defined means of detecting and handling runtime errors. For example, Java's NullPointerException and Ada's Constraint_Error.
[0] https://blog.regehr.org/archives/213
[1] https://blog.llvm.org/2011/05/what-every-c-programmer-should...
[2] https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=63...
That's what I said. If your program is correct (it may-overflow like everything does, but doesn't dynamically overflow), then by undefining overflow, you can tell the compiler that it doesn't happen. That lets it reorder operations in a way it couldn't if every + potentially wrapped around.
> No, you absolutely don't.
You seem to have said "no" and then agreed with me. I was proposing trapping on all undefined behavior in debug mode!
> You use exceptions or some other well-defined means of detecting and handling runtime errors. For example, Java's NullPointerException and Ada's Constraint_Error.
Trapping is plausible, but that's not always how people want to fix undefined behavior. For instance some people want undefined memory reads to return 0, or want overflow to wrap. In that case it's hard to distinguish errors from intentional behavior.
I don't like exceptions very much either because control flow gets more complicated. Trapping like Swift does is fine, though.
I don't follow the distinction here. A correct program should never invoke signed overflow, regardless of input.
> by undefining overflow, you can tell the compiler that it doesn't happen
Right, that's essentially the effect of the standard saying it's undefined behaviour: it should never happen when the code runs.
> That lets it reorder operations in a way it couldn't if every + potentially wrapped around.
Right, or more generally, it enables various compiler optimisations.
> I was proposing trapping on all undefined behavior in debug mode!
Ok, I thought that by If something's undefined you know it's a bug every time you see it you were saying that UB always results in a loud explosion.
Unfortunately it's not easy to build a C compiler that traps whenever UB is encountered at runtime. An example: the compiler can't know the size of an array passed to your library. C uses 'thin pointers', unlike most languages where, whenever you pass an array, the callee can inspect the array's length.
> Trapping is plausible, but that's not always how people want to fix undefined behavior.
Ada's solution, of raising exceptions (roughly like Java), seems sensible. Of course, part of C's appeal is that it's very compact and lacks things like exceptions.
> some people want undefined memory reads to return 0
That doesn't sound reasonable. To implement that could be pretty burdensome.
> or want overflow to wrap.
This is something some compilers support as a non-standard feature. GCC supports it with the -fwrapv flag. I suppose it would be friendlier if there were a standard and portable #pragma to tell the compiler what you want, but I'm not sure it's a big enough problem to make it into the standard.
You can 'fake it' pretty well by converting to a unsigned integer type, doing the arithmetic, and then converting back to the original signed integer type. You could write a function to do this. You could use the preprocessor to defer to a compiler-specific intrinsic where one is available. I think GCC's __builtin_add_overflow would do the job but its definition isn't terribly explicit regarding wrapping behaviour.
I think this code would do the job portably, and I don't think it relies on anything platform specific. (I'm relying on the signed/unsigned conversions using two's-complement, I believe this is guaranteed by the C/C++ language specs. I've also used fixed-length integer types for good measure.) Godbolt tells me GCC can optimise it down to a single LEA instruction on AMD64.
#include <cstdint>
using std::int32_t;
using std::uint32_t;
/*inline*/ int32_t wrapping_add_int32t(int32_t num1, int32_t num2)
{
return (int32_t)((uint32_t)num1 + (uint32_t)num2); // Compiles down to LEA instruction
// Alternatively (also compiles down to an LEA instruction)
// int32_t ret;
// __builtin_add_overflow(num1, num2, &ret);
// return ret;
}
See also [0].> I don't like exceptions very much either because control flow gets more complicated. Trapping like Swift does is fine, though.
I agree it introduces action at a distance flow-control. I'm afraid I don't know Swift.
[0] https://stackoverflow.com/q/59307930/
Vaguely related fun: https://github.com/MaxBarraclough/IntegerAbsoluteDifferenceC...
The whole problem with undefined behavior is that it is faster to not check for undefined behavior and calling exit(1) (exit(0) would be a successful exit). Think about it in slightly higher terms: you implement a linked list that can search for an item and return a pointer to it once it finds it. Your implementation explicitly says that if you search for an item that isn’t in the list you will hit an infinite loop. I disregard the warning and let it search for an item not in the list. I hit an infinite loop. Could you have added a check for “if (current == head)” and bail then returning NULL? Sure you could but that introduces a branch and slows things down. Better label what can happen as undefined behavior because maybe on some future processor you’ll have that check because it’s cheap but on x86 it isn’t so you don’t. This is essentially the same thing.
In C's defence, you can easily get this behaviour by wrapping malloc in a safe_malloc function. Given that C lacks exceptions, it makes good sense to handle unable-to-allocate by returning NULL as this leaves the door open to all possible strategies.
This is wrong on two points.
Firstly, real world compilers very often do handle various kinds of undefined behaviour with immediate termination. On many platforms, dereferencing a null pointer will result in a segfault. Sometimes compilers generate code to trap if undefined behaviour would result. In the C++ standard, some errors are defined to result in a call to std::terminate, rather than undefined behaviour. [0]
Secondly, as IgorPartola indicates, doing this isn't irresponsible, it's the least bad way to handle undefined behaviour. If your loop has overrun the end of your array, you generally don't want the execution to silently proceed with invalid data, you want execution to end immediately.
Like I said, undefined behaviour is a problem even for Chromium and the Linux kernel. We've seen that just be diligent isn't a solution. We're way past that now.
> Every time you see these problems happening, the reason is that developers not only wrote incorrect (undefined) code, but also didn't test the code path that was affected.
Technically true, but not insightful. Testing can never be exhaustive, so this observation doesn't light the way to a solution.
> While I agree that a compiler should do a better job, ultimately the responsibility is on programmers
This statement is true of the C language, but it isn't a solution, it's the problem-statement.
It's not true of a language like Safe Rust, where the language itself closes the door on undefined behaviour, and where it is not the programmer's responsibility to avoid invoking undefined behaviour.
The major compilers already have ways to test for undefined behavior such as -fsanitize=undefined. Projects need to use these flags and test more.
Undefined behaviour is responsible for a non-trivial fraction of the security vulnerabilities of C and C++ codebases. Undefined behaviour in an application can be entirely eliminated by writing the application in a safe language. That's the point.
> The major compilers already have ways to test for undefined behavior such as -fsanitize=undefined. Projects need to use these flags and test more.
Do you really think the Chromium team isn't aware of that flag in GCC and Clang? If there were an easy fix to the problem of accidental invocation of undefined behaviour, the problem would have gone away years ago.
It's useful for a C/C++ compiler to offer to add runtime checks for a subset of the possible causes of undefined behaviour. We agree more projects should use such tools. As we're seeing, though, this isn't a silver bullet. Even extremely well-resourced and security-sensitive codebases end up with UB issues.
Memory safety problems are still a sizable proportion of the CVEs associated with C and C++ programs.
> The major compilers already have ways to test for undefined behavior such as -fsanitize=undefined. Projects need to use these flags and test more.
This is NOT a solution -- you cannot test every case. In fact, not even close: The possible state space to for signed addition of two 64-bit ints is 2^128. That is infeasible. A compiler for a 'safe' language CAN prove the absence of certain behaviors (UB being one of them).
It would be interesting to make a following experiment: * find and patch UBs in multiple opensource projects which are relatively popular (at least used not only by an author) * send pull requests wich UB fixes and see which fraction will be closed as "won't fix".
It requires a lot of time, but may show that many developers don't care about UB.
These points aren't wrong, but it understates how pernicious undefined behaviour can be. As I stated earlier, even well-resourced high-profile codebases like Chromium and the Linux kernel play cat-and-mouse with security issues originating in undefined behaviour. It's not just a matter of hiring diligent coders and using the usual quality-control processes (testing, code reviews, static analysis).
It's possible for C/C++ code to be entirely free of undefined behaviour, but in practice this only seems to happen when formal methods are used, or when people use code-generators/transpilers to avoid writing C/C++ by hand.
https://blog.regehr.org/archives/213 gives great insights into how undefined behavior in C and C++ can be difficult to reason about and cause problems.
The lack of bulletproof memory safety and easy-to-stray-into undefined behavior of C and C++ make it easy to create code that is difficult to fully grasp how it will behave, especially when optimizing compilers are used. The C / C++ code runs really fast, but there are hidden dangers lurking.
I don't doubt that C and C++ will be with us for a long time to come, but the growing use of Rust, Zig, Ada, and others show that better alternatives exist and that they will replace the use of C and C++ for many domains and use cases.
Edit: Downvotes? Did you read my whole comment? I am saying that C / C++ are not secure for real-world use cases.