Compiler Can Undo Your Security Checks

(davidbombal.com)

9 points | by birdculture an hour ago ago

9 comments

  • Hizonner 35 minutes ago 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.

    • titzer 17 minutes ago 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.

      • mpyne 4 minutes ago 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.

      • uecker 6 minutes ago ago

        This is not quite correct in C. ISO C at least requires that observable behavior until this point is preserved.

    • kelnos 30 minutes ago 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.

      • Hizonner 19 minutes ago 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.

    • wat10000 30 minutes ago 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 bat memory just fine.

  • fithisux 32 minutes ago ago

    Compiler does what I tell it. Not the other way around.

    • kibwen 19 minutes ago 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.