News for hackers. Stuff that matters.
Re:Three proposals for the C standard WG14 by geofft on Wednesday June 20, 2018 @06:03PM (#17360223)
> 6. EXAMPLE Code that checks to see if a pointer value is null cannot be omitted during translation solely on the basis that the pointer is dereferenced before the check and that earlier dereference would produce undefined behavior if the pointer value was null.

Here's the thing I don't quite understand: I gather that this is advocating for something like GCC's -fno-delete-null-pointer-checks to be part of the spec, but, how is -fno-delete-null-pointer-checks actually helpful?

I think it's because if you write code like this:

    void foo(struct foo *param) {
        int n = param->length, i;
    
        if (param == NULL) {
            return;
        }
    
        for (i = 0; i < n; i++) {
            do_something(param->data[i]);
        }
    }
with -fdelete-null-pointer-checks the compiler might optimize it in two successive steps to delete the check (because param->length was already dereferenced) and then move the dereference down to the beginning of the for loop, and with -fno-delete-null-pointer-checks the compiler might skip the first optimization and keep the second.

But for -fno-delete-null-pointer-checks, you're relying on the second optimization, moving the dereference later. Otherwise you'd just have segfaulted already (or, in a kernel context, the read might have succeeded from a userspace page mapped at the null page). Right?

It seems reasonable for the compiler (or some other tool) to produce a hard error here and prevent this code from compiling, saying, hey, this is clearly doing something unwanted. It doesn't seem reasonable to expect the compiler to do certain optimizations to make your code safe, without which your code would have a security vulnerability, and also get mad at the compiler for doing more optimizations that return it to the original state of things.

> The limitation of lvalue access by types has never made much sense and required a undefensible exception for character pointers.

My understanding of type-based alias analysis is that it's required for code like this to optimize the way you'd want:

    struct string {
        size_t len;
        char *data;
    }

    void copy_string(struct string *dest, struct string *src) {
        size_t i;
        if (dest->len < src->len) return;
        for (i = 0; i < src->len; i++) {
            dest->data[i] = src->data[i];
        }
    }
Otherwise the compiler would have to worry that dest->data would overlap with src->len and re-read it every time!

You can do this without type-based alias analysis in a language that gave you different tools for managing variables, but doing so backwards-compatibly in C seems difficult, and this proposed rule does not seem to help.

Re:Three proposals for the C standard WG14 by comex on Thursday June 21, 2018 @04:37AM (#17363201)
> It seems reasonable for the compiler (or some other tool) to produce a hard error here and prevent this code from compiling, saying, hey, this is clearly doing something unwanted.

The thing is, the redundant null check usually doesn’t appear literally in the source, but comes from an inlined function call (or even a macro). In that case, the null check is probably not a mistake; it’s just that this particular caller of the function that got inlined never passes null, and so will never trigger that code path.

I do think there’s unexplored potential for compilers to identify when they’ve eliminated code that didn’t come from inlining, and warn the user about the potential bug. However, this is much easier said than done, since the IR has gone through a lot of transformations by that point and there are a lot of edge cases to worry about. For example, even if all the IR instructions corresponding to a given piece of source code are dead, that code might still have affected the function compilation in some manner and thus isn’t useless – such as by being merged with a different (or perhaps redundant) computation, or by affecting control flow.

Also, even if some code path really is useless as compiled, it might be for innocuous reasons. For example, perhaps a given null check was not itself inlined, but the value being checked comes from an inlined function:

    void *get_a_pointer(void) {
        static int x;
        return &x; // not null!
    }
    int main(void) {
        void *p = get_a_pointer();
        if (p == NULL) …
    }
Unlike in the reverse situation where an inlined null check might be useful to another caller, here we know that that null check will never fire – that is, in this program, in this configuration, the check is useless. But perhaps with a different set of configuration flags, or on a different platform, the code uses a different implementation of get_a_pointer() which could return null. Or perhaps there is an abstraction boundary between main and get_a_pointer, where the documented interface of get_a_pointer says it might return null: even if the current implementation never does, the author expects that that might change in a future version of the code, so clients should proactively include a null check to avoid future breakage. (Of course, get_a_pointer might be part of a third-party library, and the author of main might have never even peeped at its implementation. These situations are especially common if you’re using LTO.)
Re:Three proposals for the C standard WG14 by dbaupp on Wednesday June 20, 2018 @07:18PM (#17360772)
> But for -fno-delete-null-pointer-checks, you're relying on the second optimization, moving the dereference later. Otherwise you'd just have segfaulted already (or, in a kernel context, the read might have succeeded from a userspace page mapped at the null page). Right?

Something that's actually a pointer offset, not a dereference, like `int *n = &param->length`, is also an assertion of being non-null.

Re:Three proposals for the C standard WG14 by geofft on Wednesday June 20, 2018 @07:28PM (#17360827)
Aha, I was curious if that was the case.

It seems reasonable to amend the C standard to say "That's not an assertion of being non-null" (and more generally to make &invalid_pointer->item and &invalid_pointer[n] valid; IIRC implementing offsetof as (size_t)(((type *)NULL)->member) is UB). Would that prevent any significant optimizations?

Also does the rule that pointers can only point to elements of allocated objects or one-past-the-end enable any significant optimizations?

Re:Three proposals for the C standard WG14 by cesarb on Wednesday June 20, 2018 @09:13PM (#17361270)
> Also does the rule that pointers can only point to elements of allocated objects or one-past-the-end enable any significant optimizations?

Yes, for segmented architectures like the 8086, it means that pointers don't have to be normalized after every operation. That is, when you increment or decrement a pointer, you only need to do the operation on the offset, you don't have to touch the segment. When all you have is less than 10 MHz, every operation counts.

(For those who don't remember the 8086, it's not as simple as concatenating the segment and the offset to obtain the physical address: to get the 20-bit physical address from the 16-bit segment and the 16-bit offset, the formula is segment*16+offset. As a consequence, the same memory location could be accessed through several distinct segment:offset pairs. If the memory allocator chose the segment value right, objects of 64 KiB or less could be indexed by doing all operations on the offset value, without having to touch the segment value.)

Re:Three proposals for the C standard WG14 by iainmerrick on Wednesday June 20, 2018 @06:14PM (#17360316)
Well, what benefit does -fdelete-null-pointer-checks bring? Why would I ever want to remove that check? The more conservative version either segfaults or (quite likely) accidentally does the right thing. That situation is not great but it’s better than nothing. The fancy optimization just makes a not-great situation much worse.

I agree, it would be best if the compiler rejected this as clearly incorrect code. I think that needs a change in the spec relating to undefined behavior, though (and more importantly a change in direction from compiler vendors).

Edit to add: your second example, type aliasing, is a good one too. I think that one could also be usefully addressed by a compiler that flags it as a likely error. Although you’d need some way to annotate the code in the event that the weird aliasing behavior is actually correct and expected (I dunno, make “len” volatile or something?)

Re:Three proposals for the C standard WG14 by geofft on Wednesday June 20, 2018 @07:07PM (#17360715)
> Well, what benefit does -fdelete-null-pointer-checks bring? Why would I ever want to remove that check? The more conservative version either segfaults or (quite likely) accidentally does the right thing. That situation is not great but it’s better than nothing.

My concern is largely about how to phrase "The compiler may not delete this dead code" in a way that's meaningful. For instance, I assume the intent of the proposal is not to forbid optimizing this code in the obvious way, right?

        int n = param->length, i;
    
        if (param == NULL) {
            return;
        }

        if (param == NULL) {
            return;
        }
    
        for (i = 0; i < n; i++) {
            do_something(param->data[i]);
        }
What about this?

        int n = param->length, i;
    
        for (i = 0; i < n; i++) {
            if (param == NULL) {
                return;
            }
            do_something(param->data[i]);
        }
which is more likely to show up in the form of an inline function that checks for NULL - can the compiler refactor that check into a single one?

> I think that one could also be usefully addressed by a compiler that flags it as a likely error.

The compiler can't know when compiling that one function whether src->len and dest->data alias. It might be compiling that file as a standalone object file, with no other context about who's calling it. Should it emit a warning when compiling that code? (What's the warning, "I'd like to optimize this but I can't?")

Alternatively, should it flag the function ... somehow ... so that when someone else calls it with aliasing arguments, that's an error? That isn't even a capability available to the C language as written: it's valid to call that function knowing nothing other than the struct definition and the function's prototype.

You can imagine a language where variables carried information about whether they alias. (Actually you don't really have to imagine; the `restrict` keyword does a subset of this in C itself, and other languages have much more explicit ways of tracking this, such as Rust's shared vs. mutable references.) But it would be hard to retrofit such a thing onto existing C code.

The fundamental problem with the C language working group is that they're obligated to keep working on C as it already exists.

Re:Three proposals for the C standard WG14 by vyodaiken on Thursday June 21, 2018 @10:09AM (#17365232)
One of the reasons for this proposal is that LTO is going to make C completely unusable otherwise. Suppose that a driver produces a structure that points to data buffers. Core code interacting with the drivers knows that drivers are of variable quality and checks that the structure is properly setup: if(x->buffer == 0)panic("bad driver" ) or something more sophisticated. Everything works beautifully until some driver is added with a dereference of x->buffer - which then defeats the guard in the core core because now the compiler " knows" that the x->buffer is not null. Separate compilation and inlining can't work reliably in this situation.
Re:Three proposals for the C standard WG14 by geofft on Thursday June 21, 2018 @11:18AM (#17365918)
Are you suggesting that driver code has an opportunity to run before core code can implement checks?

If so, then again, this has the fundamental problem that you're expecting the compiler to implement some optimizations - like pushing the dereference out of the buggy driver via LTO - and not others. If the compiler had all optimizations off, it would just generate code to read from x->buffer in the obvious place, which would page-fault at runtime before the check from core code even happened.

If the core code implements the check first, then this optimization won't cause you problems, because this optimization is strictly about checking for a NULL pointer after using it.

For the specific case of computing the address of x->buffer without actually dereferencing it, I do agree that the compiler should not consider that to be an implicit non-null promise. That seems much more likely to be a change you can make without breaking the language spec. See the subthread with 'dbaupp.

(Alternatively, you could work on building tools for driver writers to write in a less error-prone language than C, because drivers being of poor quality is a much broader problem with much broader impacts than just triggering ill-advised optimizations under LTO, and simply optimizing drivers less efficiently isn't going to solve the problem that the driver shouldn't be dereferencing x->buffer without its own NULL check in the first place. If you check my recent GitHub activity you might notice a project to do exactly this in Linux....)

Re:Three proposals for the C standard WG14 by vyodaiken on Thursday June 21, 2018 @10:07PM (#17370738)
This is a really common code pattern: client sends request to server; server sanity checks request. If you omit sanity checks from the server because you assume there are no errors in the client, you break a lot of systems.

I am expecting the compiler to only make optimizations based on things it knows are true - that are true. So if the source code is "check for null;check for null; do something;" the compiler should be able to delete the second check (hopefully with a warning). In fact, the Standard is very explicit about permitting optimizing compilers to delete repeated operations. However, in the case I described, the compiler does not know that the pointer is non-null, it's just easier to assume it is not null. That leads to an "optimization" that is not an optimization. I agree that requiring compilers to only use true optimizations is more of a challenge than allowing them to use false ones, but nobody said it was going to be easy. In particular, it looks like Clang loses way too much information going from source to llvm language which makes it difficult to carry information about whether, for example, a pointer dereference is safe or not - but that's a defect in the compiler, not in the source language.

Re:Three proposals for the C standard WG14 by iainmerrick on Wednesday June 20, 2018 @07:16PM (#17360764)
What's the warning, "I'd like to optimize this but I can't?"

Yes, that's exactly what I had in mind! :)

Re:Three proposals for the C standard WG14 by geofft on Wednesday June 20, 2018 @07:38PM (#17360881)
My instinctive response was going to be "That can't work, there are too many of them," but apparently you can do this particular one with -Wnull-dereference (which requires -fdelete-null-pointer-checks enabled to work, but you can just compile twice, once as a throwaway and once for production with -fno-delete-null-pointer-checks, or do the former for non-release builds, or something).

More generally, it might be the case that enough of the last several years of unpopular optimizations are controlled by flags that you can compile the code twice, once with the optimization enabled and once without, and warn whenever the optimization changes something. It might even be possible to do this with a shell script that treats the compiler as a black box with -ffunction-sections and uses objdump or something.

I do wonder how hard it is to write code that produces the same object code regardless of something more major like -ftrapv, though.

Re:Three proposals for the C standard WG14 by Asooka on Thursday June 21, 2018 @04:11AM (#17363065)
My view is that the compiler cannot remove any of these NULL checks. There should be a separate tool that gives the programmer hints that she is making unneeded checks and suggest program transformations, but removing NULL checks is unsafe when done automatically. Because I might be on a platform where param->length is actually referring to valid memory, so my program won't segfault and the NULL check is the only way to know.
Re:Three proposals for the C standard WG14 by geofft on Thursday June 21, 2018 @08:44AM (#17364548)
The compiler can't remove repeated NULL checks and collapse them to a single one?? Why?
Re:Three proposals for the C standard WG14 by vyodaiken on Thursday June 21, 2018 @10:14AM (#17365289)
a) these are marginal optimizations, at best. b) because it violates C practice. My parsing library is written to distrust inputs so as to protect against buffer exploits. Your compiler "optimizes" away my security check because some buggy code dereferences a null pointer - which actually works in this implementation. Great. Now you have "optimized" to create a security hole in the program by removing a single instruction that is actually free in many pipelined architectures. Wonderful. Thanks so much.
Re:Three proposals for the C standard WG14 by geofft on Thursday June 21, 2018 @11:30AM (#17366047)
> a) these are marginal optimizations, at best.

The optimization isn't written in the form of "If you see this ridiculous code, make it less ridiculous." It's written in the form of "You can refactor repeated checks of presumed-non-aliased variables into a single check." Otherwise code like

    for (i = 0; i < *plen; i++)
would involve dereferencing plen on every single loop, which for many applications is definitely not marginal!

> some buggy code dereferences a null pointer - which actually works in this implementation.

What implementation is this? (Are you mmapping at NULL, or writing for an architecture without an MMU, or something?)

NULL doesn't have to be address zero. It's denoted by literal zero, in the C source, but it can be something totally different when translated to the machine. Can you arrange for some other address to generate a segfault, and tell your C compiler to use that as NULL? (I don't know how to do this with gcc, and I'd like to know how, honestly - it seems like a significant win for kernel security if NULL pointers in kernel code were an invalid kernelspace address and not an invalid userspace address.)

Re:Three proposals for the C standard WG14 by cesarb on Thursday June 21, 2018 @11:50AM (#17366221)
Many C programs assume that initializing a variable to all-bits-zero, through memset, calloc, the .bss section, or on the kernel the GFP_ZERO flag, will initialize all pointers within it to NULL. See also: http://austingroupbugs.net/view.php?id=940 ("all 0 bits should be a null pointer on POSIX systems").
Re:Three proposals for the C standard WG14 by vyodaiken on Friday June 22, 2018 @10:42AM (#17374295)
Not correct. g(x){ ... if x is null panic otherwise x->v = n} can currently be optimized to x->v =n given f(){ ... y =x->v ... g(x) } because null dereferences are assumed impossible in some twisted interpretation of the standard. Imagine the effects with inlining and LTO. That's my issue, not perfectly ordinary standard value caching etc.
Re:Three proposals for the C standard WG14 by vyodaiken on Thursday June 21, 2018 @12:06AM (#17362071)
For certain types of image manipulations or other computations, it may well be that the pointers are supposed to refer to overlapping regions. This is the kind of problem that, as Dennis Ritchie pointed out, bedeviled FORTRAN for ever. I think the solution is to fix "restricted" and let programmers opt-in. If the programmer doesn't do that, or gets it wrong, too bad.
Re:Three proposals for the C standard WG14 by comex on Thursday June 21, 2018 @04:43AM (#17363232)
The issue isn’t just that src->data and dest->data might overlap, which TBAA doesn’t forbid anyway (although that’s what restrict can help with). It’s that dest->data might alias src->len, and magically change the length midloop. I doubt there’s any legitimate image manipulation where that would happen, but the compiler doesn’t know that...
Re:Three proposals for the C standard WG14 by vyodaiken on Friday June 22, 2018 @11:02AM (#17374466)
This is exactly why I don't propose to do away with UB but to limit what compilers can do with UB. The programmer in C cannot be prevented from writing terrible code where e.g. a loop counter is written over by an aliased pointer in the middle of the loop - the current standard doesn't prevent it either. In fact, strictly conforming C code can use char pointers to randomly write over any aliased variables if I understand it correctly. The positive thing about UB is that it makes this the programmers problem, not the compilers problem. The negative thing is that in current interpretations, the compiler is free to detect such an error and then instead of either failing to compile with a warning or just letting things happen as the programmer has specified, it can introduce global program failures because "it's impossible to have UB".
Re:Three proposals for the C standard WG14 by geofft on Thursday June 21, 2018 @08:43AM (#17364544)
Your proposals are going to slow down tons of real-world C programs that are not engaged in these types of manipulations, to the point where people writing high-performance software are better off using Python, or need to make so many changes to their source code to recover reasonable performance that they might as wll make a few more changes and end up with Go and Rust.

If you really think C is enough of a disaster that this is justified, why even care about fixing C?

Re:Three proposals for the C standard WG14 by vyodaiken on Thursday June 21, 2018 @10:02AM (#17365166)
If you could actually provide some concrete basis for that argument, it would be interesting.