> Rewriting SQLite in Rust, or some other trendy “safe” language, would not help. In fact it might hurt.
(see link for expansion on that matter, which is a question of tooling and testing)
At some point some popular compiler is going to make a subtle but important change to some undefined behavior that's not going to be immediately obvious as to it's repercussions, and the fallout will be massive. It boggles my mind the mental contortions people will go though to justify what is essentially an argument of "it hasn't caused a problem yet" while ignoring that it's caused many problems already, just not that they've noticed or that have affected them.
I don't think the argument is that these cases should be ignored -- they are now corrected, after all. It is that most of these cases should be treated as low priority compared to issues that are creating observable problems.
> Perhaps it's safe to say that today we don't need to address these issues on this project in this language, but that's taking on technical debt and setting ourselves up for work in the future (that may be difficult to identify at that time).
Yes, that is succinctly saying something I was just implying. Even if a current C program is verified as having absolutely no problems with any current compiler due to the way it is using undefined behavior and the compilers interpret it, it's impossible to assume it will remain in that state by the nature of the problem being examined. Undefined behavior is undefined, and thus may change. Now, any language may decide to change how something works, so every program has to deal with this at some level, but again by the nature of this problem, there's much less assurance that the problem won't be a small,subtle change, possibly in one compiler, which is missed until it's widespread.
This would be much less of an issue if there was a specific subset of C which defined most the undefined behavior which could be turned on with a flag. It would probably prevent portability in some respect, but I hope we've finally reached a place where portability is accepted as secondary to security.
In this specific case with sqlite it certainly is more of a fixed-compiler-warning. However, you could imagine some undefined behavior in SSL implementation to require emergency-patch level as well. I agree with you that undefined behavior "in most cases should be treated as low priority".
That is, I'm not arguing that as much undefined behavior as possible should be made defined and possible, just that it's defined. That definition may very well be "you are not allowed to do this. Don't do this."
Some of this stuff can be caught at compile time. Like code that does signed addition of positive values and checks if they might be less.
Wait until you realize that the length of a byte in C is not clearly defined. Someday a processor will come along where a byte is 6 bits, and the fallout will be massive. (Really, it's happened before).
No, actually that processor would not become popular because on one would use it.
The reality is unless you are using a formally-defined language like ML, you are relying on undefined behavior in your language.
> The reality is unless you are using a formally-defined language like ML, you are relying on undefined behavior in your language.
This is not true, not using the definition of "undefined behavior" provided in the C standard.
POSIX specifically requires it to be exactly 8.
There's languages that have a few undefined corners, and then there's C. A sufficiently large difference in quantity becomes a difference in quality. C is qualitatively worse than most modern languages with its undefined behavior. (Granted, some modern languages escape by having the one implementation, which is then the definition. But still, that's less undefined than C.)
For example you could say that division by zero will either give a result or trap, but that it can't do anything else, and the code path cannot be ignored. Or that an uninitialized variable is equivalent to initializing it with a semi-random number.
Even if an out of bounds array access will cause untold chaos, you can at least specify that it will cause that chaos at X bytes past the base of the array.
Merely creating an invalid pointer would be, on architectures where it doesn't trap, 100% harmless.
And for crying out loud, is forgetting to terminate a string literal with a " still undefined? There are so many bits of undefined behavior that are easy to remove.
This proposal destroys lots of dead code elimination optimizations that are very important in code post inlining (for example, in the STL).
http://llvm.org/docs/LangRef.html#undefined-values
There's also the issue that having a stable value forces the register allocator to keep a live range for the undefined variable, which can cause unnecessary spills or remat in other places. Especially on 32-bit x86, this can be a problem.
Even specifying that they have a new arbitrary value on each access would be a big improvement over the status quo. It wouldn't allow nasal demons. Since LLVM seems to already have these semantics, it makes a good argument that it wouldn't hurt C's performance to tighten the spec at least that much.
But I'm not seeing how C or C++ code gets you in a situation where you're purposefully doing arithmetic or bitwise operations on uninitialized variables. If it almost never happens, it doesn't need to optimize particularly well. What parts of the STL can be faster by treating uninitialized variables as impossible?
> There's also the issue that having a stable value forces the register allocator to keep a live range for the undefined variable, which can cause unnecessary spills or remat in other places.
This would only happen when the variable is accessed multiple times. In which case having to keep the value safe is no worse than if it actually had been initialized. I don't see how this is a problem.
What I'm mostly thinking of is allowing unused branches to be pruned. The STL tends to get inlined really heavily, which results in a whole pile of IR being emitted for what look like very simple operations. Based on the actual parameters and state, the optimizer then wants to prune out as much dead code as possible to reduce i-cache footprint, and sometimes time as well.
Take small string optimization. That optimization requires a branch on length in almost every string operation. But if you have a std::string of constant size, you don't need the heap spilled code to be emitted at all. Usually the only way to work this out is inlining + constprop + DCE. That's where undefined value semantics are really helpful: a branch on an undefined value can be completely removed as undefined behavior, which can make its branch targets unreachable, and allow them to be removed, and so on recursively. Undefined values allow entire CFG subtrees to be eliminated in one fell swoop, which is an extremely powerful technique for reducing code size.
I don't have a precise example off the top of my head as to where this kicks in in the STL, but I strongly suspect it does.
Even if you can remove one of the branches because you know if the string is small, the logic of "this branch can't happen" -> undefined -> delete sounds more complex than "this branch can't happen" -> delete.
I was skeptical about all this about six months ago myself, but the continuous stream of articles on this topic, plus the spectacular and highly educational failure of Friendly C (and not just that it failed, but why it failed, which is why I posted that exact link) has satisfied me. It is also part of why I've stepped up my own anti-C rhetoric since I've been so convinced... as bad as I thought C was, it really is, no sarcasm, not merely "hating", even worse than I thought. I am honestly scared to use it professionally and pretty much refuse to touch it without good static analysis support.
Edit: Is it figuring out that the pointer in the string object is uninitialized? That doesn't seem any easier than reasoning about the length value, and I don't see how it would lead to "br undef".
Teach jump threading some more simple tricks:
1) have it fold "br undef", which does occur with
surprising frequency as jump threading iterates.
...
Chris didn't cite any specific numbers in the log, but I believe him when he says it actually happens. You should be able to run "opt -O2 -debug-only=jump-threading" and see where it does :)Related, with some actual numbers to prove it helps, LLVM commit #138618:
SimplifyCFG: If we have a PHI node that can evaluate to NULL and do a load or
store to the address returned by the PHI node then we can consider this
incoming value as dead and remove the edge pointing there, unless there are
instructions that can affect control flow executed in between.
In theory this could be extended to other instructions, eg. division by zero,
but it's likely that it will "miscompile" some code because people depend on
div by zero not trapping. NULL pointer dereference usually leads to a crash so
we should be on the safe side.
This shrinks the size of a Release clang by 16k on x86_64.Lots of LLVM users won't. The competition between LLVM and GCC is (or was) pretty brutal.
> Plus in most or all of those cases, the optimization would still be possible even with stable uninitialized values. It just needs to be done in a different way.
I doubt that's possible without justification. How? Are you familiar with all of the passes of LLVM, how they interact, and with the code patterns generated by the STL?
I trust Chris in that he didn't add the jump threading optimization for no reason, which is the main one that matters here. If he says that this occurs with "surprising frequency", sorry, but I'm going to trust him. Compiler developers are usually right about the impact of their optimizations. Submit a patch to LLVM if you like to remove it, but I highly doubt it'll go through. If it did go through, Rust (and perhaps Swift) would probably revert it, as we ensure that we don't emit UB in the front end, so losing the optimization hurts us for no reason.
It's not that I think the Chris is wrong, it's that I think other optimizations related to dead code have gotten better in the last many years, and focused effort could improve them more.
My position, by the way, is hardly uncommon among longtime C compiler developers.
What do we do about the fact that undefined behavior actually makes it harder or impossible to write efficient code in some cases, like checking for integer overflow?
Even if your goal is performance over anything, there are a whole lot of undefined behaviors that have absolutely zero performance benefit.
This is a good example of why we should be moving away from C. :) Signed overflow being undefined is basically necessary due to a self-inflicted wound from 1978: the fact that "int" is the easiest thing to type for the loop index when iterating over arrays. Nobody is going to go through and fix all the C code in existence to use unsigned, so we're stuck with that UB forever. The realistic solution is to start migrating away from C (and I have no illusions about how long that will take, but we'll never get there if we don't start now).
> Even if your goal is performance over anything, there are a whole lot of undefined behaviors that have absolutely zero performance benefit.
Sure. But compilers don't exploit those in practice (because compiler authors are not language lawyers for fun), so they're basically harmless in practice. They're essentially just spec bugs for the committee to fix in the next revision.
C should be writable by humans.
This entire discussion is predicated on the idea that undef values having a live range is something that actually prevents bugs, which I find extremely dubious. This is not like the "null dereference is UB" thing in the Linux kernel (which I also found to be overstated in importance, but it has caused at least one problem). I can't think of a single bug that this has caused, other than a compiler bug in Rust that was only exposed by people intentionally trying to write code that would break--that is, the bug had no practical effect. It strikes me as "friendly C lawyering" for little benefit.
Then smack the spec writers for never removing undefined behavior. You don't have to smack both, but the two groups are working together to make C more dangerous every day.
>They are doing this because their customers demand it.
If you show a chart of how often the customer's code will stop working because of these optimizations, perhaps they will stop demanding it. But even if it's implemented, there should be a --reduce-undefined-optimization flag.
> I can't think of a single bug that this has caused
If it was defined, then the Debian RNG bug likely would not have happened.
If an undefined behavior is rare, that's a good reason to flatly remove it from the spec. If we could have 10 undefined behaviors instead of 200, the world would be better off. There are undefined behaviors you can hit during preprocessing, for crying out loud.
(Also even in the most extreme version, you wouldn't need to count it as live until the first access, and if that's the last access then there's no live range at all.)
No. The Debian bug was not the result of a compiler optimization based on undefined behavior. It was due to OpenSSL doing something dodgy [1]. The compilers, being conservative, all declined to do any optimizations based on the undefined behavior. It was Valgrind that called OpenSSL out for the uninitialized use of memory. This is not a bug in Valgrind either! If the C standard defined live ranges for uninitialized values, then Valgrind would still be right to flag uses of uninitialized values, because they usually indicate bugs, and Valgrind is a bug finding tool. The Debian bug occurred because OpenSSL was doing something silly, Valgrind was doing its job properly pointing out that silly thing that OpenSSL was doing, and the fix was dangerously incorrect.
If it had been valid by the C standard, it is not unlikely that they would have kept it. Then there would have been no disaster. They would not have gotten confused about what was valid and what wasn't.
Extra entropy to mix in isn't that silly.
Edit: In other words, making "Is this code valid?" more complex to answer can have bad effects even in the absence of aggressive compilers.
Once again, if the C standard were to change to allow uninitialized variables to have live ranges, Valgrind would not just "git rm -rf memcheck; git commit". That is because Valgrind is a tool that is designed to check for uninitialized memory. It did its job properly. It is not designed to check for undefined behavior according to the C standard.
You seem to be determined to lay the blame for every problem at some combination of C compiler authors and C spec writers, regardless of whether the problem actually is theirs.
I don't blame all problems on C, but I think it should be easier to write valid C. Dealing with uninitialized variables was just an example of something that could be changed. It wasn't supposed to be the biggest most horrible problem with C.
Compilers may not turn functions that hit most kinds of UB into { return; }, but they can. As a general category, that's a bad thing.
It's highly unlikely that the maintainer of OpenSSL at Debian is a spec lawyer. The maintainer ran Valgrind because, as a general principle, reading undefined memory is bad. That is independent of whether the C standard says that it's OK. If I were the maintainer of the package in question, I would not ignore reads of undefined memory just because the C committee has declared that it has some kind of behavior. That would be silly.
Valgrind contains many tools that check for things that are not violations of the C standard: for example, memcheck checks for "fishy" arguments to malloc that are not C standard violations but are nevertheless usually bugs.
> Compilers may not turn functions that hit most kinds of UB into { return; }, but they can. As a general category, that's a bad thing.
Depends on the undefined behavior. For undefined behavior coming from the C preprocessor, sure, that shouldn't be undefined, and the committee should change the spec. For other kinds of UB, if compiler authors hadn't figured out those tricks, we'd have slower programs today. Undefined behavior is much of what has allowed performance of our programs to keep improving despite the language not changing over decades.
This is approximately what LLVM describes undef as, and it indeed leads to nasal demons. This description enables a single value to both pass a bounds check, and then subsequently go out of bounds!
(Of course, you could define it to work, but I would suspect that would make this sort of value much less useful.)
The CompCert C verified compiler is a verified compiler which supports "almost all of the ISO C90 / ANSI C language" - http://compcert.inria.fr/ .
It isn't C. It is a formally-defined language that is very similar to C. It is otherwise nothing like ML.
This discussion seems to miss a couple things. SQLite is embedded quite often, by programs written in C; would embedding a rust library and possibly runtime fix things? The parent program would still potentially have defects and those defects could impact SQLite.
SQLite is also rather mature, I do t see how you can compare a rewrite to nature code. Take OpenSSL as an example, they are t tossing it, they are fixing it, it's a much more shallow lift to fix it.
I'm all for some big rust programs to prove its case though, a mailer, a dns daemon, some sort of database. Something useful cut from whole cloth, and ideally something we have historically not done well. I don't think a rewrite is it though.
Aliasing rules[1]? Compilers apparently don't agree on that now, so even if it were to coalesce into the same undefined behavior, one would have to change.
> This discussion seems to miss a couple things. SQLite is embedded quite often, by programs written in C; would embedding a rust library and possibly runtime fix things?
I'm not advocating for Rust as much as I'm advocating against C, or at least against C as it currently exists and is implemented with so much undefined behavior. I've argued elsewhere in this discussion that a special subset of C with as much undefined behavior as possible specifically defined, which could be enabled through a flag, would do wonders (even if at the expense of some portability).
> Take OpenSSL as an example, they are t tossing it, they are fixing it, it's a much more shallow lift to fix it.
It's much more shallow to review and patch it. Let's not kid ourselves that it will be fixed when they are done with it. There will likely be bugs regardless of the language used to implement a crypto library. Does that mean we should ignore when one language allows an entire class of bugs that another does not, especially when it's for a Crypto library?
Let me make my case another way. What if C nevercaught on as the dominant language, or C was defined originally in a more strict manner, and most the utilities and tools we take for granted were instead implemented in a language that had less undefined portions, allowed less undefined behavior, and thus had less bugs and security problems? Would the resultant small performance hit (due to being unable to optimize around the undefined behavior) outweigh the added stability and security, or would we have been better off? I think we would have been better off, and I think since C's still widely in use, it's not too late to make that case.
Testing "every single instruction" does not guarantee that all C-level UB has been eliminated, because some bugs can be input-dependent. For example, even if your coverage tells you that this function has been tested, it could still trigger undefined behavior for other inputs that trigger overflow:
int f(int x) { return x * 2; }If the inputs here are coming from something external to the program, such that the compiler can't know the value of x then the machine code should be faithful to the language statements, it's a bug if overflow occurs which may have other runtime implications, but the compiler isn't going to remove some chunk of code, etc. because of it.
From their testing page SQLite say they also test boundary conditions (I don't know to what granularity that is though) which which may catch something like this, which is agreeing that instruction level coverage isn't enough. They also run their tests with all the various sanitisers enabled.
Isn't this the same case in Rust, the non-release builds would need to see a suitable test case for the overflow check to cause a panic.
I guess I'm asking is this example relevant to concerns about undefined behaviour and optimising compilers vs. Rust? Since the function is input value dependent in practice isn't this more like implementation defined behaviour at runtime, in that a platform will alway provide a consistent behaviour, e.g. overflow, trap, saturate, etc.?
I've followed regehr's blog for a few years, I've read a lot about Rust, but I'm mostly working in higher level dynamic languages and don't have a lot of hands on experience with C or Rust and just wondering if I'm missing some subtlety here.
Yes, all programming languages allow the programmer to write bugs. But languages very much vary in how many, and what kinds of bugs programmers write in practice. Saying "well, Rust doesn't eliminate all bugs" is attacking a straw man. If you want to argue that Rust isn't worth it, you need to convince me that C plus gcov results in fewer bugs in the important areas in practice than Rust (plus kcov [1] if you like) does. I think that's going to be pretty hard. (Especially if memory safety issues are the most important bug class you're concerned about: I think it's completely impossible for any C-based solution to compete with Rust here, regardless of how much tooling you add.)
Drawing an equivalence between undefined behavior and compiler bugs also doesn't make sense. Compiler bugs are way way less commonly encountered than undefined behavior in C. Also, they're qualitatively different: compiler bugs get fixed in new compiler versions, while UB is by design and doesn't get fixed.
[1]: https://users.rust-lang.org/t/tutorial-how-to-collect-test-c...
Yes, it's very much akin to saying "well, a seat belt won't eliminate all deaths". I would understand an argument of "I'm not prepared to rewrite the software at this time" or "I'm not familiar enough with replacement X to assess whether it's a good choice", but at some point more people will have to acknowledge C's failings with more than lip-service. That doesn't mean Rust has to be the solution/replacement, but something does.
That a programmer who has produced such high-quality and rigorously tested software as sqlite should be portrayed as either cavalier or naive about software quality is something I find profoundly mis-guided.
"Rust doesn't eliminate all bugs" is a rephrased version of "Some well-formed rust programs will generate machine code that behaves differently from what the programmer expected."
> That a programmer who has produced such high-quality and rigorously tested software as sqlite should be portrayed as either cavalier or naive about software quality is something I find profoundly mis-guided.
I don't think he's cavalier or naive about software quality! SQLite's quality speaks for itself, and he is completely correct about what you need to do to achieve the level of correctness that SQLite has. He knows a lot more than I do about the rigor needed to ensure that amount of software quality. I do think, though:
1. The amount of testing that SQLite undergoes is not economically feasible for most software.
2. Rust's testing tools would allow for the same level of code quality in the absence of evidence otherwise. (The example he cited, code coverage, is wrong, as kcov is available for Rust.)
3. There is value in static analysis above and beyond the value of testing (also vice versa), because testing only reveals bugs that manifest themselves in inputs available in the test suite. This is true regardless of the code coverage of the tests.
In other words, I think Hipp's argument would make sense if it were something along the lines of: "I couldn't test a Rust SQLite as well as I can test the existing SQLite because kcov is missing features X, Y, and Z, and the value I get from the static analysis is less than the value I get from those code coverage tools, since we've found lots of bugs from those code coverage features and haven't found as many memory safety/data race bugs". That'd be totally valid. But as a blanket statement that Rust would make things worse, it doesn't make sense to me.
I think that sounded more like "Many of the programs which are UB in C are also UB in Rust, even though not (yet) specified as such."
Well, this seems either (a) false or (b) uninteresting to me. It's false because C and Rust have different semantics, and Rust rules out lots of programs that C doesn't. It's uninteresting because if the point is that Rust has accidental undefined behavior due to compiler bugs (and it does), then that UB is so rarely hit that it doesn't matter nearly as much in practice as the UB in C.
CPUs have bugs, too. Do we consider Java an unsafe language because of rowhammer?
I know about the borrow checker, but I don't think ownership bugs is a type of bug that Mr. Hipp frequently produces. It's a program design issue -- not something you think about at every single line you write. A well-designed program does not do many ownership transfers.
As someone else noted, out-of-bounds errors are sadly a pain to debug in C. For testing code, C has at least valgrind (and certainly many less well-known tools). For production code, you might not want dynamic index checking. It would invalidate performance arguments for Rust.
I think the real nasty cases of UB in C, which frequently occur as not statically refutable, are signed arithmetic overflow and shifts. As with out-of-bounds errors, I don't think Rust has a better story here -- only better built-in tooling.
Checked operations: https://doc.rust-lang.org/std/primitive.i8.html#method.check...
Saturating: https://doc.rust-lang.org/std/primitive.i8.html#method.satur...
Wrapping: https://doc.rust-lang.org/std/primitive.i8.html#method.wrapp...
Wrapping with notification: https://doc.rust-lang.org/std/primitive.i8.html#method.overf...
These apply to shifts too and you can tag types themselves to always behave one way. Overflows at compile time are caught as errors. More info at:
https://github.com/rust-lang/rust/issues/22020
If the requested behaviour is unspecified (just `a+b`), overflows cause runtime panic (edit: or wrapping behaviour in release build)
See https://doc.rust-lang.org/1.7.0/reference.html#behavior-not-...
As far as I know most machines can't trap on signed integer overflow. Which means having runtime panic is not practical. The page you linked says "no checking by default for optimized builds".
The non-release mode panics. Release mode with debug asserts turned on panics. Release mode with forced overflow checks panics. Release mode with no special options results in the same result as a wrapping operation.
The borrow checker eliminates use-after-free. And use-after-free is one of the most common types of vulnerability exploited in practice today, if not the single most common. (For evidence, look at reports about Pwn2Own.)
Index checking is quite cheap for most programs, and LLVM is good at eliminating redundant checks. As a useful comparison in another languages, Chromium compiles with bounds checks on std::vector by default (the [] operator, not the at() method which is required to be checked.) Actually, there are a lot of issues with Rust's current machine code output that I think slow it down, but index checking isn't one of them.
In hot paths (like codecs, compression algorithms...) I'm sure you never want bounds checking. It can be optimized away for sequential loops of course, but not so easily in the case of data lookups.
His argument is that achieving the level of quality that SQLite has requires verification (in a broad sense) after the compiler, and that's what he does. If you consider the goal to be producing quality-assured binaries, then you can treat UB, compiler bugs, and many other things as falling in a similar category, which are almost certainly eliminated by an MC/DC test suite.
As you say, this isn't feasible for almost any software (as John said in the blog post, SQLite is the only program he knows of that has MC/DC testing when not required by law). But it does mean that rewriting SQLite in Rust wouldn't provide as much value as rewriting many other things where the binaries do not have such guarantees.
> If you consider the goal to be producing quality-assured binaries, then you can treat UB, compiler bugs, and many other things as falling in a similar category, which are almost certainly eliminated by an MC/DC test suite.
I don't think that they're eliminated because dynamic testing can't eliminate everything—it only finds bugs given its test inputs.
Moreover, though, I'm also skeptical of the claim that binaries are all that matter. Lots of software projects (for example, Firefox) import SQLite as source into the project instead of using the binaries. They upgrade their compilers without running the SQLite test suite to catch regressions. (Firefox might, but I'm sure lots of other projects using SQLite from source don't.)
> But it does mean that rewriting SQLite in Rust wouldn't provide as much value as rewriting many other things where the binaries do not have such guarantees.
Sure; static analysis is more useful in systems that aren't as well dynamically tested. But static analysis still has value.
I can't think of an example of UB-exploit in the compiler that wouldn't result in different branch behavior, and thus, I think, in failure of MC/DC. But I may simply be insufficiently imaginative.
John makes the point about source code in a follow-up comment, and I agree, but I also see Hipps' perspective.
What about Implementation-defined Behavior? This is (I think!) technically a subset of Undefined Behavior and it permits such things as setting values[1] to arbitrary (but well-defined!) values on various operations, such as "excessive" left shifts. What I'm saying is that a compiler is permitted to substitute IB for UB and still be conforming. So it could start to do strange things to arithmetic, etc. Does that make sense as an example of what you're thinking of?
EDIT: [1] I obviously meant memory locations... as referred to by "variables" which aren't really variables, but are really binders/aliases. But here we are.
However, a quick glance at the bcov source code leads me to believe that it only does source-line coverage, not branch coverage. So, unless my quick reading of bcov sources is mistaken, I couldn't test a Rust SQLite as well as I can test the existing SQLite because kcov/bcov is missing the ability to measure coverage of individual machine-code branch instructions, and the value I get from static analysis is much less than the value I get from branch-coverage testing tools.
That's not being pedantic, btw. The difference between source-line coverage and machine-code branch coverage is huge. The latter really is necessary.
But the argument isn't about "most software" - it's about SQLite in specific. And that's the crux of the issue. Saying that he believes it would be counterproductive to rewrite SQLite in Rust at this time is not saying that that would be true of all or most or some other programs.
I don't have a dog in this fight, but I don't see how the burden of proof is on Hipp rather than the folks proposing the change. In other words, shouldn't the "rewrite it in Rust" folks have to prove that the cost of their proposed rewrite will be justified?
Agreed, they do. But that it hasn't been proven to make things better doesn't mean it will make things worse. It just means that we don't know enough to say. The right way to answer the "would this software have fewer bugs if it were rewritten in Rust?" question requires a detailed look at what bugs the software has empirically encountered.
I figure it's one of the signs of programmer maturity, that you start to look askance at rewrites. So tempting, yet so rarely even finished let alone better.
And Firefox has a good chance to become exactly what you describe with the weird cancer analogy--in fact, the nightly builds already are.
A: Bash would be way better for SQLite, really!
B: But Bash is a terrible choice because X, Y, Z, ...
A: If you make those arguments, you have to prove them.Yes, arguments have a burden to make sense, and provide evidence...
Is it theoretically possible to rewrite Sqlite in Rust and still maintain compatibility with programs written in C?
More details:
- http://doc.rust-lang.org/book/ffi.html#calling-rust-code-fro... - http://doc.rust-lang.org/1.6.0/book/rust-inside-other-langua...
The promise of rust, which may or may not be realized is pushing some very common problems down to the compiler. All code has bugs, so the compiler probably does things wrong in some cases. As the tools mature, these will get scrubbed out, just like every other project.
That said, array bounds checking is responsible for so many problems, it seems worth it to raise the minimum for a language. Not every developer is elite. In fact, they're pretty rare. C requires you to be really smart all the time, or at least be aware of when you're not smart enough to get a chunk of code right. Looping over some bytes from a file shouldn't be that risky. rust lets me, a less than elite developer, save my few moments of brilliance for the hard part of a program, rather than having to worry about the evaluation order of foo(i++,i++);
Maybe, it'll turn out the only way to make good software in the future is to find the best 100 developers in the world, and get them to make stuff. I doubt it though, being able to leverage the other million of us to make stuff, and have some (a lot?) of confidence it'll be free from the most common C errors is valuable.
Nobody should be forced to use tools they don't like. rust is trendy, but it has some very good ideas. Trendyness isn't reason alone to dismiss its approach. blah blah rust cheerleading blah blah.
(The one exception may be, like, DJB. But djbdns/qmail are very unusual C programs in many ways.)
It's not so much that it requires smarts. It's that is requires you to be ever-vigilant and to never make any mistakes. (That's why UB and bounds overflows are so devastating to software security. Almost any slip-up by the developers can be exploited.)
Incidentally, the ever-vigilant bit is also why we really want compilers to be doing the bounds-checking (or proving that it isn't necessary). Compilers are really good at being ever-vigilant. Humans (no matter how smart)... not so much.
There goes the price of freedom. Drats.
Amen to that! If I could just add one feature to C - at least as an option - it would be array bounds checks.
I have no trouble with manual memory management - a garbage collector is nice to have, but I have not had many problems with memory leaks or dangling pointers. And the ones I had were relatively easy to locate and fix.
But array bounds violations are so easy to commit and so nasty to track down... When I still wrote C code for a living, I would have gladly sacrificed quite a bit of performance to get bounds checks on all array accesses, at least for testing and debugging...
Note that every single browser vulnerability in this year's Pwn2Own was a use after free (dangling pointer).
Plus, what a browser does is, by nature, a lot more complicated than what I worked on.
EDIT: What I am trying to say is: I do not mean to trivialize use-after-free bugs, but in my personal experience, I did not run into many, while I witnessed (and caused, I am afraid) a bunch of array bounds violations, and they sometimes made me want to cry.
You don't want to be writing your interpreters in C.
Well, technically, I think most browsers these days are written in C++, but your point remains valid.
OTOH, the "default" Python interpreter is written in C, and so is Perl (Ruby MRI, too, I think, but I am not 100% certain). I cannot recall any major security problem with those languages that originated inside the interpreter (which, of course, does not mean those did not/do not exist). Then again, a web browser is probably far messier in terms of what input is has to deal with.
That's the ticket, the browser does have a large attack surface but more importantly it's supposed to safely execute completely arbitrary and untrusted payloads. In the same category are pretty much all of the usual suspect of security issues: flash, java (applets), …
Most interpreters are only fed trusted payloads, lest the developer starts eval'ing stuff they got from god knows where, and in that case the fault is usually laid to the developer's feet rather than the interpreter's.
Realistically, you rewrite the components piece by piece in the new language. You make sure each compiles right with testing and review. You report any problems to the compiler team, who fixes them. Eventually, you're whole app is in the safer language without a lot of work. You might even swap them where safer one becomes reference code with other one there for any platforms not supported yet or too buggy. A diversity benefit as Hipp mentions.
With all of this, the program becomes immune to most memory & concurrency issues while being easier to maintain. Its undefined behavior will probably be a fraction of C's in number and severity. That's a net win.
Note: I'd have told him Ada/SPARK instead of Rust given it's been stomping C in embedded safety for a long time w/ lots of tooling for verification activities already there. Counters his compiler maturity argument, too.
That is not at all what Hipp's comment says, and the original author's response to it gives it a much more charitable reading than you seem to be.
Next he points out Rust should reduce undefined behavior occurrences vs C while eventually having some of his own. That's correct but he neglects the biggest benefit: its safety scheme preventing many flaws found in C projects by default. Leaving this out of his comment makes it Rust vs C on undefined behavior and compiler correctness only. Bad comparison given efficient, memory safety is basically Rust's main benefit.
Next, he conflates compiler bugs with undefined behavior. They're not equivalent. One is an implementation failure to be remedied. One is a design failure to probably stay indefinitely in the language and compilers. He's falsely reframing the situation to prop up an argument.
Next, he delivers the argument: that compiler bugs mean you can't rely on Rust unless you check the machine code itself. I like that he checks machine code as that's a high-assurance recommendation with proven value. He claims there's not enough tools to get the job done and a lack of compiler diversity. The first might be true and the second usually only matters if they're implementing the same spec. Otherwise, you're getting effectively different programs you can't compare directly. Plus, most GCC, LLVM, and Rust programs are performing just fine relying on one, actively-developed compiler without machine code testing.
So, he's made some bogus claims, dismissed Rust's whole benefit package, focused discussion on machine code from buggy compilers, made claims about its verification which I lack knowledge to evaluate, and ignored field evidence that his focus area is a small problem. He's trying really hard to dismiss Rust entirely at compiler level without much to show for it. Incidentally, it takes much less writing for most of us to dismiss C on grounds of language or compiler safety. Something you don't see him doing. ;)
That said, Rust community should invest in tooling for assembly/machine level verification if he was correct in saying they don't have it. That will be important for OS and embedded where developers trust compilers very little. Past that, his comment's misdirection and level of bias deserves no charitable interpretations.
Maybe Rust is just not specified for every corner case? The compiler could just do anything in such cases (e.g. what a C compiler would do -- not checking for arithmetic overflow, for example). You can then go ahead and claim it wasn't UB in Rust. But effectively it is the same, and you can't expect that Rust will specify that a compiler must check for arithmetic overflows in the future.
> Next, he delivers the argument: that compiler bugs mean you can't rely on Rust unless you check the machine code itself.
If there are more bugs in the Rust compiler than in GCC or LLVM (which I don't know, but it sounds reasonable to assume given Rust's age) then that's just a good engineer's pragmatic realism.
Only valid point he has is that a system language needs tools to produce and/or test machine code output for source equivalence. Claims Rust doesnt have that but that's outside my knowledge. I know Ada, SPARK, CompCert C, and a Java subset have methods available.
> Maybe Rust is just not specified for every corner case?
Even in the absence of a formal specification, if you demonstrate undefined behavior in safe code, it will be regarded as a high-severity bug and slated for correction. If it's a bug in the compiler, it will be patched. If it's a bug in the language itself, the language will be redefined to prevent that behavior in safe code and the implementation will be updated to reflect this. If these changes break existing code, then so be it: soundness fixes are an instance where the Rust developers reserve the right to break backwards compatibility. Rust takes UB seriously.For all the complexity of the SQL Language, and efficiency constraints SQLite needs to have, plus all the algorithms it has to implement, it still got a pretty well-defined task. It is not easy to transfer that experience to other systems. Say, your company's backend API. Completely different constraints.
It could still be the case that, had Rust existed when SQLite was being created, that it would have taken much less engineering effort. Which is the metric that matters, as given infinite manpower, you can write anything in any language.
I do agree that rewriting SQLite now, which is a very battle-tested piece of software, would probably do more harm than good. I bet people will still try, for fun if nothing else.
I agree in principal, however the timing is wrong.
Rust is a very new language (less than 6 years old), and is undeniably totally unproven. It is the latest "buzz" language, and may not be around long term... nobody knows.
What-more, a full-fledged re-write of CoreUtils in Rust is unlikely to be used by any production environment, because this new CoreUtils will also be undeniably totally unproven. This is the same growing pains LibreSSL have been experiencing, lots of gung-ho fans, but very few actual users (outside of OpenBSD/FreeBSD) - and their undertaking is arguably a lot easier, since they're just cleaning a codebase, not starting from scratch.
Average users who just consume distro's are not going to switch to a new unproven CoreUtils (even if they knew how), and distro maintainers are not going to switch until it's proven either. It will take a huge company with a huge install-base switching and testing it in production for many years before others start to feel comfortable... however this is also an enormous burden on said mega-corporation, for little-to-zero perceived benefits.
Yes, in principal, it's "safer" code, but to a mega-corp with thousands of installs, the risk is too great. New bugs, language pitfalls, behavior potentially changing, etc. Perhaps Rust dies, perhaps it's replaced with an even better alternative. It will take a LOT of time to work all this out.
Flatly, re-writing a several decade's old, matured codebase in today's flavor-of-the-week language is not a good idea. It's a waste of time and effort.
Let the languages mature more, do more systems work that doesn't involve replacing the foundation we all stand on... and maybe, in 5-10 additional years, we'll see where Rust goes.
Seems like this is a good opportunity for Mozilla to demonstrate the resilience of Rust and its ecosystem by adopting Rust as the language of choice, where possible, and deploying in-house a custom Linux system in which Rust-written components are plugged-in, as and when they are written and ready, with perhaps a full-fledged switch to Redox sometime in the future. If Rust and software written in it are pushed to their limits within Mozilla, then its not an unreasonable recommendation for it to be deployed on an even wider scale.
I don't see Mozilla doing this, really. There's no direct benefit, and there's a nebulous future benefit for Rust.
Mozilla is using Rust components in Firefox though (as well as Servo being mostly Rust). "Adopting Rust as the language of choice" seems to be happening already -- I've heard a lot of folks enthusiastic about (re)writing in Rust. This stuff takes time, though.
It's odd that so many in the computing industry are unwilling to move on from a language from 1978. We think of ourselves as one of the most fast-moving industries, but we have this odd reverence for early C and Unix that makes us stubborn and resistant to change. The fact is: we didn't know how to do some things properly in 1978. We know more about programming language design now. (Even Rob Pike would presumably agree—that's why he created Go!)
To be sure, we shouldn't just rewrite things in new languages for no reason. I think many segments of our industry are too fad driven. But to me the right thing is simple: Let's evaluate new technologies on their merits. Rust may well be worse than C! But if it is, let's figure out why, and say so explicitly.
We're fast-moving because our foundation is solid and not changing (ie. CoreUtils and gang). It's an assumption that these things "just work" with zero fuss and weirdness between systems.
We build on-top of these systems, so changing them out from underneath us all is a dramatic shift.
Perhaps Rust is the key to making these things better. I never claimed Rust is bad. I've only claimed that Rust may or may not be the right choice here, and since it's so young and unproven, we should wait before trying to re-write "all the things" in Rust. Today, Rust is a pet language... tomorrow, maybe not.
Remember, it took C many years to "catch on", and even longer to become the de facto standard for systems work. We can't rush this sort of thing... especially given the sheer magnitude of things depending on this code.
We should also be careful who actually does the re-write when the time comes. New CS grads who cannot understand the old C code and therefore feel [insert-new-hip-language-here] is better... are not the best ones to tackle this sort of endeavor. This requires deep, deep understanding of the entire package, how all the components interact, legacy behavior and the reasons behind design decisions, etc...
That isn't a definition of "fast moving" that I would use. It sounds like slow moving.
Why is innovation in the core layers of the system less legitimate than innovation in social media apps? Boeing has no problem upgrading their engines every few years for better fuel efficiency. Why can they do that, whereas can't we do the same with things like GNU coreutils?
> Remember, it took C many years to "catch on", and even longer to become the de facto standard for systems work. We can't rush this sort of thing... especially given the sheer magnitude of things depending on this code.
Actually, Bell Labs had no problems with building their entire system front to back in their new unproven language C instead of using Fortran. I'm glad they went the way they did.
Perhaps I didn't word it correctly.
When you want to write the next WhatsApp, you don't have to start from scratch. The OS has been taken care of for you, and you can expect it to "just work".
This solid foundation allows innovation to build on-top, at a rapid pace. If your foundation was constantly changing, you'd have to account for all sorts of weird intricacies, non-portable code, etc (like the old days).
A rapidly changing OS isn't really what you want for a production environment. In fact, you want it to be as constant as possible, so that you are free to do your work.
> Actually, Bell Labs had no problems with building their entire system front to back in their new unproven language C instead of using Fortran
You're right. But do remember it took a long time for it to propagate. Today, there's still systems not written in C (although they are in the minority). At the time, a lot of systems were written in pure assembler, and it took a long time to convince those guys that C was ready as a replacement for most tasks (and C changed dramatically in that time period).
I'm glad they went the way they did at the time -- but today with all these things built on top (financial markets, governments, big mega-corps, small ma n' pa shops, etc...), we need slower changes in order to keep the stability.
And they were wrong to resist C. They should have switched over sooner.
> today with all these things built on top (financial markets, governments, big mega-corps, small ma n' pa shops, etc...), we need slower changes in order to keep the stability.
Stuart Feldman, on why Makefiles insist on tabs:
"After getting myself snarled up with my first stab at Lex, I just did something simple with the pattern newline-tab. It worked, it stayed. And then a few weeks later I had a user population of about a dozen, most of them friends, and I didn't want to screw up my embedded base. The rest, sadly, is history…"
Stability is important, but eventually you do have to go back and fix things that are wrong for us to move forward.
Operating systems written in higher level languages go back all the way to the 60's, like Burroughs written in Extended Algol in 1961.
There were multiple other OSes written in variants of Algol and PL/I, before C was even an idea on its designers.
Only home micros were fully written in Assembly by the time C was getting used outside AT&T.
It is an urban myth spread by AT&T fanboys that C is the first system programming language.
Anyone that bothers to research the history of mainframes and operating systems will easily find documents of those systems, most of them written in higher level languages with more memory safe features than C has ever had.
Aircraft technology may not have been the best comparison for your argument. Have you seen http://idlewords.com/talks/web_design_first_100_years.htm
Actually we did know how to make it properly, as Extended Algol in Burroughs B5000 in 1961 was being used, just to cite one example from many others that were ignored by the UNIX authors, because they didn't want to spend too much effort designing a proper compiler.
That's the sort of thinking we'd benefit from having less. Legacy is a terrible burden.
Honestly using higher-level languages is the same mentality as taking a pill to magically lose weight. It's quick, but detrimental (to programmers ability) in long term.
Programming becomes easy but in the long term most people forget how algorithms and data structures work, in addition to cache mechanisms and other optimizations.
Until hardware changes drastically there's no sense rewriting everything (unless of course it's just for fun). It's better use of time to study Math and lower-level concepts instead.
That being said, most businesses will take the quick pill instead.
The evidence is pretty clear that programming in C does not confer enough skill to prevent disastrous mistakes despite its near-hardware level of abstraction, so I'd really like to know what you're saying here.
Although I cannot refute there are additional memory-related issues to be aware of in C, it is precisely such awareness that make someone a better programmer because its how the underlying hardware works. For instance, parallelizing algorithms must take into account cache-alignment boundaries and so on.
Abstractions are good. However, too much abstraction is bad. Just as too much of anything is a bad thing. When people rely solely on such abstractions, it is in fact ruinous to ability.
That being said, higher-level languages certainly have their place. But I firmly believe that C is a high-enough abstraction for systems programming and with proper idioms and testing strategies, it can be just as safe as the plethora of garbage-collected languages out there in the wild.
1. No, it's not. Witness the various arguments that have happened on HN over the years concerning whether the imprecise language of the spec makes some idiom undefined behavior or not.
2. The evidence over the past 35 years has not shown that correct code is "simple" in C.
> For instance, parallelizing algorithms must take into account cache-alignment boundaries and so on.
…which Rust makes you just aware of as C does.
> But I firmly believe that C is a high-enough abstraction for systems programming and with proper idioms and testing strategies, it can be just as safe as the plethora of garbage-collected languages out there in the wild.
1. Rust isn't garbage collected.
2. Your firm belief is contradicted by 35 years' worth of memory safety track records of large-scale software written in C.
"Your firm belief is contradicted by 35 years' worth of memory safety track records of large-scale software written in C."
Keep in mind, of those 35 years, it only makes sense to consider the last decade-onward (or so) in comparison. While the language hasn't changed a whole lot, programming methodologies certainly have. How many of those libraries in question were written within the last 5 or 10 years? What is the code coverage on them? Et cetera.
I am not trying to argue that C is perfect and everything should be written in C. That would be crazy! There are definitely issues with C no doubt--but so many people say C is dangerous and difficult when in fact they don't even practice using the language. This is no different than practicing Violin on a daily basis and saying Cello is difficult so people should stop playing Cello (not a perfect analogy but you know what I mean).
The other comments is just me ranting at how the majority of programmers don't understand important computer-science concepts because of their removal from such concepts by higher-level languages. And sometimes its important to have a higher level abstraction to help solve certain problems. But, people have a tendency to become lazy and therefore they lose the fundamentals over time.
The last decade has seen an explosion of modern C++ code using best practices that routinely exhibits the same memory safety issues. C is worse.
> There are definitely issues with C no doubt--but so many people say C is dangerous and difficult when in fact they don't even practice using the language.
I practice C all the time and I think it's dangerous and difficult. I've seen so many brilliant programmers accidentally create game-over RCEs via use after free, for example.
I am certainly biased as I enjoy procedural languages for their simplicity. So many constructs in the others. As the saying goes, C++ is my favorite 4 languages. I hope that doesn't become the fate of Rust. Go-Lang seems to get that part properly then that's probably debatable.
Examples? I'd be surprised if best practice C++ (C++11, say) had memory safety issues. (I realize that's only the last half-decade...)
> C is worse.
Certainly. Lack of destructors alone makes it hard to create safe abstractions.
Pwn2Own last week. For more examples, search any browser engine's bug tracker.
Yes, this is all modern C++.
Is it? You're telling me that all code in all browsers have been re-written into C++11 (or C++14) with best practices? I don't believe you. At a minimum, I'm going to need some documentation before I believe that.
[Edit: I'm not trying to pull a No True Scotsman here. I just doubt that browsers have been completely rewritten into modern C++, or with anything approaching best practices. I've seen how long old code lives to believe it without some supporting evidence.]
I know it's hard to believe, but C++ is not memory safe, old C++ or modern C++, in theory or in practice. The new C++ features do effectively nothing to change this. As far as use-after-free goes, C++ basically adds safety over C in two places: (1) reference counting is easier to use and is easier to get right; (2) smart pointers are arguably somewhat less likely to get freed before accessed again due to the destructor rules (though I think (2) may not be true in practice). Browsers have been making use of these two features for a very long time.
Bringing up modern C++ here is "no true Scotsman" unless you can point to a specific C++11 feature that browsers are not using that is a comprehensive solution to the use-after-free vulnerabilities they suffer from. There is no such feature I am aware of.
I broke back into this account just to share my marvel at the singly most wrong sentence in human history. The Mona Lisa of being incorrect about programming.
I audit code for security vulnerabilities in several languages professionally. You are correct about the benefits of unit testing, which is language agnostic, but C is not and cannot be Easy To Be Correct, ever, no matter what.
That's not a valid argument for why better tooling can't help alleviate some of the difficulty.
> Honestly using higher-level languages is the same mentality as taking a pill to magically lose weight. It's quick, but detrimental (to programmers ability) in long term.
If that were true, the most effective programmers would only code on assembly.
> Programming becomes easy but in the long term most people forget how algorithms and data structures work, in addition to cache mechanisms and other optimizations.
Why would higher level languages obviate knowledge of any of these things? If anything, I think it would help by reducing "noise" from incidental complexity; e.g. ownership in C vs Rust.
No, because C is as fast as hand-coded assembly in most of the cases. The same can't be said of any high level language in comparison to C (save for C++ and Fortran).
Rust is basically a metal level systems language. Just like C. It has a nicer type system but it compiles to more or less the same thing in the end. Unless you make an error such as using freed memory, in which case it doesn't compile while the equivalent C code would often compile to a very high performance foot-gun. The Rust safety is paid for, for the most part, at compile time unlike the usual high level languages like java/C#/python/... which pay for it at runtime.
Most hand-coded assembly is pretty slow. C isn't any pinnacle of performance either.
C programs are compiled to some non-existent abstract machine that ignores the real variations in memory architecture and CPU implementations. The compiled binary can't adapt at runtime.
I don't know of any C implementations that take advantage of runtime information for optimization purposes. So you end up with generated code with a lot of redundant computation, tests, branches and pointer dereferences (such as function pointer dereference that always refers to same address), just because they might be necessary with some input -- input that wasn't the case this time.
A single mispredicted branch is expensive. Say branch mispredict takes 15 cycles. That's enough to do up to 480 (32*15) 32-bit floating point operations on a single core. Ignoring runtime information takes us pretty far from anything you can call optimal code.
The current crop of compilers are also pretty bad at vectorizing anything complicated. Those cases it can be pretty trivial to beat the compiler by 2-10x, in some cases even 40x+ if your vectorization can also eliminate a lot of unpredictable branches.
The mental capacity saved can be invested in higher level design issues that gone get you a lot more in the long run.
However, lower-level understanding is paramount. For instance, and most-importantly today, taking advantage of multi-threading requires understanding of cache-coherence, memory-alignments, et cetera.
Therefore, while a proper serial algorithm today may be correct its scalability is going to be limited without lower-level understanding. Although, I'll admit that can be built into the higher-level languages (like concurrency in Clojure for instance), but I personally prefer to understand what is going on rather than blissful ignorance :)
Do you know how the lifetime/borrow check system works?
I've heard plenty of criticisms of it, but I've never heard "the lifetime system doesn't make you understand manual memory management".
(disassemble 'my-function)
would give a nice printout. There are also lots of other languages, which might be high-level, but still give you excellent code quality - I used Modula-2 a long time ago which did back then beat the resident C-compiler in generated code quality. Yes, many C compilers are very good and some new stuff not much so, but claiming in general that "C" is fast is an oversimplification.
But again, performance is complicated. People have put tens of thousands of hours into c/c++ optimization. rust is young, so not so much time there. On the upside, rust has room to grow.
https://benchmarksgame.alioth.debian.org/u64q/which-programs...
Please don't generalize that into language X is beating language Y.
I cannot speak to SQlite in this regard.
Clarifying note: this is configurable.
EDIT: Thanks to replies for clarification that it's just one allocator, aborts, and others are available. Still feel weird about it but that's better.
Most programs can't tolerate oom errors, though, and it would be absolutely unreasonable for every function in the standard library that might perform an allocation to return an error that has to be handled by every programmer all the time.
EDIT: C's solution is to make it very easy to ignore oom errors, so most programmers just don't handle oom errors. Rust's solution is much better.
Rust's standard library just isn't designed for writing applications that need to survive oom errors. That's fine, its not designed for a number of other applications which Rust the language is well-suited for either (operating systems, for example). Its designed for the majority use case, because life is full of trade offs.
> The Rust team made conscious design choices in full awareness of the trade-offs
I don't think that anyone who isn't already predisposed to avoid exceptions would consider the tradeoff the Rust people made to be the correct one.
Furthermore, keep in mind that the _pure language_ Rust has no concept of dynamic allocation (apart from "language elements" like __rust_allocate, which are kind of like a pre-linker).
None the less, this is what I typically end up doing when writing Rust code as much of th Rust standard library is simply not ready for "serious" usage.
In my experience, the Rust standard library is extremely convenient and it handles corner cases very well. There are definitely still holes, but that's what "cargo add $CRATE_NAME" is for.
My biggest annoyance with Rust (and it's not a huge one) is that if I'm doing something off the beaten path, I'm probably going to need to wrap a C library or two that nobody has wrapped yet. There's a lot of great stuff on crates.io, but it's only a minuscule fraction of the total C ecosystem.
Can you elaborate on this? I've been getting serious usage out of the Rust standard library for years...
> There is no way to select over a range of TcpStream objects.
mio
> There is no stable way to select over a range of mpsc channels.
Yes, this is annoying. We should stabilize MPSC select. You can use BurntSushi's chan for now though.
> There is no way to access a TcpStream in a non-blocking manner.
mio basically supports this, no?
> There is no support whatsoever for UNIX signals.
Do many languages have good support for this? The C and C++ standard libraries don't; that's part of POSIX. Signals interact very badly with garbage collectors, so I can't imagine many languages have this.
As for Go and signals, I think it's pretty debatable as to whether you can have "excellent support" for signals without the ability to write a true signal handler. Note that this is not a fault of Go and is pretty much inherent to any garbage collected language. I suspect the Rust community would not be particularly happy with an implementation of signals that had hardwired sending to MPSC channels. Even having MPSC channels in the library at all is somewhat controversial...
Really, this is true of any programming language and any involved enough program. It's just that with C and security-critical programs, there's some unfortunate concordance between the errors you want to avoid and the errors that are harder to avoid.