Live data from Hacker News

Everything in C is undefined behavior

blog.habets.se

731–740 of 748 posts

Re: Everything in C is undefined behavior

#731

Earlier quoted context omitted.

> What if the wrapped index is used to construct an invalid pointer? Constructing an invalid pointer in rust is UB, yes, but integer wraparound is not. > What if the integer is used to read the wrong data to a disk, or corrupt data on disk by writing to the wrong location? Then it is a very bad bug. > What if the program controls a nuclear power plant and the integer causes the control system to fail, causing memory…

> Constructing an invalid pointer in rust is UB no, it is dereferencing, not constructing, an invalid pointer, that is UB. there is even a safe function provided to construct an invalid but non-null pointer: ` https://doc.rust-lang.org/stable/std/ptr/fn.dangling.html `

you're of course 100% right, I was so proud of my nerd joke that I got like the most basic rust safety rule wrong :(

Re: Everything in C is undefined behavior

#732

Earlier quoted context omitted.

The semantics are well-defined in both modes. You can predict exactly what will happen in either case. In C, the semantics are not defined at all , you can't predict what will happen and it's allowed to change between compilations of the same source. It will probably get omitted, since Undefined Behavior isn't allowed by the C abstract machine, but sadly compilers are allowed to emit code for UB in the source (partly…

UB is a runtime property. As far as you can statically verify some code parts, you can see UB at compile time, but the point of UB is exactly that it is about stuff you can't predict, or that is hard to predict as a compiler. Now why you can cook up trivial artificial examples where a compiler will remove some code sections based on statically detected UB, instead of printing an error, you have to ask the compiler au…

You can write a C compiler with exactly laid out well-defined semantics. You can't assume those semantics hold for C-the-language, because it doesn't define those semantics. UB is a property of the language, not just of a given compiler. The Rust reference defines the semantics of the safe subset of Rust without any UB, so any compliant Rust compiler won't have UB in that subset. The reference also defines the guarantees which the programmer must uphold within `unsafe` blocks to avoid UB, as long as those are upheld there's no UB at all.

Re: Everything in C is undefined behavior

#733
post #646

Earlier quoted context omitted.

“More aggressively using UB” isn’t the right way to think about it. In the C ecosystem, the compiler gets to define what UB means. They broke compatibility with their previous UB semantics, then blamed the language spec.

> In the C ecosystem, the compiler gets to define what UB means. It really doesn't though. The current revision of the ISO/IEC 9899 standards document gets to define it, nobody else.

No; that says what things have undefined behavior in the language spec.

The compiler / os / hw platform are free to define the behavior of those things. Leaving them undefined at the language spec is the mechanism that allows them to be defined by the underlying system. C has worked this way since its initial release.

Re: Everything in C is undefined behavior

#734

Earlier quoted context omitted.

> And before I get attacked for being a Rust shill, I meant Java :P If all you want is C but less insane then the obvious answer here is Zig.

If someone is switching from C because it's too easy to trigger undefined behavior, picking one of the few other not memory safe languages is missing the point.

>> If all you want is C but less insane

Zig has orders of magnitude less undefined behavior and orders of magnitude more memory safety than C. It's not a binary situation.

Re: Everything in C is undefined behavior

#735

Earlier quoted context omitted.

UB is a runtime property. As far as you can statically verify some code parts, you can see UB at compile time, but the point of UB is exactly that it is about stuff you can't predict, or that is hard to predict as a compiler. Now why you can cook up trivial artificial examples where a compiler will remove some code sections based on statically detected UB, instead of printing an error, you have to ask the compiler au…

You can write a C compiler with exactly laid out well-defined semantics. You can't assume those semantics hold for C-the-language, because it doesn't define those semantics. UB is a property of the language, not just of a given compiler. The Rust reference defines the semantics of the safe subset of Rust without any UB, so any compliant Rust compiler won't have UB in that subset. The reference also defines the guaran…

I understand that. It makes no practical difference. 99,99% of my additions don't rely on signed overflow for example, and if I'd ever need it there are ways to get just it.

Or tell me how you write a Rust program differently given that signed overflow is apparently defined? I bet you write it exactly the same way, and you get pretty much the same behaviour in practice. And we're even only debating actual overflow situations, meaning there is a bug whatever the compiled behaviour is.

C the language doesn't even guarantee that the machine has native integers with 8, 16, 32, 64 bits etc, that a cacheline is 64 bits, that a page is 4K, and here I am, writing programs for exactly that.

Re: Everything in C is undefined behavior

#736
post #698

Earlier quoted context omitted.

> How would you optimize: (x * 2) / 2 I'd do the math myself and just write x. I don't even use * for multiplication anymore, I use __builtin_mul_overflow and then check the result. Anyone who doesn't is gonna hit the overflow case one day, and they'll be lucky if their program isn't exploited because of it. I've been making an effort to use all the overflow checking builtins by default in most if not all cases. I've…

The thing with (x * 2) / 2 is that for all practical purposes you might even have written something else, so the expression cannot be replaced by x directly. What happens is that after a few common expression eliminations, peephole optmisations, code inlining, and possibly other optimisation passes, the remaining AST will be (x * 2) / 2, and then the magic happens.

That makes sense... I agree, and I don't really have an elegant answer to that at the moment.

I simply accepted the fact that the magic might stop happening at (x * 2) / 2. This acceptance bought me certainty about overflow behavior. I think it was a good tradeoff.

Re: Everything in C is undefined behavior

#737

Earlier quoted context omitted.

What stage is the "just make the compiler define the undefined" stage? Unaligned access? Packed structs. Compiler will magically generate the correct code, as if it had always known how to do it right all along! Because it has, in fact, always known how to do it right. It just didn't. Strict aliasing? Union type punning. Literally documented to work in any compiler that matters, despite the holy C standard never sayi…

> Strict aliasing? Union type punning. Literally documented to work in any compiler that matters, despite the holy C standard never saying so. It does say so, actually, since C99 TC3 (DR 283).

Thanks, I stand corrected.

Re: Everything in C is undefined behavior

#738

Earlier quoted context omitted.

It's fully defined by your CPU's silicon masks and your compiler's binary code that one of several things will happen.

Turns out that when you're implementing network applications, the set of things that could happen also depends on what the script kiddie on the other side of the globe feels like this morning. Some would prefer less excitement than this. C code should be more predictable and easier to reason about than using a macro assembler. To the extent it is not, the language has failed.

Given that it sits at the heart of the network stack, kernel and device drivers for every major operating system, is in many, many embedded devices in the World around us, and is responsible for making decent chunk of the global economy keep moving, that’s quite a failure case.

Perhaps some professional programmers know how to write secure software in a language with undefined behaviour. Maybe we should think about that more rather than just writing off an obviously huge success as a failure?

Re: Everything in C is undefined behavior

#739

Earlier quoted context omitted.

You didn’t really say that, but feel free to share any reasons you might have to think so. I don’t see any reason why it wouldn’t be perfectly fine on recent hardware, where unaligned loads are just as fast, and the cache pressure is identical for a linear search algorithm.

I asked where is the part about unaligned pointers in your string processing example. Saying that you want to load multiple bytes at a time does not imply at all that you have to do unaligned loads. Doing unaligned loads using SSE or AVX might have been possible on Intel architectures for a long time, but it is still a little bit slower afaik. But anyway when you get into sub-architecture specific details like that,…

Every vectorized string search algorithm (including those treating an unsigned long as a "vector" of 8 bytes) currently needs a prelude that performs the search up to the first alignment boundary, and then performs the bulk of the search on well-aligned blocks, and then finally a postlude search in the tail of the string, where the tail is shorter than the block size/alignment.

Using unaligned loads, you can get rid of the prelude, including the associated branches and intptr arithmetic, and just have to deal with the tail.

If you're comparing short-ish strings, almost all of the time is spent in the prelude and postlude, even if the entire substring fits in a register. This is a silly language limitation when the hardware can actually easily just support the unaligned load.

In particular, it doesn't seem justified that what at most amounts to a tiny inefficiency in hardware turns into a very expensive class of bugs (UB).

Re: Everything in C is undefined behavior

#740

Earlier quoted context omitted.

I asked where is the part about unaligned pointers in your string processing example. Saying that you want to load multiple bytes at a time does not imply at all that you have to do unaligned loads. Doing unaligned loads using SSE or AVX might have been possible on Intel architectures for a long time, but it is still a little bit slower afaik. But anyway when you get into sub-architecture specific details like that,…

Every vectorized string search algorithm (including those treating an unsigned long as a "vector" of 8 bytes) currently needs a prelude that performs the search up to the first alignment boundary, and then performs the bulk of the search on well-aligned blocks, and then finally a postlude search in the tail of the string, where the tail is shorter than the block size/alignment. Using unaligned loads, you can get rid…

Have you ever wanted to do this? I find the premise ridiculous.

But anyway, you're complaining that you have to work too hard to do unaligned loads (i.e. the wrong thing even if it should work on a particular machine) in C, when basically every other language makes you work more for basic systems programming tasks?

Whether unaligned loads can work on the machine level, it depends on the hardware. On some other architectures, you probably get anything from traps to unpredictable behaviour. It's totally fine that C does not define the behaviour for unaligned loads.

If you want to do some weird stuff like loading a single unaligned 16 byte quantity, where there was no "middle part" to begin with, just do memcpy then. The compiler might just do the appropriate thing on this architecture. Or if you need to closely control what's happened, write assembly then. But again, why would you even do this?

Post reply on HN