Null Pointer Dereferencing Causes Undefined Behavior
software.intel.com
software.intel.com
#define offsetof(st, m) ((size_t)(&((st *)0)->m))
That expression has undefined behavior, since it dereferences a null pointer. That doesn't mean it's an invalid definition of offsetof for a given implementation.Saying that it has "undefined behavior" means that the C language standard says nothing about how it behaves. If its behavior for the current compiler happens to satisfy the language requirements for offsetof, then it's a legitimate implementation. Code that implements the standard library is free to do whatever it likes, as long as the resulting behavior is correct.
Part of the problem, I think, is the way the question is phrased. Quoting the linked article:
> The programmers' community divided into two camps. The first claimed with confidence that it wasn't legal while the others were as sure saying that it was.
The word "legal" doesn't even occur in the C standard. Some program constructs violate constraints or syntax rules; any such violation requires a compile-time diagnostic (which may be a non-fatal error). Other constructs have "undefined behavior"; the standard places no restrictions on what compilers can do for such constructs. Only a `#error` directive actually requires a program to be rejected.
If a unary "&" is applied directly to the result of a unary "" operator, then neither is evaluated. But in this case, the "&" is being applied to "((st )0)->m". "x->y" is syntactic sugar for "(x).y", so this is equivalent to If a unary "&" is applied directly to the result of a unary "" operator, then neither is evaluated. But in this case, the "&" is being applied to "((st )0)->m". "x->y" is syntactic sugar for "(x).y", so this is equivalent to "((st )0)).m)".
A compiler will very likely optimize the expression so it doesn't generate machine code to dereference the null pointer. And in fact using this definition for the offsetof macro depends on that optimization, and on the compiler treating a null pointer as address 0.
But it does list some suggestions on what could happen, one of which is "behaving during translation or program execution in a documented manner characteristic of the environment", which is what most C programmers expect of the language. Violating this implicit assumption is what makes compilers user-hostile; choosing the worst possible behaviour, just because the standard does not define it, is never the right thing to do.
Related reading:
http://blog.metaobject.com/2014/04/cc-osmartass.html
Undefined is not the same as implementation-defined. Yes, a given implementation is free to do whatever it wants, but code that includes a null pointer dereference is not C code.
See http://blog.regehr.org/archives/761 for a sarcastic take on the problem, http://blog.regehr.org/archives/880 for a more serious description of the problem, and http://blog.regehr.org/archives/1180 for the "Proposal for a Friendly Dialect of C" that proposes to actually solve it.
This is ironically mainly a problem because older compilers acted normally on some undefined code. So more modern optimising compilers will expect defined behaviour and so forth, leading to new bugs reported in old code. These bugs were always present, they just appear only now.
It's not the compiler trying to exploit the undefined behaviour deliberately; it's that the compiler is expecting the behaviour to be defined and behaving accordingly. So if you have
int x = *foo;
if (foo == NULL) {
ERROR();
}
then clearly in valid C, the if statement will never be taken, so it's safe to remove it. This is something you'd want to happen in most cases, it just so happens that in the above case you might not want it to.In terms of whether the fraction of a percent is worth it, well the people who do most of the development of the compilers is large companies like Google, and they don't do it out of the goodness of their hearts, they do it because the benefit of having high performance compilers is worth the expenditure.
In terms of whether other languages are better, of course they are for many tasks, but C is king of the embedded world, where implementing a full Rust/Java/Node/language of day compiler is too much work. Then for many high performance tasks it's very difficult to beat the sheer performance of C or C++. If a task written in C or C++ runs just 20% faster than Java for Google/Microsoft/etc's 40,000 computer cluster, then that's 7000 computers that you don't need to buy.
For the bit about GPUs, well that's kinda an aside but GPUs are useful for some things and not others and appealing to their use suggests ignorance. People use C for lots of things, and 'high performance bit-twiddling' isn't a standard one. GPUs are terrible at most tasks.
The proposal for a friendly dialect of C is useful, but I suspect that the smarter way of doing it is the [0] undefined behaviour sanitiser, which produces an instrumented binary that errors when it reaches undefined behaviour.
The developer, upon finding a new bug in their code, compiles, runs, gets an error, fixes, rinses, repeats.
[0] - https://developerblog.redhat.com/2014/10/16/gcc-undefined-be...
As they should. C and C code predates all of the standards, so ALL that C code was, at the time, "undefined". There were still working compilers and working code, how could that possibly be??
You are framing the problem in terms that make the situation sound almost reasonable: given that this code is invalid, what should the compiler do? In fact, the question I asked is whether particular code should be invalid.
The choices made by the standards committees so far on this question — the one you are trying to divert attention from — have proven catastrophic. This is far from being a “misrepresentation”; it is patently obvious. We have reached a situation where every non-trivial execution of GCC (which is written in C, remember) executes undefined behavior; and where even zlib, which has gone nine years without a security hole, is full of undefined behavior.
Yes it was. Or at least, that's the constituency C currently has - and understandably C continues to serve it. There needs to be a language for that kind of people, and that language is C.
If you don't want that, stop using C. It's not hard.
What are my options if I want a simple, easy to understand language that is close to the execution model of current machines?
Also, there are still Unisys Clearpath/MCP systems in use.
Your suggested change would amount to a delta on the language spec. That would probably be unprecedented.
All it's doing is saying that "my compiler uses the freedom to do anything that undefined behavior grants to do something sane".
By definition, anything that would be broken by such a change was already undefined behavior.
So really, the "something sane" that you are talking about should be issuing an error or warning.
The reasoning for having it an extension is mainly so that current code can be trivially ported.
If you don't... maybe. But do you realistically think "paranoid C" would ever get its library ecosystem up to a level where it matched e.g. OCaml? Do you think embedded vendors would be more likely to support it (the other big use case for C)? If you're going to be switching libraries anyway, OCaml is a much nicer language to work in.
(On a side note. I like OCaml, but don't like the syntax. I wish there was a language out there that was basically OCaml with c-like syntax. Or rather, I wish language syntax in general was less coupled with language semantics. It should be possible, and is, to, for example, automatically convert between Pythonic-style indentation and C-style braces and lisp-esque braces. Define the language in terms of an AST and leave the syntax up to the editor.)
[1] http://stackoverflow.com/questions/12887700/can-code-that-is...
That's why IMO people saying that the compiler shouldn't bother with these 'excessive' optimisations are wrong. It might be worthless for them and their programs, but might be valuable for others. Don't force your priorities upon other people.
... &p[1].foo ...
for (int i = 0; i < ...; i++) {
bar += p[0].foo;
}
The compiler wants to transform the loop as follows: auto tmp = p[0].foo;
for (int i = 0; i < ...; i++) {
bar += tmp;
}
Assume that the compiler cannot prove that the loop will execute at least once. Now observe that this transformation is correct if and only if the pointer p is dereferenceable (with the offset of the field foo applied). The optimization would miscompile a perfectly valid program if p were NULL and the loop ran zero times. And because of strict-aliasing rules, the only way that p would not be dereferenceable in this way is if it were NULL or one element beyond the end of an array.So the compiler wants to prove that p is not NULL upon entry to the loop. And in this program, it can--because of the address computation, the behavior is undefined if p is NULL or one element beyond the end of an array. Therefore, the compiler can safely assume that p is not NULL and perform the loop invariant code motion. In this program, it can only do so thanks to the rule about undefined behavior in address computations.
Note that I'm not aware of any compiler that actually does this, because the C++ standard committee seems to consider this undefined behavior an oversight in the spec (per the discussion linked there). But the OP successfully argues that it could, per a lawyer-y reading of the spec.
Edit: Argh, I missed the case in which p was one element beyond the end of an array. Updated.
if (max > 0) {
int i = max;
int tmp = p[0].foo;
do {
bar += tmp;
} while (--i);
}
I'd even argue the assembly is slightly better when interpreted this way, since branch prediction will tend execute the load or not as appropriate, rather than potentially taking a unnecessary TLB miss for a load that's never used.(I'm pretty sure POSIX does impose certain non-stupidity requirements on its underlying architecture, so there are things that are undefined in plain C that are guaranteed to work on any POSIX platform. But I don't know if this is one of them.)
As he showed, a null pointer cannot point to any object, and since a lvalue that does not designate an object gives undefined behavior, saying '* podhd' would clearly give undefined behavior when 'podhd' is a null pointer.
However, instead of '* podhd' evaluating to a non-existant object, we have 'podhd->line6' asking for the object some particular offset from a non-existant object. It's entirely possible that a sufficiently insane spec might actually define such an operation.
What the spec says on that point is the key. That's where the undefinedness must come from, but he doesn't talk about it or quote from the spec on '->'. He just assumes it, which leaves his original point unproven, even if it is correct.
So why would anybody want to define legal behavior for dereferencing a null pointer with '->'? Well, some people start college funds for children who have yet to be conceived. If non-existant people can have account balances, why can't non-existant objects have properties? It's crazy, but it's not crazy like Intecal, where syntax errors are legal statements that you actually use for productive purposes. http://catb.org/~esr/intercal/ick.htm#Syntax-Error See also the statement "COME FROM".
From my reading, he didn't even show that:
>> 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.
So a null pointer could point to an object, it just wouldn't compare equal to any pointer, even itself.
C standard 3.2.2.3: "An integral constant expression with the value 0, or such an expression cast to type void * , is called a null pointer constant."
C standard 4.1.5: "The macros are... NULL, which expands to an implementation-defined null pointer constant."
See [1] for more info.
[1]: http://stackoverflow.com/questions/2599207/can-a-conforming-...
There is no requirement for an integer-to-pointer conversion to retain the bit pattern of the integer value, any more than an integer-to-floating-point conversion is required to do so.
In most modern implementations, conversions among integer and pointer types of the same size are trivial, simply reinterpreting the bits as a value of a different type.
A conforming C or C++ compiler can use, for example, the bit pattern 0xFFFFFFFF to represent a null pointer. Converting a constant 0 to a pointer type would yield a pointer with the representation 0xFFFFFFFF. Converting a non-constant 0 to a pointer type does not necessarily yield a null pointer; the result is implementation-defined, and may be indeterminate garbage.
See also http://stackoverflow.com/questions/2597142/when-was-the-null...
struct usb_line6 *line6 = &podhd->line6;
The assumption is that this is OK so long as you don't try to access the contents of line6 without doing some upstream null checking first. This assumption is false. The very act of dereferencing is undefined, regardless of whether you access the resulting address or not.The assumption fails whether null is implemented as 0, 200, 0x8000000000000000, 0xffffffffffffffff.
struct usb_line6 *line6 = &podhd->line6;
Offset is calculated "below the hood". Pointer arithmetic is defined with arithmetic operands: +,--> and . are not arithmetic operands.
If you look at the definition of the -> and [], you might learn something!
[] operator doesn't have any place in this debate.
What @jwatte tried to say is that `->` works like this:
// foo is struct thingity*
&foo->bar == foo + offsetof(struct thingity, bar);
The `[]` does a similar thing: // s is char*
*(s+i) == s[i]It does however specify that [] operator is equivalent to pointer arithmetic. But we are talking ->,. operators and not [].
That becomes a problem with modern optimizing compilers, because they make assumptions about your code based on what's allowed according to the spec.
If this code were a part of some abstract "spherical horse in vacuum" project that may potentially build on esoteric platforms using random C compilers, then, yes, it might be worth re-factoring. In the context of Linux kernel project given the toolchain used the code is perfectly fine as is.
The debate is silly though, can't argue with that.
Code my team is working on at the moment was written for a completely different architecture than it is run on right now. Sudden platform shifts happen, like when tsunami affected Renesas' ability to manufacture chips used in automotive. It IS pragmatic to avoid code that can potentially introduce problems in the future. Perhaps not for every piece of software, but low level one? Definitely.
This is another reason I look forward to the day when C is a legacy language.
Although, I don't mind C. I work in it every day and I've comfortable with it. A colleague of mine (who works primarily in Python) teased me it's because I have developed a Stockholm syndrome.
However it doesn't help that C++ allows for a C subset.
We are several decades away from achieving that goal.
In fact I'm pretty sure a GCC update broke this exact type of code in the Linux kernel, by effectively removing null pointer checks after the optimizer used its knowledge of a pointer use later in a function to assume that the pointer must be valid and non-null (after all, if it had been null, the behavior would be undefined, and the compiler is allowed to assume no undefined behavior happens, which means the earlier null pointer check had been redundant).
I believe that got fixed in GCC to do what you'd expect, but that's just one example of the awesomeness of undefined behavior in C-based languages.
> When writing low-level code it's really handy to know
> the offset of a member in a struct.
C provides offsetof() in <stddef.h> for that purpose, precisely because it's not expressible in C without compiler support.GCC has __builtin_offsetof: https://gcc.gnu.org/onlinedocs/gcc/Offsetof.html
Eg the conversion (void *)0 might result in a value with all bits set and member access will overflow.
Another example would be hardware that raises a signal when encountering an invalid value during address calculations.
In practice, the address calculation will work as expected (ie I'm unaware of machines where it doesn't). However, an aggressively optimizing compiler migh treat an expression like foo->bar as assert(foo != NULL), which might lead to unintended consequences in worst-case scenarios.
In fact most modern optimising compilers do exactly that, and the article does refer to such a behaviour having had security implications in the past.
sizeof(&P->m_foo)
does NOT return the size of `m_foo` but the size of a pointer to an `m_foo` on the target architecture.Any compiler that generates anything other than an add instruction (or lea or similar) is broken, for rather unsubtle reasons.
Or to put it another way: dereferencing a null pointer may be undefined, but arithmetic on any pointer (including null) is well defined, and the code in question is arithmetic, not dereference.
And, yet another broken blog post will make the rounds on the internet every three years and confuse even more people. It's sad.
Do you have links stating otherwise?
It's not-- It's a high level language, even though a fairly low level looking one. The obligation of the language is to behave as the spec says, and there may not be a close mapping between your program and what runs on the machine, though there often is.
An "Object" is a concept defined and used extensively in the language specification; "a region of data storage in the execution environment,".
(At least, the above is what I'd expect from a language like Rust)
unsafe extern "stdcall" fn callback(arg0: *const IUnknown) {
let myself: &MyStruct = &*(arg0.offset(-(&((*std::ptr::null::<MyStruct>()).as_event_handler) as isize)) as *const MyStruct);
}There might one day be a "Rust standard", but then let's all wish the standardization process leads to something like the Ada Language Reference Manual instead of the mess that is ANSI C.
The undefined behaviour in C is in my opinion a bad design that unfortunately poisoned a whole lot of software for minor gains in speed somewhere. A mistake that did not need to have become popular but for chance in history.