6

This code :

int *p = nullptr;
p++;

cause undefined behaviour as it was discussed in Is incrementing a null pointer well-defined?

But when explaining fellows why they should avoid UB, besides saying it is bad because UB means that anything could happen, I like to have some example demonstating it. I have tons of them for access to an array past the limits but I could not find a single one for that.

I even tried

int testptr(int *p) {
    intptr_t ip;
    int *p2 = p + 1;
    ip = (intptr_t) p2;
    if (p == nullptr) {
        ip *= 2;
    }
    else {
        ip *= -2;
    } return (int) ip;
}

in a separate compilation unit hoping that an optimizing compiler would skip the test because when p is null, line int *p2 = p + 1; is UB, and compilers are allowed to assume that code does not contain UB.

But gcc 4.8.2 (I have no useable gcc 4.9) and clang 3.4.1 both answer a positive value !

Could someone suggest some more clever code or another optimizing compiler to exhibit a problem when incrementing a null pointer ?

Community
  • 1
  • 1
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • 3
    "I have tons of them for access to an array past the limits" Do you collect examples of UB? :) – lisyarus Apr 24 '15 at 10:34
  • 10
    Build a standards-compliant compiler that eats your cat if you increment a null pointer. Job done. – Bathsheba Apr 24 '15 at 10:35
  • 1
    @lisyarus : no I'm not specially fond of UB, but I really like concrete example to strengthen my speechs – Serge Ballesta Apr 24 '15 at 10:37
  • @Bathsheba : I course I could try it, by besides writing a full C++ compiler is far beyond my capacity, I would like an existing compiler not only used by me :-) – Serge Ballesta Apr 24 '15 at 10:40
  • 4
    I vaguely remember something about weird hardware that has different memory for strings and other stuff and if you create a pointer pointing to the wrong thing without dereferencing you got a segfault already. This is the reason why you are only allowed to have pointers point to real memory (besides the `nullptr` of course), so C++ works on that weird hardware. Maybe someone remembers what machine that was so you can get it and show it off. – nwp Apr 24 '15 at 10:48
  • Undefined behaviour is a problem because anything the code does when executed is correct. Different compilers can do different things, and all of them are correct. Doing nothing untoward is just as correct as trashing your system.. – Peter Apr 24 '15 at 10:49
  • What do you mean "**besides** saying it is bad because UB means that anything could happen"?? – Barry Apr 24 '15 at 10:55
  • @Barry Some people are not happy just knowing the rules, they want to know what the rules are for. This helps in doing the right thing even if the rules do not cover some edge case and to figure out if maybe the rule is broken. – nwp Apr 24 '15 at 11:04
  • @Peter UB can even be worse than what you say. Different compilers doing different things would be just *implementation defined*. UB is normaly more like writing random values at random memory locations ... what I cannot exhibit from null pointer incrementation :-( – Serge Ballesta Apr 24 '15 at 11:17
  • 1
    Absolutely incorrect, Serge. Undefined behaviour doesn't mean anything has to be trashed. Trashing memory is only one possible symptom of undefined behaviour. Undefined behaviour (no bounds whatsoever on what is permitted to occur) is also not the same as implementation defined behaviour (the behaviour might vary between compilers, within some specified bounds, but must be documented for each one). – Peter Apr 24 '15 at 11:30
  • @Peter :You are right of course. I should have say : *UB* **can** *be as as worse as writing ...* . But to frighten fellows from doing arithmetic with null pointer, that's the kind of symptom I'd really like to show – Serge Ballesta Apr 24 '15 at 11:42
  • This always returns false on gcc 4.9: `bool is_nullptr(int* p) { return (p-1) < nullptr && (p+1) > nullptr; }` – Not a real meerkat May 19 '15 at 14:35
  • @CássioRenan : Nice try ! But too easy ... `intptr_t` is unsigned and you would get same thing with an unsigned type : `bool f() { unsigned int u =0, v = 0; return (u - 1) < v; } returns also false, because 0 is the minimum unsigned integer. – Serge Ballesta May 19 '15 at 18:53
  • 1
    http://www.cl.cam.ac.uk/research/security/ctsrd/pdfs/201503-asplos2015-cheri-cmachine.pdf – Hans Passant May 19 '15 at 20:54
  • The integral constant 0 or other null pointer constants (such as `nullptr`) will be converted into the appropriate *NULL* value for the system. The *NULL* value is supposed to be a location at which no object can be created, and does not mean the machine register that holds the *NULL* value is all bits 0. On such a system, incrementing the *NULL* value may trigger a hardware trap. – jxh May 19 '15 at 22:41
  • @jxh : I already know that one can *imagine* a sytem where null pointer arythmetic would trigger a hardware trap, but I would like an real example, or at least references – Serge Ballesta May 20 '15 at 06:43
  • @SergeBallesta Maybe http://c-faq.com/null/machexamp.html might help – Drax May 20 '15 at 08:41
  • Easy. Make nullptr be represented as "all bits set" bit pattern. Then incrementing it will cause the overflow flag to be set in the hardware, which violates assumptions needed for further computations. – n. m. could be an AI May 20 '15 at 09:01
  • @Drax : It indeed gives references on odd hardware. It seems more focused on *different representations for pointers to different types*, but the part on *nonzero null pointers* is at least a partial answer to my question. Make it an answer, and I will upvote it. – Serge Ballesta May 20 '15 at 09:19
  • A pointer variable is still a variable. It resides in a location in memory. When I increment this variable, the memory locations that contain the value are changed, not what the pointer points to and certainly not nullptr. So I am missing how the behavior can be undefined. I'm no language lawyer but it seems this rule was added for some future day where someone would need to use it to cover some really weird behavior. But again variables live in memory, adding 1 is at least deterministic, how can this realistically be UB? – Ron Kuper May 20 '15 at 12:02
  • @HansPassant : It took me some time to read and understand the referenced article. It indeed proves that future versions of compilers may even trap facing null pointer arithmetic. As of now, it is the second best answer, and if you do not make it an answer, I'll do for future reference. But Jeremy's answer is exactly what I asked for. – Serge Ballesta May 20 '15 at 15:34

4 Answers4

7

How about this example:

int main(int argc, char* argv[])
{
    int a[] = { 111, 222 };

    int *p = (argc > 1) ? &a[0] : nullptr;
    p++;
    p--;

    return (p == nullptr);
}

At face value, this code says: 'If there are any command line arguments, initialise p to point to the first member of a[], otherwise initialise it to null. Then increment it, then decrement it, and tell me if it's null.'

On the face of it this should return '0' (indicating p is non-null) if we supply a command line argument, and '1' (indicating null) if we don't. Note that at no point do we dereference p, and if we supply an argument then p always points within the bounds of a[].

Compiling with the command line clang -S --std=c++11 -O2 nulltest.cpp (Cygwin clang 3.5.1) yields the following generated code:

    .text
    .def     main;
    .scl    2;
    .type   32;
    .endef
    .globl  main
    .align  16, 0x90
main:                                   # @main
.Ltmp0:
.seh_proc main
# BB#0:
    pushq   %rbp
.Ltmp1:
    .seh_pushreg 5
    movq    %rsp, %rbp
.Ltmp2:
    .seh_setframe 5, 0
.Ltmp3:
    .seh_endprologue
    callq   __main
    xorl    %eax, %eax
    popq    %rbp
    retq
.Leh_func_end0:
.Ltmp4:
    .seh_endproc

This code says 'return 0'. It doesn't even bother to check the number of command line args.

(And interestingly, commenting out the decrement has no effect on the generated code.)

Jeremy
  • 5,055
  • 1
  • 28
  • 44
  • Very nice ! It seems that the `-std=c++11` causes the code to break. Without it it does not exhibit the unexpected behaviour. But this code is small, explicit, shows the compiled form with nice explanation. – Serge Ballesta May 20 '15 at 15:38
6

Extracted from http://c-faq.com/null/machexamp.html:

Q: Seriously, have any actual machines really used nonzero null pointers, or different representations for pointers to different types?

A: The Prime 50 series used segment 07777, offset 0 for the null pointer, at least for PL/I. Later models used segment 0, offset 0 for null pointers in C, necessitating new instructions such as TCNP (Test C Null Pointer), evidently as a sop to [footnote] all the extant poorly-written C code which made incorrect assumptions. Older, word-addressed Prime machines were also notorious for requiring larger byte pointers (char *'s) than word pointers (int *'s).

The Eclipse MV series from Data General has three architecturally supported pointer formats (word, byte, and bit pointers), two of which are used by C compilers: byte pointers for char * and void *, and word pointers for everything else. For historical reasons during the evolution of the 32-bit MV line from the 16-bit Nova line, word pointers and byte pointers had the offset, indirection, and ring protection bits in different places in the word. Passing a mismatched pointer format to a function resulted in protection faults. Eventually, the MV C compiler added many compatibility options to try to deal with code that had pointer type mismatch errors.

Some Honeywell-Bull mainframes use the bit pattern 06000 for (internal) null pointers.

The CDC Cyber 180 Series has 48-bit pointers consisting of a ring, segment, and offset. Most users (in ring 11) have null pointers of 0xB00000000000. It was common on old CDC ones-complement machines to use an all-one-bits word as a special flag for all kinds of data, including invalid addresses.

The old HP 3000 series uses a different addressing scheme for byte addresses than for word addresses; like several of the machines above it therefore uses different representations for char * and void * pointers than for other pointers.

The Symbolics Lisp Machine, a tagged architecture, does not even have conventional numeric pointers; it uses the pair <NIL, 0> (basically a nonexistent <object, offset> handle) as a C null pointer.

Depending on the ``memory model'' in use, 8086-family processors (PC compatibles) may use 16-bit data pointers and 32-bit function pointers, or vice versa.

Some 64-bit Cray machines represent int * in the lower 48 bits of a word; char * additionally uses some of the upper 16 bits to indicate a byte address within a word.

Given that those null pointers have a weird bit pattern representation in the quoted machines, the code you put:

int *p = nullptr;
p++;

would not give the value most people would expect (0 + sizeof(*p)).

Instead you would have a value based on your machine specific nullptr bit pattern (except if the compiler has a special case for null pointer arithmetic but since that is not mandated by the standard you'll most likely face Undefined Behaviour with "visible" concrete effect).

Nisse Engström
  • 4,738
  • 23
  • 27
  • 42
Drax
  • 12,682
  • 7
  • 45
  • 85
  • How exactly is this behaviour undefined then? The `nullptr` isn't defined to be 0. So an increment of the nullptr isn't defined to be any value right? It might be undefined in the sense that it doesn't do what one would expect it to do. But according to the standard all of this is perfectly defined. – laurisvr May 20 '15 at 19:33
  • @laurisvr `§5.7/5: If both the pointer operand and the result point to elements of the same array object, or one past the last element of the array object, the evaluation shall not produce an overflow; otherwise, the behaviour is undefined.` The result of that operation is explicitly undefined because the standard says so, it doesn't mean you can't predict it based on your platform informations :) – Drax May 21 '15 at 08:24
  • Yes I'm familiar with that part. I've given my view on it [here](http://stackoverflow.com/a/30343753/3243563) Doesn't the undefined part here refer to the evaluation? – laurisvr May 21 '15 at 08:30
  • @laurisvr If this quote from @nwp's comment on the OP's question is true: `I vaguely remember something about weird hardware that has different memory for strings and other stuff and if you create a pointer pointing to the wrong thing without dereferencing you got a segfault already. This is the reason why you are only allowed to have pointers point to real memory (besides the nullptr of course), so C++ works on that weird hardware.` I'd say even the operation has undefined behaviour. In all cases someone doing this is walking down a dangerous, ambiguous, undefined path :) – Drax May 21 '15 at 08:39
  • I see I screwed up a bit on my previous comment. I hope you got the point still:). I think it very well might be true:). And again, it would indeed be ridiculous to use a pointer like this. Because the point of a pointer is, well to point. I know I'm going in circles. But I stick to my case, you can treat a pointer like an int(with nullptr not being 0, but a value specified by the enviroment). And you're fine:) You shouldn't evaluate it though. – laurisvr May 21 '15 at 08:41
  • @laurisvr I think you would have to type cast it to an `intptr_t` or some equivalent before that is valid because the fact that the type is a pointer(and thus that it is supposed to point to a memory address) might induce the compiler to treat it in a way that would be the reason of the undefined behaviour in the standard (e.g the potential segfault even without dereferencing in the previous comment) while as you say you'll probably be all fine if you treat those values as integers. I think the key here is the type you're dealing with, `0` is an `int` until cast or assigned to a pointer :) – Drax May 21 '15 at 08:48
  • I've never heard of a segfault without a dereference. But I might be mistaken:). And after all it's all just speculations, since i think the standard in this particular case is slightly ambiguous. – laurisvr May 21 '15 at 08:52
  • I will post a question about the specif ambiguity. Maybe there's someone out there who does know:) – laurisvr May 21 '15 at 09:03
  • @laurisvr For completeness i'll link the question you posted : http://stackoverflow.com/questions/30369357/ambiguity-in-the-standard-on-undefined-behaviour-of-out-of-range-pointer – Drax May 21 '15 at 15:22
2

An ideal C implementation would, when not being used for kinds of systems programming that would require using pointers which the programmer knew to have meaning but the compiler did not, ensure that every pointer was either valid or was recognizable as invalid, and would trap any time code either tried to dereference an invalid pointer (including null) or used illegitimate means to created something that wasn't a valid pointer but might be mistaken for one. On most platforms, having generated code enforce such a constraint in all situations would be quite expensive, but guarding against many common erroneous scenarios is much cheaper.

On many platforms, it is relatively inexpensive to have the compiler generate for *foo=23 code equivalent to if (!foo) NULL_POINTER_TRAP(); else *foo=23;. Even primitive compilers in the 1980s often had an option for that. The usefulness of such trapping may be largely lost, however, if compilers allow a null pointer to be incremented in such a fashion that it is no longer recognizable as a null pointer. Consequently, a good compiler should, when error-trapping is enabled, replace foo++; with foo = (foo ? foo+1 : (NULL_POINTER_TRAP(),0));. Arguably, the real "billion dollar mistake" wasn't inventing null pointers, but lay rather with the fact that some compilers would trap direct null-pointer stores, but would not trap null-pointer arithmetic.

Given that an ideal compiler would trap on an attempt to increment a null pointer (many compilers fail to do so for reasons of performance rather than semantics), I can see no reason why code should expect such an increment to have meaning. In just about any case where a programmer might expect a compiler to assign a meaning to such a construct [e.g. ((char*)0)+5 yielding a pointer to address 5], it would be better for the programmer to instead use some other construct to form the desired pointer (e.g. ((char*)5)).

supercat
  • 77,689
  • 9
  • 166
  • 211
1

This is just for completion, but the link proposed by @HansPassant in comment really deserves to be cited as an answer.

All references are here, following is just some extracts

This article is about a new memory-safe interpretation of the C abstract machine that provides stronger protection to benefit security and debugging ... [Writers] demonstrate that it is possible for a memory-safe implementation of C to support not just the C abstract machine as specified, but a broader interpretation that is still compatible with existing code. By enforcing the model in hardware, our implementation provides memory safety that can be used to provide high-level security properties for C ...

[Implementation] memory capabilities are represented as the triplet (base, bound, permissions), which is loosely packed into a 256-bit value. Here base provides an offset into a virtual address region, and bound limits the size of the region accessed ... Special capability load and store instructions allow capabilities to be spilled to the stack or stored in data structures, just like pointers ... with the caveat that pointer subtraction is not allowed.

The addition of permissions allows capabilities to be tokens granting certain rights to the referenced memory. For example, a memory capability may have permissions to read data and capabilities, but not to write them (or just to write data but not capabilities). Attempting any of the operations that is not permitted will cause a trap.

[The] results confirm that it is possible to retain the strong semantics of a capability-system memory model (which provides non-bypassable memory protection) without sacrificing the advantages of a low-level language.

(emphasize mine)

That means that even if it is not an operational compiler, researches exists to build one that could trap on incorrect pointer usages, and have already been published.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252