[HN Gopher] Undefined Behavior in C and C++ (2024)
       ___________________________________________________________________
        
       Undefined Behavior in C and C++ (2024)
        
       Author : imadr
       Score  : 86 points
       Date   : 2025-08-08 21:30 UTC (4 days ago)
        
 (HTM) web link (russellw.github.io)
 (TXT) w3m dump (russellw.github.io)
        
       | VivaTechnics wrote:
       | We switched to Rust. Generally, are there specific domains or
       | applications where C/C++ remain preferable? Many exist--but are
       | there tasks Rust fundamentally cannot handle or is a weak choice?
        
         | imadr wrote:
         | I haven't used Rust extensively so I can't make any criticism
         | besides that I find compilation times to be slower than C
        
           | ost-ing wrote:
           | I find with C/++ I have to compile to find warnings and
           | errors, while with Rust I get more information automatically
           | due to the modern type and linking systems. As a result I
           | compile Rust significantly less times which is a massive
           | speed increase.
           | 
           | Rusts tooling is hands down better than C/++ which aids to a
           | more streamlined and efficient development experience
        
             | bch wrote:
             | > Rusts tooling is hands down better than C/++ which aids
             | to a more streamlined and efficient development experience
             | 
             | Would you expand on this? What was your C tooling/workflow
             | that was inferior to your new Rust experience?
        
               | simonask wrote:
               | Not the GP, but the biggest one is dependency management.
               | Cargo is just extremely good.
               | 
               | As for the language tooling itself, static and runtime
               | analyzers in C and C++ (and these are table stakes at
               | this point) do not come close to the level of accuracy of
               | the Rust compiler. If you care about writing unsafe code,
               | Miri is orders of magnitude better at detecting UB than
               | any runtime analyzer I've seen for C and C++.
        
               | johnisgood wrote:
               | Pacman is extremely good, too, for C. :)
        
               | simonask wrote:
               | Pacman solves a different problem. Cargo manages your
               | project's dependencies, not system packages.
        
               | johnisgood wrote:
               | I know, but often that is all you need for C.
        
               | uecker wrote:
               | I do not think package management should be done at the
               | level of programming languages.
        
           | ykonstant wrote:
           | I also hear that Async Rust is very bad. I have no idea; if
           | anyone knows, how does async in Rust compare to async in C++?
        
             | 01HNNWZ0MV43FF wrote:
             | I am yet to use async in c++, but I did work on a multi
             | threaded c++ project for a few years
             | 
             | Rust is nicer for async and MT than c++ in every way. I am
             | pretty sure.
             | 
             | But it's still mid. If you use Rust async aggressively you
             | will struggle with the borrow checker and the architecture
             | results of channel hell.
             | 
             | If you follow the "one control thread that does everything
             | and never blocks" you can get far, but the language does
             | not give you much help in doing that style neatly.
             | 
             | I have never used Go. I love a lot of Go projects like
             | Forgejo and SyncThing. Maybe Go solved async. Rust did not.
             | C++ did not even add good tagged unions yet.
        
               | ykonstant wrote:
               | Thanks for the info!
        
               | eru wrote:
               | Go (at least before generics) was really annoying to use.
               | 
               | Doing anything concurrent in Go is also really annoying
               | (be that async or with threads), because everything is
               | mutable. Not just by default but always. So anything
               | shared is very dangerous.
        
             | ViewTrick1002 wrote:
             | > I also hear that Async Rust is very bad.
             | 
             | Not sure where this is coming from.
             | 
             | Async rust is amazing as long as you only mix in one more
             | hard concept. Be it traits, generics or whatever. You can
             | confidently write and refactor heavily multithreaded code
             | without being deathly afraid of race conditions etc. and it
             | is extremely empowering.
             | 
             | The problem comes when trying to write async generic traits
             | in a multithreaded environment.
             | 
             | Then just throwing stuff at the wall and hoping something
             | sticks will quickly lead you into despair.
        
           | kazinator wrote:
           | The popular C compilers are seriously slow, too. Orders of
           | magnitude compared to C compilers of yesteryear.
        
         | uecker wrote:
         | Advantages of C are short compilation time, portability, long-
         | term stability, widely available expertise and training
         | materials, less complexity.
         | 
         | IMHO you can today deal with UB just fine in C if you want to
         | by following best practices, and the reasons given when those
         | are not followed would also rule out use of most other safer
         | languages.
        
           | lifthrasiir wrote:
           | > short compilation time
           | 
           | > IMHO you can today deal with UB just fine in C if you want
           | to by following best practices
           | 
           | In the other words, short compilation time has been traded
           | off with wetware brainwashing... well, adjustment time, which
           | makes the supposed advantage much less desirable. It _is_
           | still an advantage, I reckon though.
        
             | uecker wrote:
             | I do not understand what you are tying to say, but it seems
             | to be some hostile rambling.
        
               | lifthrasiir wrote:
               | Never meant to be hostile (if I indeed were, I would have
               | question every single word), but sorry for that.
               | 
               | I mean to say that best practices do help much but
               | learning those best practices take much time as well. So
               | short compilation time is easily offseted by learning
               | time, and C was not even designed to optimize compilation
               | time anyway (C headers can take a lot to parse and
               | discard even when unused!). Your other points do make
               | much more sense and it's unfortunate that first points
               | are destructively interfering each other, hence my
               | comment.
        
               | uecker wrote:
               | Sorry, maybe I misread your comment. There are certainly
               | languages easier to learn than C, but I would not say C++
               | or Rust fall into this category. At the same time, I find
               | C compilation extremely fast exactly because of headers.
               | In C you can split interface and implementation cleanly
               | between header and c-file and this enables efficient
               | incremental builds. In C++ most of the implementation is
               | in headers, and all the template processing is order of
               | magnitude more expensive than parsing C headers. Rust
               | also does not seem to have proper separate compilation.
        
               | steveklabnik wrote:
               | > I find C compilation extremely fast exactly because of
               | headers.
               | 
               | The header model is one of the parts that makes compiling
               | C slower than it could be. This doesn't mean that it is
               | slow, but it's fast _in spite_ of headers, not because of
               | them.
               | 
               | > In C you can split interface and implementation cleanly
               | between header and c-file and this enables efficient
               | incremental builds.
               | 
               | That's not what does, it is the ability to produce
               | individual translation units as intermediary files.
               | 
               | > Rust also does not seem to have proper separate
               | compilation.
               | 
               | Rust does separate compilation, and also has efficient
               | incremental builds. Header files are not a hard
               | requirement for this.
        
               | uecker wrote:
               | If you say the header model makes it slower than it could
               | be, you need to compare it to something. I do not see how
               | it causes significant slow downs in C projects (in
               | contrast to C++). And yes, I wrote compilers and
               | (incomplete) preprocessors. I do not understand what you
               | mean by your second point. What separation of interface
               | and implementation allows you to do is updating the
               | implementation without having to recompile other TUs. You
               | can achieve this is also in different ways, but in C this
               | works by in this way.
               | 
               | I am not sure how it works in Rust as you need to
               | monomorphize a lot of things, which come from other
               | crates. It seems this would inevitably entangle the
               | compilations.
        
               | steveklabnik wrote:
               | > I do not see how it causes significant slow downs in C
               | projects
               | 
               | It's that textual inclusion is just a terrible model. You
               | end up reprocessing the same thing over and over again,
               | everywhere it is used. If you #include<foo.h> 100 times,
               | the compiler has to reparse those contents 100 times.
               | Nested headers end up amplifying this effect. It's also
               | at a file-level granularity, if you change a header,
               | every single .c that imports it must be recompiled, even
               | if it didn't use the thing that was changed. etc etc.
               | These issues are widely known.
               | 
               | > I do not understand what you mean by your second point.
               | What separation of interface and implementation allows
               | you to do is updating the implementation without having
               | to recompile other TUs.
               | 
               | Sure, but you don't need to have header files to do this.
               | Due to issues like the above, they cause _more_ things to
               | be recompiled than necessary, not less.
               | 
               | > You can achieve this is also in different ways, but in
               | C this works by in this way.
               | 
               | Right, my point is, those other ways are better.
               | 
               | > I am not sure how it works in Rust as you need to
               | monomorphize a lot of things, which come from other
               | crates. It seems this would inevitably entangle the
               | compilations.
               | 
               | The fact that there are "other crates" is because Rust
               | supports separate compilation: each crate is compiled
               | independently, on its own.
               | 
               | The rlib contains the information that, when you link two
               | crates together, the compiler can use for
               | monomorphization. And it's true that monomorphization can
               | cause a lot of rebuilding.
               | 
               | But to be clear, I am not arguing that Rust compilation
               | is fast. I'm arguing that C could be even faster if it
               | didn't have the preprocessor.
        
           | simonask wrote:
           | This is a pet peeve, so forgive me: C is not portable in
           | practice. Almost every C program and library that does
           | anything interesting has to be manually ported to every
           | platform.
           | 
           | C is portable in the least interesting way, namely that
           | compilers exist for all architectures. But that's where it
           | stops.
        
             | uecker wrote:
             | Compilers existing is essential and not trivial (and also
             | usually then what other languages build on). The
             | conformance model of C also allows you to write programs
             | that are portable without change to different platforms.
             | This is possible, my software runs on 20 different
             | architectures without change. That one can then also adopt
             | it to make use of specific features of different platforms
             | is quite natural in my opinion.
        
               | simonask wrote:
               | It is essential and nontrivial, but it's also the
               | extremely bare minimum.
               | 
               | You cannot write portable code without platform-specific
               | and even environment-specific adaptations, like handling
               | the presence of certain headers (looking at you, stdint.h
               | and stddef.h), and let's not even start about interacting
               | with the OS in any way.
        
               | uecker wrote:
               | There may be platforms that are not conforming to the C
               | standard. But I doubt those then have comprehensive
               | implementations of other languages either.
        
             | pjmlp wrote:
             | Back in the 2000's I had lots of fun porting code across
             | several UNIX systems, Aix, Solaris, HP-UX, Red-Hat Linux.
             | 
             | A decade earlier I also used Xenix and DG/UX.
             | 
             | That is a nice way to learn how "portable" C happens to be,
             | even between UNIX systems, its birthplace.
        
             | snovymgodym wrote:
             | > C is not portable in practice. Almost every C program and
             | library that does anything interesting has to be manually
             | ported to every platform.
             | 
             | I'm guessing you mean that every cross-platform C codebase
             | ends up being plastered in cascading preprocessor code to
             | deal with OS and architecture differences. Sure that's
             | true, you still have to do some porting work regardless of
             | the language you chose.
             | 
             | But honestly, is there any language more portable than C? I
             | struggle to come up with one.
             | 
             | If someone told me "I need a performant language that
             | targets all major architectures and operating systems, but
             | also maybe I want to run it on DOS, S390X, an old Amiga I
             | have in my closet, and any mystery-meat microcontroller I
             | can find." then really wouldn't have a better answer for
             | them than C89.
             | 
             | If C isn't portable then nothing is.
        
               | simonask wrote:
               | If "portability" to you has to include incredibly
               | esoteric architectures in 2025, then what C has to offer
               | is probably the best you can do, but my point is it
               | doesn't do any better on mainstream platforms either.
               | 
               | If you are targeting any recent platform, both Rust and
               | Zig do what you want.
        
         | mrheosuper wrote:
         | Rust can do inline ASM, so finding a task Rust "fundamentally
         | cannot handle" is almost impossible.
        
           | eru wrote:
           | That's almost as vacuous as saying that Rust can implement
           | universal Turing machines are that Rust can do FFI?
        
         | pizza234 wrote:
         | Yes, based on a few attempts chronicled in articles from
         | different sources, Rust is a weak choice for game development,
         | because it's too time-consuming to refactor.
        
           | Defletter wrote:
           | Yup, this one (https://news.ycombinator.com/item?id=43824640)
           | comes to mind. The first comment says _" Another failed game
           | project in Rust"_, hinting that this is very common.
        
           | bakugo wrote:
           | There's also the fact that a lot of patterns that are
           | commonly used in game development are fundamentally at odds
           | with the borrow checker.
           | 
           | Relevant: https://youtu.be/4t1K66dMhWk?si=dZL2DoVD94WMl4fI
        
             | simonask wrote:
             | Basically all of those problems originate with the
             | tradition of conflating pointers and object identity, which
             | is a problem in Rust as soon as you have ambiguous
             | ownership or incongruent access patterns.
             | 
             | It's also very often not the best way to identify objects,
             | for many reasons, including performance (spatial locality
             | is a big deal).
             | 
             | These problems go away almost completely by simply using
             | `EntityID` and going through `&mut World` for
             | modifications, rather than passing around `EntityPtr`. This
             | pattern gives you a lot of interesting things for free.
        
               | bakugo wrote:
               | The video I linked to is long but goes through all of
               | this.
               | 
               | Pretty much nobody writing games in C++ uses raw pointers
               | in entities to hold references to other related entities,
               | because entities can be destroyed at any time and there's
               | no simple way for a referring entity to know when a
               | referenced entity is destroyed.
               | 
               | Using some sort of entity ID or entity handle is very
               | common in C++, the problem is that when implementing this
               | sort of system in Rust, developers often end up having to
               | effectively "work around" the borrow checker, and they
               | end up not really gaining anything in terms of
               | correctness over C++, ultimately defeating the purpose of
               | using Rust in the first place, at least for that
               | particular system.
        
               | simonask wrote:
               | Can you give an example of what problems need workarounds
               | here?
               | 
               | The benefits seem pretty massive, at least on the
               | surface. For example, you can run any system that only
               | takes `&World` (i.e., immutable access) in parallel
               | without breaking a sweat.
        
           | ramon156 wrote:
           | We've only had 6-7 years of hame dev in rust. Bevy is coming
           | along nicely and will hopefully remove these pain points
        
             | flohofwoe wrote:
             | "Mit dem Angriff Steiner's wird das alles in Ordnung
             | kommen" ;)
             | 
             | As shitty as C++ is from today's PoV, the entire gaming
             | industry switched over within around 3 years towards the
             | end of the 90s. 6..7 years is a _long_ time, and a single
             | engine (especially when it 's more or less just a runtime
             | without editor and robust asset pipeline) won't change the
             | bigger picture that Rust is a pretty poor choice for
             | gamedev.
        
               | eru wrote:
               | > As shitty as C++ is from today's PoV, the entire gaming
               | industry switched over within around 3 years towards the
               | end of the 90s.
               | 
               | Did they? What's your evidence? Are you including
               | consoles?
               | 
               | Btw, the alternatives in the 1990s were worse than they
               | are now, so the bar to clear for eg C or C++ were lower.
        
               | flohofwoe wrote:
               | I was there Gandalf... ;) Console SDKs offering C or C++
               | APIs doesn't really matter, because you can call C APIs
               | from C++ just fine. So the language choice was a team and
               | engine developer decision, not a platform owner decision
               | (as it should be).
               | 
               | From what I've seen, around the late mid-90's, C++ usage
               | was still rare, right before 2000 it was already common
               | and most middleware didn't even offer C APIs anymore.
               | 
               | Of course a couple of years later Unity arrived and made
               | the gamedev language choice more complicated again.
        
               | eru wrote:
               | > I was there Gandalf... ;)
               | 
               | You were at most in one place. My question was rather,
               | which corners of the industry are you counting?
               | 
               | However you are right that one of the killer features of
               | C++ was that it provided a pretty simple upgrade path
               | from C to (bad) C++.
               | 
               | It's not just API calls. You can call C APIs from most
               | languages just fine.
        
               | flohofwoe wrote:
               | My corner of the industry back then was mostly PC gamedev
               | with occasional exploration of game consoles (but only
               | starting with the OG Xbox. But that doesn't really matter
               | much since it was obvious that the entire industry was
               | very quickly moving to C++ (we had internet back then
               | after all in my corner of the wood, as well as gamedev
               | conferences to feel the general vibe).
               | 
               | id Software was kinda famous for being the last big C
               | holdout, having only switched to C++ with Doom 3, and
               | development of Doom 3 started in late 2000.
        
               | pjmlp wrote:
               | As another Gandalf, Playstation 2 was the very first
               | console to actually offer proper C++ tooling.
               | 
               | That would be 2000, until then Sega, Nintendo and
               | Playstion only had C and Assembly SDKs, even the
               | Playstation Yaroze for hobbists did get released only
               | with C and Assembly support.
               | 
               | PC was naturally another matter, especialy with Watcom
               | C/C++.
        
             | account42 wrote:
             | And there are millions of game engines written in C++. Many
             | of them have also been coming along nicely for years.
             | 
             | Making a nontrivial game with them is a wholly different
             | story.
        
         | bluetomcat wrote:
         | Rust encourages a rather different "high-level" programming
         | style that doesn't suit the domains where C excels. Pattern
         | matching, traits, annotations, generics and functional idioms
         | make the language verbose and semantically-complex. When you
         | follow their best practices, the code ends up more complex than
         | it really needs to be.
         | 
         | C is a different kind of animal that encourages terseness and
         | economy of expression. When you know what you are doing with C
         | pointers, the compiler just doesn't get in the way.
        
           | eru wrote:
           | Pattern matching should make the language less verbose, not
           | more. (Similar for many of the other things you mentioned.)
           | 
           | > When you know what you are doing with C pointers, the
           | compiler just doesn't get in the way.
           | 
           | Alas, it doesn't get in the way of you shooting your own foot
           | off, too.
           | 
           | Rust allows unsafe and other shenanigans, if you want that.
        
             | bluetomcat wrote:
             | > Pattern matching should make the language less verbose,
             | not more.
             | 
             | In the most basic cases, yes. It can be used as a more
             | polished switch statement.
             | 
             | It's the whole paradigm of "define an ad-hoc Enum here and
             | there", encoding rigid semantic assumptions about a
             | function's behaviour with ADTs, and pattern matching for
             | control-flow. This feels like a very academic approach and
             | modifying such code to alter its opinionated assumptions
             | isn't funny.
        
           | za_creature wrote:
           | > When you know what you are doing with C pointers, the
           | compiler just doesn't get in the way.
           | 
           | Tell me you use -fno-strict-aliasing without telling me.
           | 
           | Fwiw, I agree with you and we're in good[citation needed]
           | company: https://www.mail-archive.com/linux-
           | btrfs@vger.kernel.org/msg...
        
         | mgaunard wrote:
         | Rust forces you to code in the Rust way, while C or C++ let you
         | do whatever you want.
        
           | nicoburns wrote:
           | > C or C++ let you do whatever you want.
           | 
           | C and C++ force you to code in the C and C++ ways. It may
           | that that's what you want, but they certainly dont let me
           | code how I want to code!
        
             | mgaunard wrote:
             | There is no C or C++ ways. It's widely known that every
             | codebase is its own dialect.
        
               | nicoburns wrote:
               | There are lots of C and particularly C++ ways, but you're
               | still restricted. Want to use methods in C: nope, you
               | can't. Want language-level tagged unions and pattern
               | matching in either language: nope. Same for guaranteed
               | tail call optimisation and a bunch of other things.
               | 
               | This is especially true for C which supports almost
               | nothing (it doesn't even have a sensible array type!).
               | But is also true for C++: while it supports a lot, it
               | doesn't support everything.
        
               | bigfishrunning wrote:
               | what changes, in your opinion, would need to be made to
               | the C array type to make it "sensible"? C's array is
               | simplistic, but I don't think it's not "sensible"...
        
               | mgaunard wrote:
               | consider the C++ std::array, which exists to make arrays
               | behave like normal objects.
               | 
               | You can do the same in C by wrapping your array in a
               | struct.
        
               | nicoburns wrote:
               | It would need to store a length and not decay to a
               | pointer when passed to a function.
        
               | mgaunard wrote:
               | The funny part is that all of these things are easy to
               | achieve as libraries/paradigms.
               | 
               | Methods in C, just have function pointers as members.
               | Common in many codebases.
               | 
               | Guaranteed tail calls, all the compilers guarantee that
               | function calls that are a return expression are tail
               | calls.
               | 
               | Tagged union in C++, it's trivial as a library, see
               | std::variant for a bad example of it, and all the various
               | monadic/pattern-matching variants (pun intended) people
               | have written. C is at a disadvantage here due to lack of
               | lambdas, but I'm sure people have built stuff using some
               | GCC extensions.
        
         | pjmlp wrote:
         | Yes, all the industries where C and C++ are the industry
         | standards like Khronos APIs, POSIX, CUDA, DirectX, Metal,
         | console devkits, LLVM and GCC implementation,....
         | 
         | Not only you are faced with creating your own wrappers, if no
         | one else has done it already.
         | 
         | The tooling, for IDEs and graphical debuggers, assumes either C
         | or C++, so it won't be there for Rust.
         | 
         | Ideally the day will come where those ecosystems might also
         | embrace Rust, but that is still decades away maybe.
        
         | m-schuetz wrote:
         | Prototyping in any domain. It's nice to do some quick&dirty way
         | to rapidly evaluate ideas and solutions.
        
           | eru wrote:
           | I don't think C nor C++ were ever great languages for
           | prototyping? (And definitely not better than Rust.)
        
             | m-schuetz wrote:
             | Please try not to be obnoxious and turn this into a
             | language war.
        
         | eru wrote:
         | > Generally, are there specific domains or applications where
         | C/C++ remain preferable?
         | 
         | Well, anything were your people have more experience in the
         | other language or the libraries are a lot better.
        
         | teunispeters wrote:
         | embedded hardware, any processor Rust doesn't support (there
         | are many), and any place where code size is critical. Rust has
         | a BIG base size for an application, uselessly so at this time.
         | I'd also love to see if it offered anything that could be any
         | use in those spaces - especially where no memory allocation
         | takes place at all. C (and to a lesser extent C++) are both
         | very good in those spaces.
        
           | steveklabnik wrote:
           | You can absolutely make small rust programs, you just have to
           | actually configure things the right way. Additionally, the
           | Rust language doesn't have allocation at all, it's purely a
           | library concern. If you don't want heap allocations, then
           | don't include them. It works well.
           | 
           | The smallest binary rustc has produced is like ~145 bytes.
        
             | teunispeters wrote:
             | That is far from my only concern. But it's good to see Rust
             | is finally paying attention to binary sizes. And the
             | overwhelming complexity of rust code is definitely not a
             | gain when one is working in embedded spaces anyway. I am
             | however really REALLY annoyed with the aggressive sales
             | tactics of the rust community.
        
               | steveklabnik wrote:
               | > But it's good to see Rust is finally paying attention
               | to binary sizes.
               | 
               | Just to be clear, this isn't a recent development, it has
               | been this way for many years at this point.
        
         | jandrewrogers wrote:
         | An application domain where C++ is notably better is when the
         | ownership and lifetimes of objects are not knowable at compile-
         | time, only being resolvable at runtime. High-performance
         | database kernels are a canonical example of code where this
         | tends to be common.
         | 
         | Beyond that, recent C++ versions have much more expressive
         | metaprogramming capability. The ability to do extensive codegen
         | and code verification within C++ at compile-time reduces lines
         | of code and increases safety in a significant way.
        
         | mckravchyk wrote:
         | If you wanted to develop a cross-platform native desktop /
         | mobile app in one framework without bundling / using a web
         | browser, only QT comes to mind, which is C++. I think there are
         | some bindings though.
        
       | fattah25 wrote:
       | Rust here rust there. We are just talking about C not rust. Why
       | we have to using rust. If you talking memory safety why there is
       | no one recommends Ada language instead of rust.
       | 
       | We have zig, Hare, Odin, V too.
        
         | ViewTrick1002 wrote:
         | > Ada language instead of rust
         | 
         | Because it never achieved mainstream success?
         | 
         | And Zig for example is very much not memory safe. Which a
         | cursory search for "segfault" in the Bun repo quickly tells
         | you.
         | 
         | https://github.com/oven-sh/bun/issues?q=is%3Aissue%20state%3...
        
           | lifthrasiir wrote:
           | More accurately speaking, Zig helps spatial memory safety
           | (e.g. out-of-bound access) but doesn't help temporal memory
           | safety (e.g. use-after-free) which Rust excels at.
        
             | ViewTrick1002 wrote:
             | As long as you are using the "releasesafe" build mode and
             | not "releasefast" or "releasesmall".
        
             | pjmlp wrote:
             | Which is something that even PL/I predating C already had.
        
           | johnisgood wrote:
           | > Because it never achieved mainstream success?
           | 
           | And with this attitude it never will. With Rust's hype, it
           | would.
        
         | pjmlp wrote:
         | None of them solve use after free, for example.
         | 
         | Ada would rather be a nice choice, but most hackers love their
         | curly brackets.
        
         | the__alchemist wrote:
         | Even within the rust OSS community it's irritating. They will
         | try to cancel people for writing libs using `unsafe`, and makes
         | APIs difficult to use by wrapping things in multiple layers of
         | traits, then claim using other patters are unsafe/unsound/UB.
         | They make claims that things like DMA are "advanced topics",
         | and "We haven't figured it out yet/found a good solution yet".
         | Love rust/hate the Satefy Inquisition. Or say things like "Why
         | use rust if you don't use all the safety-features and
         | traits"... which belittles rust as a one-trick lang!
        
       | uecker wrote:
       | One has to add that from the 218 UB in the ISO C23, 87 are in the
       | core language. From those we already removed 26 and are in
       | progress of removing many others. You can find my latest update
       | here (since then there was also some progress): https://www.open-
       | std.org/jtc1/sc22/wg14/www/docs/n3529.pdf
        
         | ncruces wrote:
         | And yet, I see P1434R0 seemingly trying to introduce new
         | undefined behavior, around integer-to-pointer conversions,
         | where previously you had reasonably sensible implementation
         | defined behavior (the conversions _"are intended to be
         | consistent with the addressing structure of the execution
         | environment "_).
         | 
         | https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p14...
        
           | gpderetta wrote:
           | Pointer provenance already existed before, but the standards
           | were contradictory and incomplete. This is an effort to more
           | rigorously nail down the semantics.
           | 
           | i.e., the UB already existed, but it was not explicit had to
           | be inferred from the whole text and the boundaries were
           | fuzzy. Remember that anything not explicitly defined by the
           | standard, is implicitly undefined.
           | 
           | Also remember, just because you can legally construct a
           | pointer it doesn't mean it is safe to dereference.
        
             | JonChesterfield wrote:
             | Pointer provenance was certainly not here in the 80s.
             | That's a more modern creation seeking to extract better
             | performance from some applications at a cost of making
             | others broken/unimplementable.
             | 
             | It's not something that exists in the hardware. It's also
             | not a good idea, though trying to steer people away from it
             | proved beyond my politics.
        
               | gpderetta wrote:
               | I'm not a compiler writer, but I don't know how you would
               | be able to implement any optimization while allowing
               | arbitrary pointer forging and without whole-program
               | analysis.
        
               | ncruces wrote:
               | Why? What specific optimization do you have in mind that
               | prevents me from doing an aligned 16/32/64-byte vector
               | load that covers the address pointed to by a valid char*?
        
               | gpderetta wrote:
               | Casting a char pointer to a vector pointer and doing
               | vector loads doesn't violate provenance, although it
               | might violate TBAA.
               | 
               | Regarding provenance, consider this:                 void
               | bar();       int foo() {         int * ptr =
               | malloc(sizeof(int));         *ptr = 10;         bar();
               | int result = *ptr;         free(ptr);         return
               | result;       }
               | 
               | If the compiler can track the lifetime of the dynamically
               | allocated int, it can remove the allocation and covert
               | this function to simply                 int foo() {
               | bar();           return 10;       }
               | 
               | It can't if arbitrary code (for example inside bar()) can
               | forge pointers to that memory location. The code can seem
               | silly, but you could end up with something similar after
               | inlining.
        
               | torstenvl wrote:
               | > _It can 't if arbitrary code (for example inside bar())
               | can forge pointers to that memory location._
               | 
               | Yes. It absolutely can. What are you even talking about?
               | 
               | C is not the Windows Start Menu. This habit of thinking
               | it needs to do what it thinks I might expect instead of
               | what I told it is deeply psychotic.
        
               | gpderetta wrote:
               | I litterally have no idea what are you trying to say. Do
               | you mean that bar should be allowed to access *ptr with
               | impunity or not?
        
               | torstenvl wrote:
               | I'm not trying to say anything. I said and meant exactly
               | what I said. No more, no less. Your logic is obviously
               | flawed. There is _nothing_ preventing that optimization
               | in the presence of a forged pointer in bar().
        
               | gpderetta wrote:
               | Either there is no provenance, forging is allowed and the
               | optimization is disallowed; or there is provenance and
               | forging the pointer and attempting to inspect (or modify)
               | the value of *ptr in bar() is UB.
        
               | torstenvl wrote:
               | Attempting to inspect or modify the value of *ptr in
               | bar() through a forged pointer was always UB. You are
               | saying absolutely nothing meaningful.
        
               | ncruces wrote:
               | You never converted ptr to an integer. If you did, if the
               | pointer escapes, yes, I claim that then the allocation
               | can't be optimized away. Why is that so bad?
        
               | ncruces wrote:
               | Can't reply to the sibling comment, for some reason.
               | 
               | If you don't know the extents of the object pointed to by
               | the char*, using an aligned vector load can reach outside
               | the bounds of the object. Keeping provenance makes _that_
               | undefined behavior.
               | 
               | Using integer arithmetic, and pointer-to-integer/integer-
               | to-pointer conversions would make this implementation
               | defined, and _well_ defined in all of the hardware
               | platforms where an aligned vector load can never possibly
               | fail.
               | 
               | So you can't do some optimizations to functions where
               | this happens? Great. Do it. What else?
               | 
               | As for why you'd want to do this. C makes strings null-
               | terminated, and you can't know their extents without
               | strlen first. So how do you implement strlen? Similarly
               | your example. Seems great until you're the one
               | implementing malloc.
               | 
               | But I'm sure "let's create undefined behavior for a libc
               | implemented in C" is a fine goal.
        
               | gpderetta wrote:
               | [when there is no reply button, you need to click on the
               | date (i.e. N minutes ago) to get the reply box]
               | 
               | I think your example would fall foul of reading beyond
               | the end of an object in addition to pointer provenance.
               | In your case the oob read is harmless as you do not
               | expect any meaningful values for the extra bytes, but
               | generally the compiler would not be able to give any
               | guarantees about the content of the additional memory (or
               | that the memory exists in the first place).
               | 
               | This specific use case could be addressed by the
               | standard, but vectors are already out of the standard, so
               | in practice you use whatever extension you have to use
               | and abide to whatever additional rule the compiler
               | requires (of course this is often underspecified). For
               | example, on GCC simd primitives already have carve-outs
               | for TBAA.
               | 
               | FWIW, a libc implementation in practice already must rely
               | on compiler specific, beyond the standard behaviour
               | anyway.
        
               | tialaramex wrote:
               | > [when there is no reply button, you need to click on
               | the date (i.e. N minutes ago) to get the reply box]
               | 
               | As an off-topic aside here that might help anybody who is
               | wondering: HN deliberately doesn't provide "Reply" for
               | very recent comments to try to dissuade you from having
               | the sort of urgent back-and-forth you might reasonably do
               | in a real time chat system, and less reasonably attempt
               | (and likely regret) on platforms like Twitter.
               | 
               | A brief window to _think_ about the thing you just read
               | might cause you to write something more thoughtful, and
               | even to realise that it wasn 't saying what you had
               | thought in the first place.
               | 
               | My favourite example was an example where somebody said a
               | feature means "less typing" and another comment insisted
               | it did not, and I was outraged until I realised all
               | that's happening is that one person thinks "Typing" means
               | "You know, pressing keys on your keyboard" and the other
               | person thinks "Typing" means "You know, why an integer is
               | different from a float in C" and so they're actually not
               | even disagreeing the conflict is purely syntax!
        
               | gpderetta wrote:
               | Allegedly. Instead I like to think it is a reality check
               | to remind me I'm wasting too much time on HN and should I
               | do something productive :D
        
               | lmkg wrote:
               | It very much is something that exists in hardware. One of
               | the major reasons why people finally discovered the
               | provenance UB lurking in the standard is because of the
               | CHERI architecture.
        
               | AnimalMuppet wrote:
               | So it's something that exists in _some_ hardware. Are you
               | claiming that it exists in _all_ hardware, and we only
               | realized that because of CHERI? Or are you claiming that
               | it exists in CHERI hardware, but not in others.
               | 
               | If it only exists in some hardware, how should the
               | standard deal with that?
        
               | lmkg wrote:
               | > If it only exists in some hardware, how should the
               | standard deal with that?
               | 
               | Generally seems to me the C standard makes things like
               | that UB. Signed integer overflow, for example.
               | Implemented as wrapping two's-complement on modern
               | architectures, defined as such in many modern languages,
               | but UB in C due to ongoing support for niche
               | architectures.
               | 
               | The issues around pointer provenance are inherent to the
               | C abstract machine. It's a much more immediate show-
               | stopper on architectures that don't have a flat address
               | space, and the C abstract machine doesn't assume a flat
               | address space because it supports architecture where
               | that's not true. My understanding is that reflects some
               | oddball historical architectures that aren't relevant
               | anymore, nowadays that includes CHERI.
        
               | uecker wrote:
               | Historically, the reason was was often niche
               | architectures. But sometimes certain behavior dies out
               | and we can make semantics more strict. For example, two's
               | complement is now a requirement for C. Still, we did not
               | make signed overflow defined. The reasons are
               | optimization and - maybe surprising for some - safety. UB
               | can be used to insert the compile-time checks we need to
               | make things safe, but often we can not currently require
               | everyone to do this. At the same time, making things
               | defined may make things worse. For example, finding
               | wraparound bugs in unsigned arithmetic - though well-
               | defined - is a difficult and serious problem. For signed
               | overflow, you use a compiler flag and this is not
               | exploitable anymore (could still be a DoS).
        
               | pjmlp wrote:
               | People keep forgetting that SPARC ADI did it first with
               | hardware memory tagging for C.
        
               | tialaramex wrote:
               | > It's not something that exists in the hardware
               | 
               | This is sort of on the one hand not a meaningful claim,
               | and then on the other hand not even really true if you
               | squint anyway?
               | 
               | Firstly the hardware does not have pointers. It has
               | addresses, and those really _are_ integers. Rust 's
               | addr() method on pointers gets you just an address, for
               | whatever that's worth to you, you could write it to a log
               | maybe if you like ?
               | 
               | But the Morello hardware demonstrates CHERI, an ARM
               | feature in which a pointer has some associated
               | information that's _not_ the address, a sort of hardware
               | provenance.
        
               | jcranmer wrote:
               | Pointer provenance probably dates back to the 70s,
               | although not under that name.
               | 
               | The essential idea of pointer provenance is that it is
               | somehow possible to enumerate all of the uses of a memory
               | location (in a potentially very limited scope). By the
               | time you need to introduce something like "volatile" to
               | indicate to the compiler that there are unknown uses of a
               | variable, you have to concede the point that the compiler
               | needs to be able to track all the known uses within a
               | compiler--and that process, of figuring out known uses,
               | is pointer provenance.
               | 
               | As for optimizations, the primary optimization impacted
               | by pointer provenance is... moving variables from stack
               | memory to registers. It's basically a prerequisite for
               | doing _any_ optimization.
               | 
               | The thing is that traditionally, the pointer provenance
               | model of compilers is generally a hand-wavey "trace
               | dataflow back to the object address's source", which
               | breaks down in that optimizers haven't maintained source-
               | level data dependency for a few decades now. This hasn't
               | been much of a problem in practice, because breaking data
               | dependencies largely requires you to have pointers that
               | have the same address, and you don't really run into a
               | situation where you have two objects at the same address
               | _and_ you 're playing around with pointers to their
               | objects in a way that might cause the compiler to break
               | the dependency, at least outside of contrived examples.
        
             | ncruces wrote:
             | The current standard still says integer-to-pointer
             | conversions are implementation defined ( _not_ undefined)
             | and furthermore _" intended to be consistent with the
             | addressing structure of the execution environment"_ (that's
             | a direct quote).
             | 
             | I have an execution environment, Wasm, where doing this is
             | pretty well defined, in fact. So if I want to read the
             | memory at address 12345, which is within bounds of the
             | linear memory (and there's a builtin to make sure), why
             | should it be undefined behavior?
             | 
             | And regarding pointer provenance, why should going through
             | a pointer-to-integer and integer-to-pointer conversions try
             | to preserve provenance at all, and be undefined behavior in
             | situations where that provenance is ambiguous?
             | 
             | The reason I'm using integer (rather than pointer)
             | arithmetic is _precisely_ so I don 't have to be bound by
             | pointer arithmetic rules. What _good_ purpose does it serve
             | for this to be undefined (rather than implementation
             | defined) beyond preventing certain programs to be
             | meaningfully written at all?
             | 
             | I'm genuinely curious.
        
               | uecker wrote:
               | I fully agree with your analysis but compilers writers
               | did think the could bend the rules, hence it was
               | necessary to clarify that pointer-to-integer casts do
               | work as intended. This still not in ISO C 23 btw because
               | some compiler vendors did argue against it. But it is a
               | TS now. If you are, please file bugs against your
               | compilers.
        
               | gpderetta wrote:
               | Do you fully agree? I finally went and read n3005.pdf.
               | The important item there is that a cast to integer
               | exposes the pointer and now the compiler must be
               | conservative and assume that the pointed object might be
               | changed via non trackable pointers. This seems quite a
               | reasonable compromise to make existing code work without
               | affecting the vast majority of objects whose address is
               | never cast to an integer. But ncruces wants defined
               | semantics for arbitrary forged pointers.
        
               | uecker wrote:
               | You are right, I wasn't thinking straight. I do not fully
               | agree. Creating arbitrary pointers can not work. Forging
               | pointers to implementation-defined memory region would be
               | ok though.
        
               | ncruces wrote:
               | Why can't it work though?
               | 
               | And I'm taking about both things.
               | 
               |  _Integer_ arithmetic that produces pointers that are
               | just out of bounds of an object. Why can 't this work?
               | Why can't the compiler assume that, since I explicitly
               | converted a pointer to an integer, the pointed-to object
               | can't be put into a register, or made to go out or scope
               | early?
               | 
               | Second, fabricating pointers. If I have a pointer to
               | mmap/sbrk memory, shouldn't I be allowed to "fabricate"
               | arbitrary pointers from integers that point into that
               | area? If not, why not?
               | 
               | Finally Wasm. The linear memory is addressable from
               | address 0 to __builtin_wasm_memory_size * PAGESIZE. Given
               | this, and except maybe the address at zero, why should it
               | be undefined behavior to dereference any other address?
               | 
               | What's the actual advantage to making these undefined
               | behavior? What to we gain in return?
        
               | bcrl wrote:
               | It is important to understand why undefined behaviour has
               | proliferated over the past ~25 years. Compiler developers
               | are (like the rest of us) under pressure to improve
               | metrics like the performance of compiled code. Often
               | enough that's because a CPU vendor is the one paying for
               | the work and has a particular target they need to reach
               | at time of product launch, or there's a new optimization
               | being implemented that has to be justified as showing a
               | benefit on existing code.
               | 
               | The performance of compilers is frequently measured using
               | the SPEC series of CPU benchmarks, and one of the main
               | constraints of the series SPEC series of tests is that
               | the source code of the benchmark cannot be changed. It is
               | static.
               | 
               | As a result, compiler authors have to find increasingly
               | convoluted ways to make it possible for various new
               | compiler optimizations to be applied to the legacy code
               | used in SPEC. Take 403.gcc: it's based on gcc version 3.2
               | which was released on August 14th 2002 -- nearly 23 years
               | ago.
               | 
               | By making certain code patterns undefined behaviour,
               | compiler developers are able to relax the constraints and
               | allow various optimizations to be applied to legacy code
               | in places which would not otherwise be possible. I
               | believe the gcc optimization to eliminate NULL pointer
               | checks when the pointer is dereferenced was motivated by
               | such a scenario.
               | 
               | In the real world code tends to get updated when
               | compilers are updated, or when performance optimizations
               | are made, so there is no need for excessive compiler
               | "heroics" to weasel its way into making optimizations
               | apply via undefined behaviour. So long as SPEC is used to
               | measure compiler performance using static and unchanging
               | legacy code, we will continue to see compiler developers
               | committing undefined behaviour madness.
               | 
               | The only way around this is for non-compiler developer
               | folks to force language standards to prevent compilers
               | from using undefined behaviour to do that which normal
               | software developers considers to be utterly insane code
               | transformations.
        
               | pjmlp wrote:
               | Dr. Dobbs used to have articles with those benchmarks,
               | here are a couple of examples,
               | 
               | https://dl.acm.org/doi/10.5555/11616.11617
               | 
               | https://jacobfilipp.com/DrDobbs/articles/DDJ/1991/9108/91
               | 08h...
        
               | uecker wrote:
               | Language standards have much less power than people think
               | and compiler-vendors are of course present in the
               | standard working groups. Ultimately, the users need to
               | put pressure on the compiler vendors. Please file bugs -
               | even if this often has no effect, it takes away the
               | argument "this is what our users want". Also please
               | support compilers based on how they deal with UB and not
               | on the latest benchmark posted somewhere.
        
               | bcrl wrote:
               | Language standards have plenty of power over compiler
               | vendors, however, very few people that are not involved
               | in writing compilers tend to participate in the standards
               | process. Standards bodies bend to the will of those
               | participating.
        
               | SkiFire13 wrote:
               | > I have an execution environment, Wasm, where doing this
               | is pretty well defined, in fact. So if I want to read the
               | memory at address 12345, which is within bounds of the
               | linear memory (and there's a builtin to make sure), why
               | should it be undefined behavior?
               | 
               | How would you define it? Especially in a way that is
               | consistent with the rest of the language and allows
               | common optimizations (remember that C supports variables,
               | which may or may not be stored in memory)?
        
               | ncruces wrote:
               | Just read whatever is at address 12345 of the linear
               | memory. Doesn't matter what that is. If it's an object,
               | if it was malloc'ed, if it's the "C stack", a "global".
               | 
               | It's the only way to interpret *(uint64_t*)(12345) when
               | the standard says that a integer-to-pointer conversion is
               | _" intended to be consistent with the addressing
               | structure of the execution environment"_.
               | 
               | There exists an instruction to do that load in Wasm,
               | there's a builtin to check that 12345 points to
               | addressable memory, the load is valid at the assembly
               | level, the standard says the implementation _should_
               | define this to be consistent with the addressing
               | structure of the execution environment, why the heck are
               | we playing games and allowing the compiler to say,
               | "nope, that's not valid, so your entire program is
               | invalid, and we can do what ever we want, no diagnostic
               | required"?
        
               | jcranmer wrote:
               | In a compiler, you essentially need the ability to trace
               | all the uses of an address, at least in the easy cases.
               | Converting a pointer to an integer (or vice versa) isn't
               | really a deal-breaker; it's essentially the same thing as
               | passing (or receiving) a pointer to an unknown external
               | function: the pointer escapes, whelp, nothing more we can
               | do in that case for the most part.
               | 
               | But converting an integer to a pointer creates a problem
               | if you allow that pointer to point to anything--it breaks
               | all of the optimizations that assumed they could trace
               | all of the uses of an address. So you need something like
               | provenance to say that certain back-conversions are
               | illegal. The most permissive model is a no-address-taken
               | model (you can't forge a pointer to a variable whose
               | address was never taken). But most compilers opt instead
               | for a data-dependency-based model: essentially, even
               | integer-based arithmetic of addresses aren't allowed to
               | violate out-of-bounds at the point of dereference. Or at
               | least, they claim to--the documentation for both gcc and
               | llvm have this claim, but both have miscompilation bugs
               | because they don't actually allow this.
               | 
               | The proposal for pointer provenance in C essentially
               | looks at how compilers generally implement things and
               | suggests a model that's closer to their actual
               | implementation: pointer-to-integer exposes the address
               | such that any integer-to-pointer can point to it. Note
               | this is more permissive than the claimed models of
               | compilers today--you're explicitly able to violate out-
               | of-bounds rules here, so long as both objects have had
               | their addresses exposed. There's some resistance to this
               | because adhering to this model also breaks other
               | optimizations (for example, (void*)(uintptr_t)x is not
               | the same as x).
               | 
               | As a practical matter, pointer provenance isn't that big
               | of a deal. It's not hard to come up with examples that
               | illustrate behaviors that cause miscompilation or are
               | undefined specifically because of pointer provenance. But
               | I'm not aware of any application code that was actually
               | miscompiled because the compiler implemented its
               | provenance model incorrectly. The issue gets trickier as
               | you move into systems code that exists somewhat outside
               | the C object model, but even then, most of the relevant
               | code can ignore their living outside the object model
               | since resulting miscompiles are prevented by inherent
               | optimization barriers anyways (note that to get a
               | miscompile, you generally have to simultaneously forge
               | the object's address, have the object's address be known
               | to the compiler already, and have the compiler think the
               | object's address wasn't exposed by other means).
        
         | tialaramex wrote:
         | A _lot_ of that work is basically fixing documentation bugs,
         | labelled  "ghosts" in your text. Places where the ISO document
         | is so bad as a description of C that you would _think_ there 's
         | Undefined Behaviour but it's actually just poorly written.
         | 
         | Fixing the document is worthwhile, and certainly a reminder
         | that WG21's equivalent effort needs to _make the list_ before
         | it can even begin that process on its even longer document, but
         | practical C programmers don 't read the document and since this
         | UB was a "ghost" they weren't tripped by it. Removing items
         | from the list this way does not translate to the meaningful
         | safety improvement you might imagine.
         | 
         | There's not a whole lot of movement there towards actually
         | fixing the problem. Maybe it will come later?
        
           | uecker wrote:
           | Fixing the actual problems is work-in-progress (as my
           | document also indicates), but naturally it is harder.
           | 
           | But the original article also complains about the number of
           | trivial UB.
        
           | taneq wrote:
           | > practical C programmers don't read the document and since
           | this UB was a "ghost" they weren't tripped by it
           | 
           | I would strongly suspect that C compiler implementers very
           | much do read the document, though. Which, as far as I can
           | see, means "ghosts" could easily become actual UB (and worse,
           | sneaky UB that you wouldn't expect.)
        
             | tialaramex wrote:
             | The previous language might cause a C compiler developer to
             | get very _confused_ because it seems as though they can
             | choose something else but what it is isn 't specified, but
             | almost invariably eventually they'll realise oh, it's just
             | badly worded and didn't mean "should" there.
             | 
             | It's like one of those tricky self-referential parlor box
             | statements. "The statement on this box is not true"? Thanks
             | I guess. But that's a game, the puzzles are supposed to be
             | like that, whereas the mission of the ISO document was not
             | to confuse people, so it's good that it is being improved.
        
               | uecker wrote:
               | Most of the "ghosts" are indeed just cleaning up the
               | wording. But compiler writers historically often used any
               | excuse that the standard is not clear to justify
               | aggressive optimization. This starts with an overreaching
               | interpretation of UB itself, to wacky concepts such as
               | time-travel, wobbly numbers, incorrect implementation of
               | aliasing (e.g. still in clang), and pointer-to-integer
               | round trips.
        
               | tialaramex wrote:
               | I'm sure the compiler authors will disagree that they
               | were "using any excuse". From their point of view they
               | were merely making transformations between equivalent
               | programs, and so any mistake is _either_ that these are
               | not in fact equivalent programs because they screwed up -
               | which is certainly sometimes the case - or the standard
               | should not have said they were equivalent but it did.
               | 
               | One huge thing they have on their side is that their
               | implementation is concrete. Whatever it is that, say, GCC
               | does is de facto actually a thing a compiler can do. The
               | standards bodies (and WG21 has been worse by some margin,
               | but they're both guilty) may standardize anything, but
               | concretely the compiler can only implement some things.
               | "Just do X" where X isn't practical works fine on paper
               | but is not implementable. This was the fate of the
               | Consume ordering. Consume/ Release works fine on paper,
               | you "just" need to have whole program analysis to
               | implement it. Well of course that's not practical so it's
               | not implemented.
        
               | uecker wrote:
               | They sometimes screwed up, sometimes just because of
               | bugs, or because different optimization passes had
               | different assumptions that are inconsistent. This somehow
               | contradicts your second point. Compiler have something
               | things implemented which may be concrete on some sense
               | (because it is in a compiler), but still not really a
               | "thing" because it is a mess nobody can formalize using a
               | coherent set of rules.
               | 
               | But then, they also sometimes misread the standard in
               | ways I can't really understand. This often can be seen
               | when the "interpretation" changes over time. Earlier
               | compilers (or even earlier parts of the same compiler)
               | implement the standard as written, some new optimization
               | pass has some creative interpretation.
        
               | tialaramex wrote:
               | Certainly compiler developers are only human, and many of
               | them write C++ so they're humans working with a terrible
               | programming language, I wouldn't sign up for that either
               | (I have written small contributions to compilers, but not
               | in C++). I still don't see "any excuses". I see more
               | usual human laziness and incompetence, LLVM for example
               | IMNSHO doesn't work hard enough to ensure their IR has
               | coherent semantics and to deliver on those semantics.
               | 
               | The compiler bug I'm most closely following, and which I
               | suspect you have your eye on too is:
               | https://gcc.gnu.org/bugzilla/show_bug.cgi?id=119472 aka
               | https://github.com/rust-lang/rust/issues/107975
               | https://github.com/llvm/llvm-project/issues/45725
               | 
               | But it seems like it's just that everybody fucked this up
               | in similar ways, that's two different major compiler
               | backends! I wouldn't be surprised if Microsoft (whose
               | code we can't see) find that they don't get this quite
               | right either.
        
             | Sharlin wrote:
             | If I understand correctly, the "ghosts" are _vacuously_ UB.
             | As in, the standard specifies that if X, then UB, but X can
             | in fact never be true according to the standard.
        
       | kazinator wrote:
       | In C, using uninitialized data is undefined behavior only if:
       | 
       | - it is an automatic variable whose address has not been taken;
       | or
       | 
       | - the uninitialized object' bits are such that it takes on a non-
       | value representation.
        
       | laauraa wrote:
       | >Uninitialized data
       | 
       | They at least fixed this in c++26. No longer UB, but "erroneous
       | behavior". Still some random garbage value (so an uninitialized
       | pointer will likely lead to disastrous results still), but the
       | compiler isn't allowed to fuck up your code, it has to generate
       | code as if it had some value.
        
         | kazinator wrote:
         | C also fixed it in its way.
         | 
         | Access to an uninitialized object defined in automatic storage,
         | whose address is not taken, is UB.
         | 
         | Access to any uninitialized object whose bit pattern is a non-
         | value, likewise.
         | 
         | Otherwise, it's good: the value implied by the bit pattern is
         | obtained and computation goes on its merry way.
        
         | tialaramex wrote:
         | It won't be a "random garbage value" but is instead a value the
         | compiler chose.
         | 
         | In effect if you don't opt out your value _will_ always be
         | initialized but not to a useful value you chose. You can think
         | of this as similar to the (current, defanged and deprecated as
         | well as unsafe) Rust std::mem::uninitialized()
         | 
         | There were earlier attempts to make this value zero, or rather,
         | as many 0x00 bytes as needed, because on most platforms that's
         | markedly cheaper to do, but unfortunately some C++ would
         | actually have _worse_ bugs if the  "forgot to initialize" case
         | was reliably zero instead.
        
           | eru wrote:
           | What are these worse bugs?
        
             | tialaramex wrote:
             | The classic thing is, we're granting user credentials -
             | maybe we're a login proces, or a remote execution helper -
             | and we're on Unix. In some corner case we forget to fill
             | out the user ID. So it's "random noise". Maybe in the
             | executable distributed to your users it was 0x4C6F6769
             | because the word "Login" was in that memory in some other
             | code and we never initialized it so...
             | 
             | Bad guys find the corner case and they can now authenticate
             | as user 0x4C6F6769 which doesn't exist and so that's
             | useless. But - when we upgrade to C++ 26 with the
             | hypothetical zero "fix" now they're root instead!
        
         | account42 wrote:
         | That's unfortunate.
        
       | kazinator wrote:
       | Undefined behavior only means that ISO C doesn't give
       | requirements, not that nobody gives requirements. Many useful
       | extensions are instances where undefined behavior is documented
       | by an implementation.
       | 
       | Including a header that is not in the program, and not in ISO C,
       | is undefined behavior. So is calling a function that is not in
       | ISO C and not in the program. (If the function is not anywhere,
       | the program won't link. But if it is somewhere, then ISO C has
       | nothing to say about its behavior.)
       | 
       | Correct, portable POSIX C programs have undefined behavior in ISO
       | C; only if we interpret them via IEEE 1003 are they defined by
       | that document.
       | 
       | If you invent a new platform with a C compiler, you can have it
       | such that #include <windows.h> reformats all the attached storage
       | devices. ISO C allows this because it doesn't specify what
       | happens if #include <windows.h> successfully resolves to a file
       | and includes its contents. Those contents could be anything,
       | including some compile-time instruction to do harm.
       | 
       | Even if a compiler's documentationd doesn't grant that a certain
       | instance of undefined behavior is a documented extension, the
       | existence of a _de facto_ extension can be inferred empirically
       | through numerous experiments: compiling test code and reverse
       | engineering the object code.
       | 
       | Moreover, the source code for a compiler may be available; the
       | behavior of something can be inferred from studying the code. The
       | code could change in the next version. But so could the
       | documentation; documentation can take away a documented extension
       | the same way as a compiler code change can take away a _de facto_
       | extension.
       | 
       | Speaking of object code: if you follow a programming paradigm of
       | verifying the object code, then undefined behavior becomes moot,
       | to an extent. You don't trust the compiler anyway. If the machine
       | code has the behavior which implements the requirements that your
       | project expects of the source code, then the necessary thing has
       | been somehow obtained.
        
         | pjmlp wrote:
         | Unfortunely it also means that when the programmer fails to
         | understand what undefined behaviour is exposed on their code,
         | the compiler is free to take advantage of that to do the
         | ultimate performance optimizations as means to beat compiler
         | benchmarks.
         | 
         | The code change might come in something as innocent as a bug
         | fix to the compiler.
        
           | account42 wrote:
           | Ah yes, the good old "compiler writers only care about
           | benchmarks and are out to hurt everyone else" nonsense.
           | 
           | I for one am glad that compilers can assume that things that
           | can't happen according to the language do in fact not happen
           | and don't bloat my programs with code to handle them.
        
             | adwn wrote:
             | > _I for one am glad that compilers can assume that things
             | that can 't happen according to the language do in fact not
             | happen and don't bloat my programs with code to handle
             | them._
             | 
             | Yes, unthinkable happenstances like _addition on fixed-
             | width integers overflowing_! According to the language,
             | signed integers can 't overflow, so code like the
             | following:                   int new_offset =
             | current_offset + 16;         if (new_offset <
             | current_offset)             return -1; // Addition
             | overflowed, something's wrong
             | 
             | can be optimized to the much leaner                   int
             | new_offset = current_offset + 16;
             | 
             | Well, _I_ sure am glad the compiler helpfully reduced the
             | bloat in my program!
        
               | account42 wrote:
               | Garbage in, garbage out. Stop blaming the compiler for
               | your bad code.
        
             | titzer wrote:
             | Moral hazard here. The rest of us, and all of society, now
             | rests on a huge pile of code written by incorrigible misers
             | who imagined themselves able to write perfect, bug-free
             | code that would go infinitely fast because bad things never
             | happen. But see, there's bugs in your code and other people
             | pay the cost.
        
               | account42 wrote:
               | There's bugs in your code without undefined behavior too.
               | Go use a different language if you don't care about
               | performance, there are many to choose from.
        
               | pjmlp wrote:
               | Not only do I care about performance, the languages I
               | use, are able to delivery both safety and performace at
               | the level required for project delivery.
               | 
               | Unfortunely too many folks still pretend C is some kind
               | of magic portable Assembly language that no other
               | language on Earth is able to achieve the same.
               | 
               | Also if I care enough about ultimate performace, like
               | anyone that actually cares about performance, I dust off
               | my Assembly programming skills, alongside algorithms,
               | datastructures and computer organisation.
        
               | kazinator wrote:
               | There is an incredible amount of C out there relative to
               | how the sky basically isn't falling.
        
               | titzer wrote:
               | Ransomware attacks against hospitals and a dark extortion
               | economy churning tens if not hundreds of billions of
               | dollars a year in losses and waste.
               | 
               | What would the "sky falling" look like to you? If you're
               | expecting dramatic movie scenes like something out of Mr
               | Robot, I'm afraid the reality is more mundane, just a
               | never-ending series of basic programming errors that turn
               | into remote code execution exploits because of language
               | and compiler choices by people who don't pay the costs.
        
               | kazinator wrote:
               | To completely eliminate the possibility of ransomware
               | attack, you need an incredibly locked down platform, and
               | users who are impervious to social engineering.
               | 
               | Vulnerabilities to ransomware (and other forms of
               | malware) can be perpetrated without a single bad pointer
               | being dereferenced.
               | 
               | For instance, a memory-safe e-mail program can
               | automatically open an attachment, and the memory-safe
               | application which handles the attachment can blindly run
               | code embedded in the document in a leaky sandbox.
               | 
               | There is an incredible amount of infrastructure out there
               | that depends on C. Embedded devices, mobile devices,
               | desktops, servers. Network stacks, telephony stacks,
               | storage, you name it. Encryption, codecs, ...
               | 
               | Sky is falling would mean all of it would be falling down
               | so badly that, for instance, you would have about a 50%
               | chance of connecting a server that is more than four hops
               | away.
        
         | throw-qqqqq wrote:
         | > Undefined behavior only means that ISO C doesn't give
         | requirements, not that nobody gives requirements. Many useful
         | extensions are instances where undefined behavior is documented
         | by an implementation.
         | 
         | True, most compilers have sane defaults in many cases for
         | things that are technically undefined (like take sizeof(void _)
         | or do pointer arithmetic on something other than a char_ ). But
         | not all of these cases can be saved by sane defaults.
         | 
         | Undefined behavior means the compiler can replace the code with
         | whatever. So if you e.g. compile optimizing for size, the
         | compiler will rip out the offending code, as replacing it with
         | nothing yields the greatest size optimization.
         | 
         | See also John Regehr's collection of UB-Canaries:
         | https://github.com/regehr/ub-canaries
         | 
         | Snippets of software exhibiting undefined behavior, executing
         | e.g. both the true and the false branch of an if-statement or
         | none etc. UB should not be taken lightly IMO...
        
           | eru wrote:
           | > [...] undefined behavior, executing e.g. both the true and
           | the false branch of an if-statement or none etc.
           | 
           | Or replacing all you mp3s with a Rick Roll. Technically
           | legal.
           | 
           | (Some old version of GHC had a hilarious bug where it would
           | delete any source code with a compiler error in it. Something
           | like this would technically legal for most compiler errors a
           | C compiler could spot.)
        
         | quietbritishjim wrote:
         | > Including a header that is not in the program, and not in ISO
         | C, is undefined behavior.
         | 
         | What is this supposed to mean? I can't think of any
         | interpretation that makes sense.
         | 
         | I think ISO C defines the executable program to be something
         | like the compiled translation units linked together. But header
         | files do not have to have any particular correspondence to
         | translation units. For example, a header might declare
         | functions whose definitions are spread across multiple
         | translation units, or define things that don't need any
         | definitions in particular translation units (e.g. enum or
         | struct definitions). It could even play macro tricks which
         | means it declares or defines different things each time you
         | include it.
         | 
         | Maybe you mean it's undefined behaviour to include a header
         | file that declares functions that are not defined in any
         | translation unit. I'm not sure even that is true, so long as
         | you don't use those functions. It's definitely not true in C++,
         | where it's only a problem (not sure if it's undefined exactly)
         | if you ODR-rule use a function that has been declared but not
         | defined anywhere. (Examples of ODR-rule use are calling or
         | taking the address of the function, but not, for example, using
         | sizeof on an expression that includes it.)
        
           | kazinator wrote:
           | > I can't think of any interpretation that makes sense
           | 
           | Start with a concrete example. A header that is not in our
           | program, or described in ISO C. How about:
           | #include <winkle.h>
           | 
           | Defined behavior or not? How can an implementation respond to
           | this #include while remaining conforming? What are the limits
           | on that response?
           | 
           | > But header files do not have to have any particular
           | correspondence to translation units.
           | 
           | A header inclusion is just a mechanism that brings
           | preprocessor tokens into a translation unit. So, what does
           | the standard tell us about the tokens coming from #include
           | <winkle.h> into whatever translation unit we put it into?
           | 
           | Say we have a single file program and we made that the first
           | line. Without that include, it's a standard-conforming Hello
           | World.
        
             | quietbritishjim wrote:
             | Do you just meant an attempt to include a file path that
             | couldn't be found? That's not a correct usage of the term
             | "program" - that refers to the binary output of the
             | compilation process, whereas you're taking about the source
             | files that are the input to the compilation. That sounds a
             | bit pedantic but I really didn't understand what you meant.
             | 
             | I just checked, and if you attempt to include a file that
             | cannot be found (in the include path, though it doesn't use
             | that exact term) then that's a constraint violation and the
             | compiler is required to stop compilation and issue a
             | diagnostic. Not undefined behaviour.
        
               | kazinator wrote:
               | Yes; we are more interested in the other case: it happens
               | to be found.
               | 
               | What are the requirements then?
        
               | quietbritishjim wrote:
               | I don't get your point then. If the file is found then
               | there is no undefined behaviour in the process of the
               | file being included. There might be undefined behaviour
               | in the overall translation unit after the text has been
               | substituted in, but that's nothing to do with the
               | preprocessor.
        
               | kazinator wrote:
               | > If the file is found then there is no undefined
               | behaviour in the process of the file being included.
               | 
               | Correct; but processing doesn't stop there.
               | 
               | > _There might be undefined behaviour in the overall
               | translation unit_
               | 
               | But what does that mean; how do you infer that there
               | _might_ be undefined behavior?
               | 
               | Does ISO C define the behavior, or does it not?
               | 
               | ISO C has nothing to say about what is in #include
               | <winkle.h> if such a header is found and didn't come from
               | the program.
               | 
               | Without having anything to say about what is in it, if it
               | is found at all, ISO C cannot be giving a definition of
               | behavior of the tokens that are substituted for that
               | #include.
        
             | im3w1l wrote:
             | I think we are slowly getting closer to the crux of the
             | matter. Are you saying that it's a problem to include files
             | from a library since they are "not in our program"? What
             | does that phrase actually mean? What is the bounds of "our
             | program" anyway? Couldn't it be the set {main.c, winkle.h}
        
               | kazinator wrote:
               | > What is the bounds of our program?
               | 
               | N3220: 5.1.1.1 Program Structure
               | 
               | A C program is not required to be translated in its
               | entirety at the same time. The text of the program is
               | kept in units called source files, (or preprocessing
               | files) in this document. A source file together with all
               | the headers and source files included via the
               | preprocessing directive #include is known as a
               | preprocessing translation unit. After preprocessing, a
               | preprocessing translation unit is called a translation
               | unit. Previously translated translation units may be
               | preserved individually or in libraries. The separate
               | translation units of a program communicate by (for
               | example) calls to functions whose identifiers have
               | external linkage, manipulation of objects whose
               | identifiers have external linkage, or manipulation of
               | data files. Translation units may be separately
               | translated and then later linked to produce an executable
               | program.
               | 
               | > Couldn't it be the set {main.c, winkle.h}
               | 
               | No; in this discussion it is important that <winkle.h> is
               | understood _not_ to be part of the program; no such
               | header is among the files presented for translation,
               | linking and execution. Thus, if the implementation doesn
               | 't resolve #include <winkle.h> we get the uninteresting
               | situation that a constraint is violated.
               | 
               | Let's focus on the situation where it so happens that
               | #include <winkle.h> does resolve to something in the
               | implementation.
        
               | quietbritishjim wrote:
               | The bit of the standard that you've quoted says that the
               | program consists of all files that are compiled into it,
               | including all files that are found by the #include
               | directive. So, if <winkle.h> does successfully resolve to
               | something, then it must be part of the program by
               | definition because that's what "the program" means.
               | 
               | Your question about an include file that isn't part of
               | the program just doesn't make any sense.
               | 
               | (Technically it says that those files together make up
               | the "program text". As my other comment says, "program"
               | is the binary output.)
        
               | kazinator wrote:
               | I see what you are getting at. Programs consist of
               | materials that are presented to the implementation, and
               | also of materials that come from the implementation.
               | 
               | So what I mean is that no file matching <winkle.h> has
               | been presented as part of the external file set given to
               | the implementation for processsing.
               | 
               | I agree that if such a file is found by the
               | implementation it becomes part of the program, as makes
               | sese and as that word is defined by ISO C, so it is not
               | right terminology to say that the file is not part of the
               | program, yet may be found.
               | 
               | If the inclusion is successful, though, the content of
               | that portion of that program is not defined by ISO C.
        
               | quietbritishjim wrote:
               | It still seems like you have invented some notion of
               | "program" that doesn't really exist. Most suspicious is
               | when you say this:
               | 
               | > So what I mean is that no file matching <winkle.h> has
               | been presented as part of the external file set given to
               | the implementation for processsing.
               | 
               | The thing is, there is no "external file set" that
               | includes header files, so this sentence makes no sense.
               | 
               | Note that when the preprocessor is run, the only inputs
               | are the file being preprocessed (i.e., the .c file) and
               | the _list of directories to find include files_ (called
               | the include path). That 's not really part of the ISO
               | standard, but it's almost universal in practice. Then the
               | output of the preprocessor is passed to the compiler, and
               | now it's all one flat file so there isn't even a concept
               | of included files at this point. The object files from
               | compilation are then passed to the linker, which again
               | doesn't care about headers (or indeed the top-level
               | source files). There are more details in practice
               | (especially with libraries) but that's the essence.
               | 
               | I wonder if your confusion is based on seeing header
               | files in some sort of project-like structure in an IDE
               | (like Visual Studio). But those are just there for ease
               | of editing - the compiler (/preprocessor) doesn't know or
               | care which header files are in your IDE's project, it
               | only cares about the directories in the include path. The
               | same applies to CMake targets: you can add include files
               | with target_sources(), but that's just to make them show
               | up in any generated IDE projects; it has no effect on
               | compilation.
               | 
               | Or are you just maybe saying that the developer's file
               | system isn't part of the ISO C standard, so this whole
               | textual inclusion process is by some meaning not defined
               | by the standard? If so, I don't think that matches the
               | conventional meaning of undefined behaviour.
               | 
               | If it's neither of those, could you clarify what exactly
               | you mean by "the external file set given to the
               | implementation for processing"?
        
               | kazinator wrote:
               | Let's drop the word "program" and use something else,
               | like "project", since the word "program" is normative in
               | ISO C.
               | 
               | The "project" is all the files going into a program
               | supplied other than by the implementation.
               | 
               | C programs can contain #include directives. Those
               | #include directives can be satisfied in one of three
               | ways: they can reference a standard header which is
               | specified by ISO C and hence effectively built into the
               | hosted language, such as <stdio.h>.
               | 
               | C programs can #include a file from the project. For
               | instance someone's "stack.c" includes "stack.h". So yes,
               | there is an external file set (the project) which can
               | have header files.
               | 
               | C programs can also #include something which is neither
               | of the above. That something might be not found
               | (constraint violation). Or it might be found (the
               | implementation provides it). For instance <sys/mmap.h>:
               | not in your project, not in ISO C.
               | 
               | My fictitious <winkle.h> falls into this category. (It
               | deliberately doesn't look like a common platform-specific
               | header coming from any well-known implementation---but
               | that doesn't matter to the point).
               | 
               | > Or are you just maybe saying that the developer's file
               | system isn't part of the ISO C standard, so this whole
               | textual inclusion process is by some meaning not defined
               | by the standard?
               | 
               | Of course, it isn't, no I'm not saying that. The C
               | standard gives requirements as to how a program (project
               | part and other) is processed by the implementation,
               | including all the translation phases that include
               | preprocessing.
               | 
               | To understand what the requirements are, we must consider
               | the content of the program. We know what the content is
               | of the project parts: that's in our files. We (usually
               | indirectly) know the content of the standard headers,
               | from the standard; we ensure that we have met the rules
               | regarding their correct use and what we may or may not
               | rely on coming form them.
               | 
               | We don't know the content of successfully included
               | headers that don't come from our project or from ISO C;
               | or, rather, we don't know that content just from knowing
               | ISO C and our project. In ISO C, we can't find any
               | requirements as to what is supposed to be there, and we
               | can't find it in our project either.
               | 
               | If we peek into the implementation to see what #include
               | <winkle.h> is doing (and such a peeking is usually
               | possible), we are effectively looking at a document, and
               | then if we infer from that document what the behavior
               | will be, it is a _documented extension_ --- standing in
               | the same place as what ISO C calls undefined behavior.
               | Alternatively, we could look to actual documentation.
               | E.g. POSIX tells us what is in  <fcntl.h> without us
               | having to look for the file and analyze the tokens. When
               | we use it we have "POSIX-defined" behavior.
               | 
               | #include <winkle.h> is in the same category of thing as
               | __asm__ __volatile__ or __int128_t or what have you.
               | 
               | #include <winkle.h> could contain the token
               | __wipe_current_directory_at_compile_time which the
               | accompanying compiler understands and executes as soon as
               | it parses the token. Or __make_demons_fly_out_of_nose. :)
               | 
               | Do you see the point? When you include a nonstandard
               | header that is not coming from your project, and the
               | include succeeds, _anything can happen_. ISO C no longer
               | dictates the requirements as to what the behavior will
               | be. Something unexpected can happen, still at translation
               | time.
               | 
               | Now headers like <windows.h> or <unistd.h> are exactly
               | like <winkle.h>: same undefined behavior.
        
         | gpderetta wrote:
         | You are basically trying to explain the difference between a
         | conforming program and a strictly conforming one.
        
       | pizlonator wrote:
       | I don't buy the "it's because of optimization argument".
       | 
       | And I especially don't buy that UB is there for register
       | allocation.
       | 
       | First of all, that argument only explains UB of OOB memory
       | accesses at best.
       | 
       | Second, you could define the meaning of OOB by just saying
       | "pointers are integers" and then further state that nonescaping
       | locals don't get addresses. Many ways you could specify that, if
       | you cared badly enough. My favorite way to do it involves saying
       | that pointers to locals are lazy thunks that create addresses on
       | demand.
        
         | j16sdiz wrote:
         | > First of all, that argument only explains UB of OOB memory
         | accesses at best.
         | 
         | It explains many loop-unroll and integer overflow as well.
        
         | tialaramex wrote:
         | > Second, you could define the meaning of OOB by just saying
         | "pointers are integers"
         | 
         | This means losing a lot of optimisations, so in fact when you
         | say you "don't buy" this argument you only mean that you don't
         | care about optimisation. Which is fine, but this does mean the
         | "improved" C isn't very useful in a lot of applications, might
         | as well choose Java.
        
           | pizlonator wrote:
           | > This means losing a lot of optimisations
           | 
           | You won't lose "a lot" of optimizations and you certainly
           | won't lose enough for it to make a noticeable difference in
           | any workload that isn't SPEC
        
         | gpderetta wrote:
         | > nonescaping locals don't get addresses
         | 
         | inlining, interprocedural optimizations.
         | 
         | For example, something as an trivial accessor member function
         | would be hard to optimize.
        
           | pjmlp wrote:
           | Safer languages manage similar optimizations without having
           | to rely on UB.
        
             | gpderetta wrote:
             | Well, yes, safer languages prevent pointer forging
             | statically, so provenance is trivially enforced.
             | 
             | And I believe that provenance is an issue in unsafe rust.
        
               | tialaramex wrote:
               | Unlike C++ and (until Martin's work is moved to the
               | actual language ISO document rather than separate) C the
               | Rust language actually has a definition for how
               | provenance is supposed to work.
               | 
               | https://doc.rust-lang.org/std/ptr/index.html#provenance
               | 
               | The definition isn't deemed complete because of aliasing.
               | AIUI The definition we have is adequate if you're OK with
               | treating all edge cases for "Is this an alias?" as "Yes"
               | but eventually Rust will also need to carefully nail down
               | all those edge cases so that you can tread closer without
               | falling off.
        
           | pizlonator wrote:
           | Inlining doesn't require UB
        
             | gpderetta wrote:
             | I didn't claim that. What I mean is that if a pointer
             | escapes into an inlined function and no further, it will
             | still prevent further optimizations if we apply your rule
             | that only non-escaping locals don't get addresses. The main
             | benefit of inlining is that it is effectively a simple way
             | to do interprocedurally optimizations. I.e.
             | inline void add(int* to, int what) { *to += what; }
             | void foo();       void bar() {           int x = 0;
             | add(&x, 1);           foo();           return x;       }
             | 
             | By your rules, optimizing bar to return the constant 1
             | would not be allowed.
        
               | pizlonator wrote:
               | I think you're applying a very strange strawman
               | definition to "nonescaping". It's certainly not the
               | definition I would pick.
               | 
               | The right definition is probably something like:
               | 
               | - pointers that come out of the outside world (syscalls)
               | are escaped. They are just integers.
               | 
               | - pointers to locals have provenance. They point to an
               | abstract location. It is up to the implementation to
               | decide when the location gets an integer value (is in an
               | actual address) and what that value is. The
               | implementation must do this no later than when the
               | pointer to the local escapes.
               | 
               | - pointer values passed to the outside world (syscalls)
               | escape.
               | 
               | - pointer values stored in escaped memory also escape,
               | transitively
               | 
               | That's one possible definition that turns the UB into
               | implementation defined behavior. I'm sure there are
               | others
        
               | gpderetta wrote:
               | I think you have a non-standard definition. An escaping
               | pointer is an address that the compiler cannot fully
               | track (directly or indirectly). It could be to a syscall,
               | it could be a separately compiled function (without LTO),
               | it could even be to a function in the same translation
               | unit if the compiler cannot inline that function nor do
               | sufficient intraprocedural analysis.
               | 
               | Again, I'm not a compiler writer, but my understanding is
               | that non escaping variables can be optimized in SSA form,
               | escaped variables are otherwise treated as memory and the
               | compiler must be significantly more conservative.
               | 
               | In any case, whether a pointer escapes or not depends
               | purely on the compiler capabilities and optimization
               | level, so it would not be sane making a code well defined
               | or UB depending on the compiler or optimization level.
               | 
               | edit: to be more concrete, do you think that in my
               | example the constant folding of the return into return 1
               | should be allowed? And if so, which variant of this code
               | would prevent the optimization and why?
        
               | pizlonator wrote:
               | > Again, I'm not a compiler write
               | 
               | I am a compiler writer.
               | 
               | The definition I gave in my post is general enough to
               | cover all possible compilers (ones that have LTO, ones
               | that are inside a DBT, etc).
               | 
               | Yes the constant folding should be allowed because the
               | pointer to the local never escaped.
        
         | OskarS wrote:
         | No, it's absolutely because of optimization. For instance,
         | C++20 defined signed integer representation as having two's
         | complement, but signed integer overflow is still undefined
         | behaviour. The reason is that if you compile with flags that
         | make it defined, you lose a few percentage points of
         | performance (primarily from preventing loop unrolling and auto-
         | vectorization).
         | 
         | Same thing with e.g. strict aliasing or the various UB that
         | exists in the standard library. For instance, it's UB to pass a
         | null pointer to strlen. Of course, you can make that perfectly
         | defined by adding an `if` to strlen that just returns 0. But
         | then you're adding a branch to every strlen, and C is simply
         | not willing to do that for performance reasons, so they say
         | "this is UB" instead.
         | 
         | Pretty much instance of UB in standard C or C++ is because
         | making it defined would either hamper the optimizer, or it
         | would make standard library functions slower. They don't just
         | make things UB for fun.
        
           | account42 wrote:
           | I wish there was a way to opt into undefined behavior for
           | unsigned overflow. Its rare that wraparound is actually what
           | you want and in many cases overflow is still a bug. Sucks to
           | have to either miss out on potential optimizations or miss
           | out on the guarantee that the value can't be negative.
        
             | OskarS wrote:
             | You do need some way to overflow properly, because
             | sometimes that is what you want. A common example would be
             | PRNGs, which frequently rely on overflow (the classic LCG,
             | for instance). You could argue that should just be a
             | library function or something (e.g. `add_with_overflow`),
             | though that's more C++ than C.
             | 
             | You are absolutely, 100% correct though: I've never seen a
             | case where _accidental_ overflow doesn 't start causing
             | bugs anyway. Like, the Pac-Man kill screen is caused by a
             | byte overflowing (it happens on level 256), and the game
             | goes insane. Pac-Man was written in assembly where overflow
             | is defined behavior, but that doesn't matter at all, the
             | game is still broken. If signed overflow is essentially
             | always a bug anyway, why not make it UB and optimize around
             | it? Especially since it is super-valuable in being able to
             | unroll loops.
             | 
             | People always bring up signed integer overflow as an
             | argument for why UB is scary, and it always seemed like
             | such a bad argument to me. Like, I can understand why
             | people think UB has gone too far in C/C++, but signed
             | overflow is such a bad example. It's one of the most
             | sensible bits of UB in the entire standard, IMHO.
        
             | pizlonator wrote:
             | -fwrapv
        
             | uecker wrote:
             | I recently filed a bug for this:
             | https://gcc.gnu.org/bugzilla/show_bug.cgi?id=116193
        
           | pizlonator wrote:
           | This isn't the reason why the UB is in the spec in the first
           | place. The spec left stuff undefined to begin with because of
           | lack of consensus over what it should do.
           | 
           | For example the reason why 2s complement took so long is
           | because of some machine that ran C that still existed that
           | was 1s complement.
           | 
           | > The reason is that if you compile with flags that make it
           | defined, you lose a few percentage points of performance
           | (primarily from preventing loop unrolling and auto-
           | vectorization).
           | 
           | I certainly don't lose any perf on any workload of mine if I
           | set -fwrapv
           | 
           | If your claim is that implementers use optimization as the
           | excuse for wanting UB, then I can agree with that.
           | 
           | I don't agree that it's a valid argument though. The
           | performance wins from UB are unconvincing, except maybe on BS
           | benchmarks that C compilers overtune for marketing reasons.
        
             | OskarS wrote:
             | > For example the reason why 2s complement took so long is
             | because of some machine that ran C that still existed that
             | was 1s complement.
             | 
             | You're misunderstanding me: as of C++20, there is no other
             | representation in C++ for signed integers other than two's
             | complement (no signed ones' complement, no signed
             | magnitude, nothing else), but signed overflow is _still_
             | UB. It 's not because of obscure machines or hardware, such
             | hardware is not relevant for C++20 and later. The reason
             | for it is performance. From the accepted paper [1]:
             | 
             | > The following polls were taken, and corresponding
             | modifications made to the paper. The main change between
             | [P0907r0] and the subsequent revision is to maintain
             | undefined behavior when signed integer overflow occurs,
             | instead of defining wrapping behavior. This direction was
             | motivated by:
             | 
             | > * Performance concerns, whereby defining the behavior
             | prevents optimizers from assuming that overflow never
             | occurs
             | 
             | You may disagree, you may think they're wrong, but their
             | motivation is performance, that's why this is UB. It's
             | right there in black and white. This was C++, not C, but
             | it's not at all unthinkable that the C standard will also
             | mandate two's complement at some point, and if they do,
             | they almost certainly keep signed overflow undefined for
             | exactly the same reason.
             | 
             | It's not hard to write code that optimizes much better when
             | you use signed loop variables. One of my favorite examples
             | is this function [2] to turn a 3D mesh inside out by
             | flipping the edges of each triangle in a triangle mesh. The
             | godbolt link has two versions of the same function, one
             | with a signed loop variable, one with an unsigned one. The
             | signed one auto-vectorizes and optimizes much better
             | because it can assume that the loop variable never
             | overflows (this version is C++, it's trivial to rewrite it
             | in C and get the same results).
             | 
             | This is why signed overflow is UB.
             | 
             | [1]: https://www.open-
             | std.org/jtc1/sc22/wg21/docs/papers/2018/p09...
             | 
             | [2]: https://godbolt.org/z/a1P5Y17fn
        
               | pizlonator wrote:
               | I agree that the stated motivation for continuing to keep
               | UB is performance.
               | 
               | I know that this is misguided based on my own perf tests
               | and others' perf tests.
               | 
               | Also, it's wrong to say flat out that UB on signed ints
               | is somehow necessary for perf when even a simple perf
               | test shows that it just doesn't matter, and the
               | optimization it enables is quite obscure.
        
       | safercplusplus wrote:
       | A couple of solutions in development (but already usable) that
       | more effectively address UB:
       | 
       | i) "Fil-C is a fanatically compatible memory-safe implementation
       | of C and C++. Lots of software compiles and runs with Fil-C with
       | zero or minimal changes. All memory safety errors are caught as
       | Fil-C panics." "Fil-C only works on Linux/X86_64."
       | 
       | ii) "scpptool is a command line tool to help enforce a memory and
       | data race safe subset of C++. It's designed to work with the
       | SaferCPlusPlus library. It analyzes the specified C++ file(s) and
       | reports places in the code that it cannot verify to be safe. By
       | design, the tool and the library should be able to fully ensure
       | "lifetime", bounds and data race safety." "This tool also has
       | some ability to convert C source files to the memory safe subset
       | of C++ it enforces"
        
         | tialaramex wrote:
         | Fil-C is interesting because as you'd expect it takes a
         | significant performance penalty to deliver this property, if
         | it's broadly adopted that would suggest that - at least in this
         | regard - C programmers genuinely do prioritise their simpler
         | language over mundane ideas like platform support or
         | performance.
         | 
         | The resulting language doesn't make sense for commercial
         | purposes but there's no reason it couldn't be popular with
         | hobbyists.
        
           | eru wrote:
           | Well, you could also treat Fil-C as a sanitiser, like memory-
           | san or ub-san:
           | 
           | Run your test suite and some other workloads under Fil-C for
           | a while, fix any problems report, and if it doesn't report
           | any problems after a while, compile the whole thing with GCC
           | afterwards for your release version.
        
             | safercplusplus wrote:
             | Right. And of course there are still less-performance-
             | sensitive C/C++ applications (curl, postfix, git, etc.)
             | that could have memory-safe release versions.
             | 
             | But the point is also to dispel the conventional wisdom
             | that C/C++ is necessarily intrinsically unsafe. It's a
             | tradeoff between safety, performance and
             | flexibility/compatibility. And you don't necessarily need
             | to jump to a completely different language to get a
             | different tradeoff.
             | 
             | Fil-C sacrifices some performance for safety and
             | compatibility. The traditional compilers sacrifice some
             | safety for performance and flexibility/compatibility. And
             | scpptool aims to provide the option of sacrificing some
             | flexibility for safety and performance. (Along with the
             | other two tradeoffs available in the same program). The
             | claim is that C++ turns out to be expressive enough to
             | accommodate the various tradeoffs. (Though I'm not saying
             | it's always gonna be pretty :)
        
       | roman_soldier wrote:
       | Just use Zig, it fixes all this
        
       | grougnax wrote:
       | Worse languages ever.
        
         | OskarS wrote:
         | C and C++ are languages that brought us UNIX, the Linux kernel,
         | macOS and Windows, the interpreters of virtually every other
         | language in the world, powering virtually all software in the
         | world as well as the vast majority of embedded devices.
         | 
         | Chill the fuck out.
        
         | compiler-guy wrote:
         | Jack Sparrow: "... but you have heard of them."
         | 
         | The dustbin of programming languages is jam packed with
         | elegant, technically terrific, languages that never went
         | anywhere.
        
         | account42 wrote:
         | Except for all the others.
        
       | IshKebab wrote:
       | This asserts that UB was deliberately created for optimisation
       | purposes; not to handle implementation differences. It doesn't
       | provide any evidence though and that seems unlikely to me.
       | 
       | The spec even says:
       | 
       | > behavior, upon use of a _nonportable_ or erroneous program
       | construct or of erroneous data, for which this International
       | Standard imposes no requirements
       | 
       | No motivation is given that I could find, so the actual
       | difference between undefined and implementation defined behaviour
       | seems to be based on whether the behaviour needs to be
       | documented.
        
         | flohofwoe wrote:
         | I'd say the original intent of UB was not the sort of
         | "optimizer exploits" we see today, but to allow wiggle room for
         | supporting vastly different CPUs without having to compromise
         | runtime performance or increasing compiler complexity to
         | balance performance versus correctness. Basically an escape
         | hatch for compilers. The difference to IB also has always been
         | quite fuzzy.
         | 
         | Also the C spec has always been a pragmatic afterthought,
         | created and maintained to establish at least a minimal common
         | feature set expected of C compilers.
         | 
         | The really interesting stuff still only exists outside the spec
         | in vendor language extensions.
        
       | agent327 wrote:
       | I, once again, disagree with the premise that UB is a necessary
       | precondition for optimisation, or that it exists to allow for
       | optimisation. You do not need UB to unroll a loop, inline a
       | function, lift an object or computation out of a loop, etc.
       | Moreover, _most_ UB does not assist in optimisation at all.
       | 
       | The two instances where UB allows for optimisation are as
       | follows:
       | 
       | 1. The 'signed overflow' UB allows for faster array indexing. By
       | ignoring potential overflow, the compiler can generate code that
       | doesn't check for accidental overflow (which would require
       | masking the array index, recomputing the address on each loop
       | iteration). I believe the better solution here would be to
       | introduce a specific type for iterating over arrays that will
       | never overflow; size_t would do fine, and making signed overflow
       | at least implementation defined, if not outright fully defined,
       | after a suitable period during which compilers warn if you use a
       | too-small type for array indexing.
       | 
       | 2. The 'aliasing' UB does away with the need to read/write values
       | to/from memory each time they're used, and is extremely important
       | to performance optimisation.
       | 
       | But the rest? Most of it does precisely nothing for performance.
       | At 'best', the compiler uses detected UB to silently eliminate
       | code branches, but that's something to be feared, not celebrated.
       | It isn't an optimisation if it removes vital program logic,
       | because the compiler could 'demonstrate' that it could not
       | possibly take the removed branch, on account of it containing UB.
       | 
       | The claim in the linked article ("what every C programmer should
       | know") that use of uninitialized variables allows for additional
       | optimisation is incorrect. What it does instead is this: if the
       | compiler see you declare a variable, and then reading from it
       | before writing to it, it has detected UB, and since the rule is
       | that "the compiler is allowed to assume UB does not occur", use
       | that as 'evidence' that that code branch will never occur and can
       | be eliminated. It does not make things go faster; it makes them
       | go _wrong_.
       | 
       | Undefined behaviour, ultimately, exists for many reasons: because
       | the standards committee forgot a case, because the underlying
       | platforms differ too wildly, because you cannot predict in
       | advance what the result of a bug may be, to grandfather in broken
       | old compilers, etc. It does not, in any way, shape, or form,
       | exist _in order to_ enable optimisation. It _allows_ it in some
       | cases, but that is, and never was, not the goal.
       | 
       | Moreover, the phrasing of "the compiler is allowed to assume that
       | UB does not occur" was originally only meant to indicate that the
       | compiler was allowed to emit code as if all was well, without
       | introducing additional tests (for example, to see if overflow
       | occurred or if a pointer was valid) - clearly that would be very
       | expensive or downright infeasible. Unfortunately, over time this
       | has enabled a toxic attitude to grow that turns minor bugs into
       | major disasters, all in the name of 'performance'.
       | 
       | The two bullet points towards the end of the article are both
       | true: the compiler SHOULD NOT behave like an adversary, and the
       | compiler DOES NEED license to optimize. The mistake is thinking
       | that UB is a necessary component of such license. If that were
       | true, a language with more UB would automatically be faster than
       | one with less. In reality, C++ and Rust are roughly identical in
       | performance.
        
       | agalunar wrote:
       | A small nit: the development of Unix began on the PDP-7 in
       | assembly, not the PDP-11.
       | 
       | (The B language was implemented for the PDP-7 before the PDP-11,
       | which are rather different machines. It's sometimes suggested
       | that the increment and decrement operators in C, which were
       | inherited from B, are due to the instruction set architecture of
       | the PDP-11, but this could not have been the case. Per Dennis
       | Ritchie:1
       | 
       | > Thompson went a step further by inventing the ++ and --
       | operators, which increment or decrement; their prefix or postfix
       | position determines whether the alteration occurs before or after
       | noting the value of the operand. They were not in the earliest
       | versions of B, but appeared along the way. People often guess
       | that they were created to use the auto-increment and auto-
       | decrement address modes provided by the DEC PDP-11 on which C and
       | Unix first became popular. This is historically impossible, since
       | there was no PDP-11 when B was developed. The PDP-7, however, did
       | have a few "auto-increment" memory cells, with the property that
       | an indirect memory reference through them incremented the cell.
       | This feature probably suggested such operators to Thompson; the
       | generalization to make them both prefix and postfix was his own.
       | 
       | Another person puts it this way:2
       | 
       | > It's a myth to suggest C's design is based on the PDP-11.
       | People often quote, for example, the increment and decrement
       | operators because they have an analogue in the PDP-11 instruction
       | set. This is, however, a coincidence. Those operators were
       | invented before the language [i.e. B] was ported to the PDP-11.
       | 
       | In any case, the PDP-11 usually gets all the love, but I want to
       | make sure the other PDPs get some too!)
       | 
       | [1] https://www.bell-labs.com/usr/dmr/www/chist.html
       | 
       | [2] https://retrocomputing.stackexchange.com/questions/8869
        
       ___________________________________________________________________
       (page generated 2025-08-12 23:02 UTC)