Does &((struct name *)NULL -> b) cause undefined behaviour in C11?
Asked Answered
P

6

59

Code sample:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}

Does this cause undefined behaviour? We could debate whether it "dereferences null", however C11 doesn't define the term "dereference".

6.5.3.2/4 clearly says that using * on a null pointer causes undefined behaviour; however it doesn't say the same for -> and also it does not define a -> b as being (*a).b ; it has separate definitions for each operator.

The semantics of -> in 6.5.2.3/4 says:

A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. The value is that of the named member of the object to which the first expression points, and is an lvalue.

However, NULL does not point to an object, so the second sentence seems underspecified.

Also relevant might be 6.5.3.2/1:

Constraints:

The operand of the unary & operator shall be either a function designator, the result of a [] or unary * operator, or an lvalue that designates an object that is not a bit-field and is not declared with the register storage-class specifier.

However I feel that the bolded text is defective and should read lvalue that potentially designates an object , as per 6.3.2.1/1 (definition of lvalue) -- C99 messed up the definition of lvalue, so C11 had to rewrite it and perhaps this section got missed.

6.3.2.1/1 does say:

An lvalue is an expression (with an object type other than void) that potentially designates an object; if an lvalue does not designate an object when it is evaluated, the behavior is undefined

however the & operator does evaluate its operand. (It doesn't access the stored value but that is different).

This long chain of reasoning seems to suggest that the code causes UB however it is fairly tenuous and it's not clear to me what the writers of the Standard intended. If in fact they intended anything, rather than leaving it up to us to debate :)

Pew answered 13/11, 2014 at 10:27 Comment(8)
See also, of course, Wikipedia's page on offsetof.Unbiased
@Unbiased doesn't seem to offer any insight that the C standard doesn't :)Pew
Note for paragraph 6.5.2.3.4 says: "96) If &E is a valid pointer expression (where & is the ‘‘address-of ’’ operator, which generates a pointer to its operand), the expression (&E)->MOS is the same as E.MOS." I think this covers the relationship between . and ->.Autogamy
@MattMcNabb What we can take from that note is that a->b is maybe not as separate from (*a).b as you assume.Autogamy
@Autogamy I don't see it; that is saying that if the above expression is valid, then (&(((struct name *)NULL)->b))->b is the same as (((struct name *)NULL)->b)->b. This note only applies when E has a struct type, but here E is an intPew
@Unbiased A circular reference has been created, as Wikipedia's page now links here.Housecarl
In C99 this is a permissible constant expression and nothing is being de-referenced or accessed. That is assuming that NULL is simply 0 and not some other value.Roselleroselyn
Per C if "a null pointer is guaranteed to compare unequal to a pointer to any object" and "any two null pointers shall compare equal", then a null pointer is not a pointer to object. Hence, "a postfix expression followed by the -> operator and an identifier" does not designate a member of a structure object. Hence, ((struct name *)NULL)->b violates semantics of the -> operator.Lauree
A
24

From a lawyer point of view, the expression &(((struct name *)NULL)->b); should lead to UB, since you could not find a path in which there would be no UB. IMHO the root cause is that at a moment you apply the -> operator on an expression that does not point to an object.

From a compiler point of view, assuming the compiler programmer was not overcomplicated, it is clear that the expression returns the same value as offsetof(name, b) would, and I'm pretty sure that provided it is compiled without error any existing compiler will give that result.

As written, we could not blame a compiler that would note that in the inner part you use operator -> on an expression than cannot point to an object (since it is null) and issue a warning or an error.

My conclusion is that until there is a special paragraph saying that provided it is only to take its address it is legal do dereference a null pointer, this expression is not legal C.

Apothem answered 13/11, 2014 at 11:10 Comment(21)
Or they use it as a red flag meaning "this code cannot be reached". You know, for optimization: The fastest code is code that is not there.Pipestone
offsetof is an implementation-specific macro. Whatever it does is specific to the compiler that provides it. You cannot generalize from its definition for one or many compilers to a language requirement. There are many things that standard headers do that do not have well-defined behavior in general; these things rely on knowledge of the particular compiler, which is why they ship with the compiler.Kentiga
@PeteBecker: I totally agree with that. I'm afraid that my english is not good enough for such precise concepts. May be returns same thing (what I really meant), would be better ? Feel free to edit if you find a better sentence ...Apothem
@SergeBallesta - my point (which I somewhat obscured) is that offsetof is not necessarily implemented that way.Kentiga
@PeteBecker: It's now clear in this comment, and I think my current phrasing does not imply that.Apothem
@Deduplicator: I really despise that concept of "optimization"; it's especially horrid with integer arithmetic (IMHO, the Standards Committee should have narrowed down a list of allowable behaviors for integer overflow which would include having variables take on arbitrary or "impossible" values, but not full UB) but can be bad with pointer accesses as well. While it's entirely reasonable that the standard allows unconstrained UB when a null-pointer dereference occurs, some implementations may specify that particular behaviors occur. Some embedded-systems contexts define...Foxworth
...a null-pointer dereference as accessing physical address zero, and on some such systems there is no other way to access that address. Even if the standard does not specify a behavior for a null-pointer dereference, a system's documentation might. A compiler which tries to "get clever" with Undefined Behavior in such cases could end up "optimizing" code whose behavior would be usefully defined by other compilers for the same hardware platform.Foxworth
@Ángel : in my opinion, assuming the compiler programmer was not overcomplicated meant that NULL was address 0, and there was no special processing about it as most current compiler do.Apothem
@Foxworth There could be systems with address zero, and null pointer does not point to address 0. You can still access address zero by forming a pointer to address 1 and decrementing it.Pew
@MattMcNabb: The Standard imposes no requirement that (char*)(n1+n2) bear any relation to n1+(char*)n2, nor that (char*)n1 have any meaning for any non-zero or non-constant n1 which is not the result of a previous pointer-to-integer conversion. An implementation would be allowed to allocate a bit for every byte of memory to keep track of "has this address ever been coerced into an integer", set the bit associated with an address whenever a pointer to that address was converted to an integer, and launch nuclear missiles if an int is converted to a pointer whose bit isn't set.Foxworth
@Foxworth If n2 is a char * and n1 an integer then those expressions are the same. Otherwise it is ill-formed or implementation-defined. I don't see what that has to do with my previous comment either.Pew
@MattMcNabb: I don't know that forming a pointer to address 1 (do you mean casting an integer value 1 to a pointer?) and decrementing should be expected to be any more portable than setting an integer variable to zero and casting that to a pointer. Neither is guaranteed to work, and either may work on implementations where the other would fail.Foxworth
@Foxworth if there is known to be an object (of at least 2 bytes in size) at address 0 then it is fine to cast 1 to a pointer and decrement. Assuming of course that the implementation's definition of casting 1 to a pointer is to form a pointer to address 1. Casting 0 to a pointer is different to casting non-zero to a pointer; it forms a null pointer which may or may not have the same representation as a pointer to address 0.Pew
@MattMcNabb: If uintptr_t u=0; char *p=(char*)u; would store in p a null pointer that didn't identify address zero, I see no reason to believe that (char*)1 would yield a pointer to address 1. If a null pointer's bit pattern would identify address 0xDEADBEEF, and casting a non-constant integer value of zero to a pointer would yield that, I would think the easiest standard-conforming way to achieve that would be to say that (char*)N yields a pointer to either N+0xDEADBEEF or N^0xDEADBEEF; so a pointer to address 1 would require using an integer value other than 1.Foxworth
@Foxworth it's implementation-defined. Your suggestion makes no sense to me at all. Can you name any implementation ever that didn't have (char *)1 either trap or generate a pointer to address 1 ?Pew
@MattMcNabb: Implementations where null isn't all-bits zero are rare; I know some exist, but I don't know of any in particular; I have certainly heard of implementations using lightly-scrambled opaque numbers to encode pointers. I think Windows used to do that with things like GDI handles. I've also seen machines which didn't have byte addressable memory, but did have ways of writing only the top half or bottom half of a 16-bit word. The only C compiler I've used for such a machine had a 16-bit char type, but later processors in that family included instructions...Foxworth
...which, given a word address and a char-based offset, would automatically read or write the top half or bottom half of the address (base+ofs/2) as appropriate; the most natural form of char* on such a machine would combine a word pointer and an offset, and it's not clear in which order the two parts should appear.Foxworth
@Foxworth the representation of the pointer doesn't matter; what matters is that on a system where address 1 is a valid address, it is possible to have a pointer that points to that addressPew
@Ángel No, C does not allow NULL != 0 to be true. It allows the memory representation of a NULL pointer to differ from zero bits (in particular, memset(0) is not a standard-safe way to initialize pointers to NULL), but the NULL constant value and NULL pointers must compare equal to zero.Birdcage
@ConradMeyer: I can't find my own comment here (posted circa 4 years ago?) to review what I said, so mostly guessing what might have been the gist. C standard says that NULL must compare equal to 0, but there are cases where you can't simply replace a NULL with a 0, but you actually need something like ((void*)0)) (and not doing so might mean a crash). For example, when passed as a variadic parameter (or simply with no prototype) on an architecture where integers and pointers have different sizes.Scopp
@Ángel Yeah, no worries. I don't recall the exact details of the comment now either, but I think it was on the topic of memory representation of NULL, rather than its type. The type caveat of NULL you point out is a good one to keep in mind. (The C standard does not define NULL to be a pointer type -- just a "null pointer constant," which is permitted to be a plain integer or a pointer.) This mostly matters for, as you say, variadic parameters. Cheers.Birdcage
A
18

Yes, this use of -> has undefined behavior in the direct sense of the English term undefined.

The behavior is only defined if the first expression points to an object and not defined (=undefined) otherwise. In general you shouldn't search more in the term undefined, it means just that: the standard doesn't provide a meaning for your code. (Sometimes it points explicitly to such situations that it doesn't define, but this doesn't change the general meaning of the term.)

This is a slackness that is introduced to help compiler builders to deal with things. They may defined a behavior, even for the code that you are presenting. In particular, for a compiler implementation it is perfectly fine to use such code or similar for the offsetof macro. Making this code a constraint violation would block that path for compiler implementations.

Aflutter answered 13/11, 2014 at 11:7 Comment(2)
well, I mean undefined behaviour as defined by 3.4.3Pew
@MattMcNabb, me too, just read it for this case as "behavior, upon use of a nonportable program construct for which this International Standard imposes no requirements ". This term of "undefined behavior" should not be mystified, it stands for itself.Aflutter
B
12

Let's start with the indirection operator *:

6.5.3.2 p4: The unary * operator denotes indirection. If the operand points to a function, the result is a function designator; if it points to an object, the result is an lvalue designating the object. If the operand has type "pointer to type", the result has type "type". If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined. 102)

*E, where E is a null pointer, is undefined behavior.

There is a footnote that states:

102) Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is always true that if E is a function designator or an lvalue that is a valid operand of the unary & operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points.

Which means that &*E, where E is NULL, is defined, but the question is whether the same is true for &(*E).m, where E is a null pointer and its type is a struct that has a member m?

C Standard doesn't define that behavior.

If it were defined, new problems would arise, one of which is listed below. C Standard is correct to keep it undefined, and provides a macro offsetof that handles the problem internally.

6.3.2.3 Pointers

  1. An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. 66) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

This means that an integer constant expression with the value 0 is converted to a null pointer constant.

But the value of a null pointer constant is not defined as 0. The value is implementation defined.

7.19 Common definitions

  1. The macros are NULL which expands to an implementation-defined null pointer constant

This means C allows an implementation where the null pointer will have a value where all bits are set and using member access on that value will result in an overflow which is undefined behavior

Another problem is how do you evaluate &(*E).m? Do the brackets apply and is * evaluated first. Keeping it undefined solves this problem.

Boadicea answered 13/11, 2014 at 15:23 Comment(0)
P
5

First, let's establish that we need a pointer to an object:

6.5.2.3 Structure and union members

4 A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. The value is that of the named member of the object to which the first expression points, and is an lvalue.96) If the first expression is a pointer to a qualified type, the result has the so-qualified version of the type of the designated member.

Unfortunately, no null pointer ever points to an object.

6.3.2.3 Pointers

3 An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant.66) If a null pointer constant is converted to a pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal to a pointer to any object or function.

Result: Undefined Behavior.

As a side-note, some other things to chew over:

6.3.2.3 Pointers

4 Conversion of a null pointer to another pointer type yields a null pointer of that type. Any two null pointers shall compare equal.
5 An integer may be converted to any pointer type. Except as previously specified, the result is implementation-defined, might not be correctly aligned, might not point to an entity of the referenced type, and might be a trap representation.67)
6 Any pointer type may be converted to an integer type. Except as previously specified, the result is implementation-defined. If the result cannot be represented in the integer type, the behavior is undefined. The result need not be in the range of values of any integer type.

67) The mapping functions for converting a pointer to an integer or an integer to a pointer are intended to be consistent with the addressing structure of the execution environment.

So even if the UB should happen to be benign this time, it might still result in some totally unexpected number.

Pipestone answered 13/11, 2014 at 21:57 Comment(1)
Regarding conversion: that's what (u)intptr_t is for.Wry
F
0

Nothing in the C standard would impose any requirements on what a system could do with the expression. It would, when the standard was written, have been perfectly reasonable for it to to cause the following sequence of events at runtime:

  1. Code loads a null pointer into the addressing unit
  2. Code asks the addressing unit to add the offset of field b.
  3. The addressing unit trigger a trap when attempting to add an integer to a null pointer (which should for robustness be a run-time trap, even though many systems don't catch it)
  4. The system starts executing essentially random code after being dispatched through a trap vector that was never set because code to set it would have wasted been a waste of memory, as addressing traps shouldn't occur.

The very essence of what Undefined Behavior meant at the time.

Note that most of the compilers that have appeared since the early days of C would regard the address of a member of an object located at a constant address as being a compile-time constant, but I don't think such behavior was mandated then, nor has anything been added to the standard which would mandate that compile-time address calculations involving null pointers be defined in cases where run-time calculations would not.

Foxworth answered 22/4, 2015 at 23:56 Comment(0)
P
-2

No. Let's take this apart:

&(((struct name *)NULL)->b);

is the same as:

struct name * ptr = NULL;
&(ptr->b);

The first line is obviously valid and well defined.

In the second line, we calculate the address of a field relative to the address 0x0 which is perfectly legal as well. The Amiga, for example, had the pointer to the kernel in the address 0x4. So you could use a method like this to call kernel functions.

In fact, the same approach is used on the C macro offsetof (wikipedia):

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

So the confusion here revolves around the fact that NULL pointers are scary. But from a compiler and standard point of view, the expression is legal in C (C++ is a different beast since you can overload the & operator).

Paulita answered 13/11, 2014 at 12:17 Comment(20)
This question is about what the C standard guarantees for the original code (hence the language-lawyer tag). Compilers may define behaviour for what is not defined by the standard, so a particular compiler's implementation of offsetof does not prove anything.Pew
@MattMcNabb: There is nothing in the standard which forbids it. The NULL pointer does point to a valid address in memory for some systems while others make the first code page unaccessible to help catching bugs. That means the statement "NULL does not point to an object" isn't accurate as such. By convention on many systems, it doesn't. But if I don't have memory protection, then *(int*)NULL will return the contents of the first few bytes of memory.Paulita
You're still talking about things that are not defined by the standard (UB doesn't mean "forbidden", it means that there is not defined behaviour). Although it may be a valid point that there could be an object living at NULL.Pew
My argument is that it can't be undefined since it's a simple offset operation on an absolute address. The address is fixed (as opposed to, say, an address returned my malloc()). But the address 0x0 isn't special in any way from the point of view of a compiler. Which is the root cause of all those null pointer problems. The standard differentiates in some places but for the -> operator, NULL is just an address like any other. Hence the standard doesn't forbid it. From which I conclude that the behavior is legal and well defined.Paulita
The standard does treat null pointers differently to other pointers; e.g. it explicitly says that *(int *)NULL is undefined, and so is strlen(NULL), and memcpy(NULL, NULL, 0);Pew
And anyway, pointer arithmetic on the null pointer is undefined. Whether a null pointer is an absolute address or not.Pipestone
@Deduplicator: Be careful with context here. NULL pointer is (void*)0. Since void doesn't have a well defined size, arithmetic isn't defined. But (struct name*)NULL is a typed pointer.Paulita
@AaronDigulla: I'm not speaking of any specific null pointer there, and especially not of NULL (which can be 0 0L 0LL and many others as well).Pipestone
@MattMcNabb: Any object whose address may be taken must legally be identifiable via a non-NULL pointer. Nothing would forbid a compiler from storing something whose address is never taken in such a way that it would be hit by code trying to dereference a null pointer, but such an object could not be accessed via pointer without invoking Undefined Behavior.Foxworth
@Deduplicator: In that case, your statement is wrong. The memory address 0x0 is not different from any other, so why would adding an offset to it be illegal?Paulita
@AaronDigulla: The difference is that a null pointer shouldn't be considered to point to address 0. Semantically, it doesn't point to any valid address. (Wherever it could point to, no object can exist.)Chromophore
... and it might not even be implemented as the all-zero bit pattern. This answer here is just plain wrong: a null pointer and a pointer with the value of 0 (all bits in the representation are off) are two things that are clearly differtiated by the standard.Aflutter
@cHao: In my world, there is a C standard and reality. If the standard says something that I can't find in reality, reality always wins. I have my doubts it would be possible to write a C compiler that defines NULL as anything != 0. My main issue is that a lot of code relies that NULL == 0 in some form. So even if the standard says "you can do it differently" doesn't mean you actually can.Paulita
@JensGustedt: No matter to which bit pattern represents NULL, it's always possible to add a number to this pattern to get a new number. For the CPU, it's always a "add number to address" kind of operation. As long as you don't actually try to access the address (which the statement above doesn't and the standard clearly distinguishes this case), it's a valid statement with well defined behavior for any possible value of NULL.Paulita
@AaronDigulla: Then a lot of code is broken. NULL is not 0. (type*)0 just (unfortunately) happens to be how the standard spells "null pointer", and only when it's a constant expression. Even (type*)n, where n == 0, is not a null pointer; it's a pointer to address 0, which is an entirely different thing (even if your compiler conflates them). The actual value of a null pointer constant could be anything -- like, say, 0x8000000000000000 (an invalid address on x86-64 CPUs), which would give you "offsets" of 2^63 or so.Chromophore
s/is broken/isn't as portable as it thinks it is/Chromophore
@AaronDigulla, I can't point you to one off head, by I remember that there are C implementations that have null pointers that are not the value 0, even ones that have several different values for it. C is also made for platforms with segmented memory, and a pointer there might just not be just a simple "integer" value, but a combination of segment and offset or stuff like that. This is exactly the reason why the C standard introduced the offsetof macro, it is needed because this 0-> trick doesn't work everywhere.Aflutter
@cHao. "null pointer shouldn't be considered to point to address 0". I think that's the best point. On many many systems it does but you are right - we shouldn't consider it that way and treat it as a special symbol. Looking over to C++, on the one hand helped by making nullptr a formal symbol and then confuses matters again by casting it to 0 at every opportunity (IMHO).Fierro
@cHao: There was a change in a recent C standard so that converting an integer with a value of 0 to a data pointer is now guaranteed to produce a null pointer. Which doesn't need to be a pointer with all bits zero.Bramante
@gnasher729: The 0 must be an integer constant expression, even in C11. If you have a variable n, and can legally say n = 0;, (type*)n is not guaranteed to be a null pointer. If it were, you'd have to jump through hoops to get an actual pointer to address 0.Chromophore

© 2022 - 2024 — McMap. All rights reserved.