> 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)
> 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)
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.
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.
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.
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.