Hacker News new | ask | show | jobs
by saagarjha 28 days ago
Turning undefined behavior into implementation defined behavior is rarely a fix, though.
1 comments

It's a fix that removes the most pointy part of UB.

"Going past the end of the array results in addressing arbitrary values" I can live with. "Going past the end of an array results in anything happening" is a hard sell.

Is that really a meaningful distinction?

Once you are addressing arbitrary values you are firmly in the realm of "anything happening" in practice, but you've now given up optimization opportunities. As has been repeatedly demonstrated over the years, once memory safety breaks it is practically impossible to make any guarantees about program behavior.

Yes, it's a meaningful distinction. No you are not into "anything happening" in practice.

Your compiler emitting a load operation and it failing isn't "anything". The failure being handled by code that the compiler authors can't predict doesn't make it "anything".

And if you lose optimization opportunities because of this it's because your optimization is broken. By the way, if you lose optimization opportunities because of this, that means both codes are meaningfully different and you knew it all the time.

Compilers elide loads all the time this is one of the more basic optimizations a compiler can do. We just mostly think those are "good" optimizations.
I mean... You can turn a one byte out of bounds write into code execution.

https://daniel.haxx.se/blog/2016/10/14/a-single-byte-write-o...

And if you get code execution, then you by definition have "anything".

I think it’s a really easy sell, actually: if you go past the end of the array far enough you end up accessing the stack which includes parts of the program like “where does this function return to” or “what is the index used to perform this access” or “there is no page mapped there”. None of these are arbitrary values.
The "anything can happen" means that the compiler can simply silently refuse to emit the code does the access.

Documenting that the instructions to access will always be eliminated makes it easier to predict what will happen.

Can you unravel this further (for those of us who don’t know compilers)? I’ve always assumed access past the end of an array can’t always be detected in C, so I don’t see how those instructions could be eliminated.

For example, a dynamically linked library that takes in a pointer, and then writes to the 10 ints after it—whether or not this behavior is defined is determined after that library is compiled, right?

I think the disconnect here is that you're operating on the assumptions built by using common architectures that have solved these problems in implementation specific forms, and you're used to those solutions.

But just because those forms are common, doesn't mean the behavior is actually defined.

Ex - I might be using a vendor specific compiler for custom embedded devices where dynamic linking isn't available at all, and which might have complicated storage mechanisms that look nothing like standard memory pages.

I’m not sure there’s a disconnect at all (note that I’m not saagarjah, they and lelanthran seem to be pushing back on each other’s opinions; I’m just asking a clarifying question).
> I’ve always assumed access past the end of an array can’t always be detected in C, so I don’t see how those instructions could be eliminated.

"Can't always be detected" is jut a different way of saying "Can sometimes be detected".

Upon detection, I'd rather that the compiler still emit the instructions, not elide the code altogether.

Now the behavior of your compiler/runtime stack is dependent on the sophistication of your compiler or runtime relative to the particular code at issue + the specific information available statically or dynamically in the instance.

That does not seem like an improvement if your goal is predictable, consistent behavior.

Yes, but usually you don't want this. You think you do, but you don't: you can't always eliminate these, and often eliminating the extra accesses is not the most efficient thing to do either. Sometimes it's faster to have the loads and not check, sometimes you can check and skip that path, etc.
Are you talking about creating a pointer (more than one item) past an array, or dereferencing that pointer? Both are currently UB.

For the former, I kinda get it. It may need to be there for cases like with segmented address space where p+10 could actually be a value less than p, for the eventually generated assembly. Maybe it should be fine to create such a pointer, but have it be "indeterminate value" or whatever, if you try to compare that pointer to anything? I don't know enough about compiler internals to say one way or the other.

Dereferencing, though, can only be UB. There may not be a "value" behind that address. There may be a motor that's been I/O mapped, or a self destruct button.

I'm not saying that the result of the dereference be known, I'm saying that the instructions to do the dereference be always emitted.

Right now, if a dereference results in UB, the compiler may omit it entirely.

I think I would defer to someone more of a language lawyer than we, but I'm not sure what you're describing can be expressed in the C abstract machine. If a pointer is invalid, not pointing to an object, then I'm not sure it means anything to "read from there".

I know what you mean, but I'm just not sure you're describing something that fits what C "is". We program C to the abstract machine specified in the standard (5.1.2), and the compiler's job is to translate that into something with identical behavior on particular hardware. Piercing the layers down to actual hardware or assembly isn't really done.

Even "volatile" just says (basically) "touching this object has side effects". It implies no double-loading, speculative store, etc, but doesn't say "don't emit assembly instructions to load this unless the program logic path takes the route where the C program does load it".

The standard is not using ancient language when it refers to "objects with static storage duration" instead of "heap" or ".data segment". It is the true class of objects in the abstract machine.

Wouldn't that make a compiler that emitted bounds checks violate the standard, since it would not be emitting the actual memory operations if you deref out of bounds?
No, because it's UB so there is no standard.
Isn't the proposal from the parent comment to define the behavior?