Back to News
Advertisement
Advertisement

⚡ Community Insights

Discussion Sentiment

17% Positive

Analyzed from 1179 words in the discussion.

Trending Topics

#compiler#code#don#memory#language#result#outside#world#same#compilers

Discussion (25 Comments)Read Original on HackerNews

Narishmaabout 1 hour ago
Title should be "Your C compiler can undo your security checks".
LoganDark34 minutes ago
Any compiler can do it -- LLVM definitely does even for languages that aren't C
fithisuxabout 2 hours ago
Compiler does what I tell it. Not the other way around.
kibwenabout 1 hour ago
The compiler does everything that the language spec allows it to get away with. The language spec itself is a communication protocol between users and compilers, and that includes the definition of the abstract machine.
Hizonnerabout 2 hours ago
The compiler is allowed to transform your code if it can prove that the result would interact with the outside world in exactly the same way as your original code would, right? You can optimize on "I already checked the value of X" if you can prove that nothing could have changed X.

Well, it sounds like a lot of compilers are making unjustified assumptions about what the outside world is allowed to affect or observe. Maybe with the encouragement of specs, maybe not.

kelnosabout 2 hours ago
I don't think that's the problem, and I think your "if it can prove..." isn't really accurate.

The compile can transform your code if the result of the computations it makes is the same (plus any ordering guarantees you've encoded with the correct primitives, etc.). "Interact with the outside world in exactly the same way" is way too strong a guarantee.

The canonical example is the constant-time comparison. You want to compare a provided password hash with the one in your database in such a way that every comparison, regardless of success or failure, completes in exactly the same amount of time. The compiler does not care about this desire of yours, though, and can and will try to optimize things so it will stop the comparison as soon as it knows they don't match, which can take different amounts of time depending on the input. This is perfectly valid and reasonable, but breaks security sometimes.

Another example is to allocate some memory, write to it, and free it, without reading it. The compiler is free to optimize the entire thing away. We have `volatile` in C because sometimes merely writing to a memory address has side-effects that aren't visible to the compiler, but if you don't use it, the compiler can do what it wants.

Hizonnerabout 1 hour ago
> The compile can transform your code if the result of the computations it makes is the same (plus any ordering guarantees you've encoded with the correct primitives, etc.). "Interact with the outside world in exactly the same way" is way too strong a guarantee.

The word "result" is doing a lot of work there. `printf ("%d\n", 2+2);` isn't interesting because 4 appears in a memory cell; it's interesting because 4 appears on stdout. Which one is the "result"?

If you're going to make assumptions about what's "inside" the program and what's "outside", you have to make them explicit. And they have to be reasonable assumptions. An assumption that memory is "inside" has to be justified in the presence of shared memory, virtual memory, debuggers, or whatever. You have to actually explain what you mean in a lot more detail than I think the average spec has a chance of doing.

If I create an unlinked temp file, and the compiler can observe that I'm holding the only FD open on that file, should that file be seen as "inside the computation", or as "a collection of results and inputs"?

Without reading the specs, I can be 95 percent sure that they don't nail down all the issues... and 100 percent sure that if they do nail down all the issues, or even all the possibly important issues, the corner cases are unknown to almost all actual programmers. Which means either that it's not appropriate for the compiler to rely on just any rule regardless of what the spec says, or that it's not reasonable to write code in the language.

ueckerabout 1 hour ago
The compilers are mostly doing the right thing (not always). The C standard specifies what is considered an effect on the outside world, i.e. file I/O and volatile accesses. For concurrent programming, there is also a memory model that specifies what other threads can see.

Here, the issue seems that compilers can reload variables. If this is a bug, then you already have a data race in your program which you can prevent with correct use of locks and/or atomics.

titzerabout 1 hour ago
> The compiler is allowed to transform your code if it can prove that the result would interact with the outside world in exactly the same way

Well, in C/C++, as soon as your program has one UB bug, the compiler has absolutely no obligation whatsoever.

mpyneabout 1 hour ago
Well yes, that's what UB means. It's a singularity, you run into it and there's no longer a specified requirement for the behavior that will follow.

Rust also has UB, btw, https://doc.rust-lang.org/reference/behavior-considered-unde..., so I don't know where it is that people have imagined this is something the C and C++ language designers went out of their way to foist upon you.

If you want to write code for a VAX, then you can use the K&R C compiler where it had defined outcomes for everything. If you want to write portable C code for modern CPUs then it's fair to ask what the C language standard is supposed to define for each of those CPUs and OSes and ABIs.

And a bunch of people were nice enough to do that for you and I, but because they are not deities, there are things that they had to leave out to make the language useful, so they did.

chrismorganabout 1 hour ago
> Rust also has UB, btw

Not at all the same. C and C++ are full of hazards and I get the impression it’s genuinely difficult to avoid entirely in normal code bases, and typically impossible to avoid statically. Whereas in Rust it’s all gated behind the unsafe keyword, and if you don’t use it (and most code bases never need to use it), you cannot encounter UB; and that scoping makes it far easier to control and handle correctly.

titzerabout 1 hour ago
> I don't know where it is that people have imagined this is something the C and C++ language designers went out of their way to foist upon you.

They did. Most other languages, the vast majority of which are also memory safe languages, go out of their way to do the opposite, and give meaning even to erroneous programs. Some things slip through the cracks, and generally language designers and implementers work hard to get rid of UB.

C/C++ is the only ecosystem that has fully embraced UB as a way of life. They are the only compilers that make full use of "UB is bad and cannot ever happen" as a core tenet in how optimizations are designed. Rust UB is at least a little different. Rust UB can only be the result of unsafe code and is meant to be limited in blast radius, and is absolutely not meant as a loophole for compilers to just do whatever to make the code faster.

C/C++ have a surprisingly large set of UB, too. Thankfully, the rest of the software world is rising up and the committees are starting to make things like gasp signed arithmetic overflow into defined behavior.

But don't hold your breath.

tialaramexabout 1 hour 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.

ueckerabout 1 hour ago
This is not quite correct in C. ISO C at least requires that observable behavior until this point is preserved.
tialaramexabout 1 hour ago
This is important. Undefined Behaviour is a behaviour, and so if we can ensure the behaviour doesn't happen, our problem is averted. For example if there's UB when a zero size file is loaded by our software, we can instruct operators to check the file has a non-zero size and we're preventing whatever horrible UB would arise.

We can even rope off whole parts of the software. If the Postscript printing code has UB, simply instruction operators only to use the HP inkjet printers for which we know Postscript is not used can prevent this UB from happening.

titzerabout 1 hour ago
I don't think this is true. You're going to have to point to the exact place in the spec that says this, because optimizations in the presence of UB in most compilers make absolutely no assumptions.
wat10000about 2 hours ago
Sort of. Its idea of “the outside world” is very restrictive. Simple example: write into a pointer, then free it. From the compiler’s perspective, this write can’t be observed and can be removed. It’s not legal to read memory after it’s been freed, so there’s no legal side effect from that write. But in reality, we can use a dangling pointer or a debugger and read that memory just fine.