3 ms·
Compiler Can Undo Your Security Checks
- Hizonner 14d agoThe 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.
- wat10000 14d agoSort 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.
- kelnos 14d agoI 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.
- Hizonner 14d 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.
- titzer 14d 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.
- uecker 14d agoThis is not quite correct in C. ISO C at least requires that observable behavior until this point is preserved.
- titzer 14d agoI 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.
- uecker 14d agoIt follows from the definition of UB: undefined behavior: "behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this document imposes no requirements" The C++ spec at some point changed this to explicitly allow changing anything in the program not just the specific behavior implied by the "for which". The C spec never did this. Because people were confused about this, in C23 we added the following note. "Note 3 to entry: Any other behavior during execution of a program is only affected as a direct consequence of the concrete behavior that occurs when encountering the erroneous or non-portable program construct or data. In particular, all observable behavior (5.1.2.4) appears as specified in this document when it happens before an operation with undefined behavior in the execution of the program." Compilers mostly follow this. GCC has bugs related to volatile. Clang often follows the C++ standard, where it is different from C, so probably does not conform to the standard here (as for some other things). The new C++ standard will have UB "barrier", i.e. std::observable that will limit the effect of UB to things before this barrier.
- 14d ago
- uecker 14d agoThe 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.
- fithisux 14d agoCompiler does what I tell it. Not the other way around.
- Narishma 14d agoTitle should be "Your C compiler can undo your security checks".
- johnbender 14d agoA key problem is that compilation operates on an implicit (compiler writers have this in the back of their heads) notion of correctness which is very roughly “preservation of observable behaviors” where “observable” is sequences of system calls and then return value. That is, the final output of a compiler should never add new sequences of observable behaviors. Security properties on the other hand are very often about the relationship between these sequences. For example we like to say that an external observer/attacker can’t distinguish internal state by the external observations (confidentiality) which requires that two observable traces given different hidden values don’t have different observations from the same starting observable state. If you’re an LTS nerd you know this difference as trace properties vs hyper properties. Compilers try to preserve the former but not the latter. Separately there is the problem of “what is observable?” For example, if you include timing in your observable behaviors suddenly the kinds of compiler passes that are able to preserve observations tends to zero rather quickly.
- bmandale 14d agoweird f*ing site. apparently doesn't like hn visitors and doesn't want me opening the console. i know when im not wanted
- Hizonner 14d ago> doesn't want me opening the console Whoa. That's not even something it should be able to detect. What's the gaping security hole that lets it do that?
- wavemode 14d agothere are a wide variety of ways to indirectly detect that someone opened devtools the most common way is to set a breakpoint, which pause JS execution only if devtools are open. this pause can be detected with a timer.
- NekkoDroid 14d agoOn Firefox, while F12 is intercepted you can still open it with Ctrl+Shift+E
- omoikane 14d agoRegarding compiler deleting memset, there are multiple memory-zeroing functions that are guaranteed not to be optimized away: https://cppreference.com/c/string/byte/memset https://cppreference.com/c/string/byte/memset (memset_s) https://cppreference.com/cpp/string/byte/memset https://cppreference.com/cpp/string/byte/memset (memset_explicit) Also see notes section for various other functions that people were using before memset_s became standard.