6 ms·
If you don't want UB to mean "Absolutely anything might happen" which necessarily has to include "... forever" then you need Fil-C or similar runtime handling s
by tialaramex 4d ago
If you don't want UB to mean "Absolutely anything might happen" which necessarily has to include "... forever" then you need Fil-C or similar runtime handling so that any time its behaviour would become undefined the program exits instead.
To some extent in C and even more in C++ there's a much worse problem, IFNDR [Ill-formed No Diagnostic Required]. Programs which the language specification insists mean nothing at all, but your tools won't (in many cases can't) notice so the result might do anything. It's not Undefined Behaviour, your program never had any defined behaviour at all.
- mirashii 4d ago> Fil-C or similar runtime handling so that any time its behaviour would become undefined the program exits instead It’s worth being clear here that this is not what Fil-C does, it still has UB, and can still explode in many of the same ways as C and all (after all, it’s a clang fork). Fil-C takes one particular class of allocation related bugs and UB off the table, but leaves many of them behind.
- uecker 4d agoWhatever happens for the other UB remains bounded memory safely in Fil-C. (according to a definition of memory safety that excludes protection of subobject bounds, but Rust also redefines memory safety to what the Rust compiler can do, e.g. excludes memory leaks).
- mirashii 4d agoI'm not so sure you can make such a strong statement about what happens when UB is invoked. The presence of UB allows the compiler to make all kinds of weird assumptions, and it seems very unlikely that there exists no series of allowable transforms that results in a pointer capabilities check being elided or similar.
- tialaramex 4d agoPresumably Martin intends the usual caveat that this is subject to bugs. So, "there might be a bug in the compiler" isn't interesting. There are bugs in LLVM, bugs in Rust's trait resolution, bugs in Javascript implementations, obviously code has bugs and that's generally not very interesting - we can fix bugs. Are you claiming that there must be such transforms or only that it is likely that bugs exist which isn't interesting.
- mirashii 4d agoNeither. I'm claiming that UB allows a transform that violates or ignores the additional guards put in place by Fil-C, and by the definition of UB that is not a bug in the compiler, as any behavior is allowable. To assert that something specific always happens under UB is counter to the definition of UB. Fil-C carefully defines away UB for some operations, but to make full guarantee of safety, even by their definition and modulo bugs, I believe it necessary to fully remove UB.
- tialaramex 4d agoMaybe it was unclear that Fil-C is a compiler. So the thing doing those transforms you're worried about is Fil-C. Fil-C gets to look at some code which has UB if variable z is 9 and go "OK, lets check whether z is 9, and if so...". The result, of course, is much, much slower than executables from a typical modern C compiler, but since "It always does X" is in fact a permitted implementation of "Undefined Behaviour" this is a compliant C implementation, at least in most observable respects.
- tialaramex 4d agoLeaking isn't unsafe. Even if you have a linear type system, and so you can't leak in this useless technical sense, it makes no practical difference, the end user doesn't care that your program which gradually bloats by 1GB per hour until it blows up doesn't technically "leak" memory because it was just caching some data with no limits, whereas my program which grows by 800MB per hour and thus blows up slightly less often does technically "leak" memory because it didn't keep the references needed to free that data. To the end user these programs are both leaky garbage.
- uecker 4d agoThis depends on the definition. Rust originally included leaks in their definition of unsafe and later changed the definition when they found out that they can not reliably prevent leaks. Leaks could reasonably be considered unsafe as they can cause a program to crash due to resource exhausting, even where the actually used memory is limited. If you do not consider leaks as part of the problem, there is a trivial way to avoid use-after-free and double-free: Simply never free any memory or only at the very end. (Which in some scenarios is exactly what people do.) The later would fulfill your definition of "holding references to be able to free them".
- tialaramex 3d ago> Leaks could reasonably be considered unsafe as they can cause a program to crash due to resource exhausting, even where the actually used memory is limited. You can make this claim about any resource, there's no reason to single out memory here. You can run out of file descriptors, inodes, connections to a remote database, disk space, anything - including CPU time. And notice that it wasn't the leak that you've now said was unsafe, it was the use itself. The program didn't blow up "because of a leak", it blew up because we exceeded some arbitrary resource threshold which may have been invisible to us. > If you do not consider leaks as part of the problem, there is a trivial way to avoid use-after-free and double-free: Simply never free any memory Indeed. And that's exactly what we see in some domains and if you've solved the other issues (e.g. bounds misses, type confusion) you've now got memory safe programs. Most general purpose software can't be written this way, but there is a whole heck of a lot of software out there which could be. > The later would fulfill your definition of "holding references to be able to free them". And that former would be characterized as "leak everything" and indeed that's entirely safe and, just as I said, to an end user this is a distinction which makes absolutely no difference.
- tialaramex 4d agoDo you have a concrete example of C which will "still explode" ?
- mirashii 4d ago#include <stdio.h> #include <stdlib.h> int shift(int x, int n) { return x << n; } int main(int argc, char **argv) { printf("%d\n", shift(1, 32)); /* n == width: UB */ return 0; } This program exhibits UB in Fil-C, and you can see that the optimizer does different things at -O0 (outputs 1) and -O1/-O2/-O3 (outputs 0). Since this creates poison, which Fil-C doesn't remove, you can use it to construct all kinds of weird things. static void loop(void) { int s = shift(1, 32); int n = 0; for (int i = 0; i < s + 3; i++) n++; printf("[loop] iterations=%d (s+3=%d)\n", n, s + 3); } static void sw(void) { switch (shift(1, 32)) { case 0: puts("[switch] case 0"); break; case 1: puts("[switch] case 1"); break; default: puts("[switch] default"); break; } } int main(int argc, char **argv) { loop(); sw(); return 0; } In Fil-C -O0, this gives 4 iterations of the loop and executes sw(). At any higher optimization level, it turns loop() into an infinite loop and drops sw() from the binary entirely.
- tialaramex 4d agoNeither of those sound like Undefined Behaviour to me, they're maybe unspecified but they don't sound undefined at all - are you confusing Undefined Behaviour with "I wanted it to do something else" ? You said "explode" earlier and so I was expecting something a bit more dramatic than "Unsurprisingly Fil-C has unspecified results for some expressions".
- mirashii 4d agoYou seem to be trying to apply some colloquial definition of undefined behavior. Undefined behavior is a very specific, defined term. The shift(1, 32) call is by definition undefined behavior (See 6.5.7 in the C standards from C99 up). The behavior of the program itself when undefined behavior is invoked is allowed to be _anything_. I've simply demonstrated here that the compiler is using the fact that there is undefined behavior to perform optimizations that would not be allowed without undefined behavior. Those optimizations are allowed to result in the program doing anything at the compiler's whim.