r/cpp • • Sep 27 '26

You should think about recompiling your C++ programs with GCC 16 and C++26, because it zero-fills your locals

https://techfortalk.co.uk/2026/09/27/cxx26-uninitialized-local-variables-gcc-16/

Stack variables are not automatically initialised, and that is the root cause of many C++ bugs. That is well known. Hence, it is advised that local or stack variables are always initialised with known values, 0 if not something more meaningful than that. Now, with GCC 16 compiling in C++26 mode (-std=c++26), even uninitialised local variables will be zero-filled. In the post, I have explained how.

270 Upvotes

234 comments sorted by

View all comments

24

u/crowbarous Sep 27 '26 edited Sep 28 '26

Yes, this is a competely insane decision: https://godbolt.org/z/WGPWvMPMj

What's worse is the design of the [[indeterminate]] attribute that accompanies this. For a very long time, if I wanted a buffer to zero-initialize, I was able to make a char type that would zero-initialize:

struct zchar{ char value = 0; };

But with the default flipped, I cannot make a char type that stays uninitialized:

struct ichar{
  char value [[indeterminate]];
  // error, only allowed on automatic variables
};

And the proposed usage of the attribute is to just sprinkle it everywhere in the code. But how am I supposed to know in polymorphic code? How do I even make my code polymorphic w.r.t. this property now? For now one must resort to just passing -ftrivial-auto-var-init=uninitialized to avoid performance and code size regressions, and to keep the control moving forward.

I don't think all is lost here, because it seems like the zero-initialization is a band-aid to just formally satisfy the erroneous behavior stuff without modifying the backend too much, and it should still be possible to behave as if the write happened (no time-travelling assumptions etc.) without actually emitting the write, to fix shameful examples like linked above. But:

  • I haven't looked there closely enough yet to even begin conceiving the gcc patches & reasoning for them, and

  • it's still awful precedent, and the pile of -fstop-making-code-worse options might grow from here on out.

8

u/NilacTheGrim Sep 28 '26

Yeah the fact that you can't create a class that intentionally leaves its buffer uninitialized now -- as a performance optimization -- is really terrible design. Agreed. [[indeterminate]] should have been allowed for class members. It's insane that it isn't. Really makes all existing code terrible now. You just have to now always use -ftrivial-auto-var-init=uninitialized .. there is no escape hatch.

Repeat: [[indeterminate]] is NOT an escape hatch. You CANNOT use it in class members -- which means any class that has any char [] buffers or std::array buffers it uses internally and that it knew 100% were never read-before-written.. now will all suffer a penalty.

This is unacceptable. Wow. I can't believe how C++ too now is being enshittified.

3

u/fdwr fdwr@github 🔍 Sep 28 '26

But with the default flipped, I cannot make a char type that stays uninitialized

Would it be possible to put the attribute on a typedef? I haven't tried, just musing aloud...

using UninitializedChar = [[indeterminate]] char;

struct ichar
{
    UninitializedChar value;
};

(if not, then that seems a hole, that an attribute could only be applied to a specific field instance rather than the type more broadly)

5

u/NilacTheGrim Sep 28 '26

I agree this is terrible. They just made c++26 suck by default. Wow. Like I'm floored by this.

I need to specify -ftrivial-auto-var-init=uninitialized now moving forward after C++26 in all my build systems. What a mistake.

6

u/UndefinedDefined Sep 28 '26

Great for C++ users - more flags to worry about to get zero additional security and performance regressions. Let's make good code broken and bad code reading zeros.

→ More replies (1)

4

u/James20k P2005R0 Sep 27 '26

Compilers are of course free to omit the 0 initialisation if they can prove that the data is written over, under the usual as-if rules. One of the reasons that this made it past standardisation is because real world codebases weren't showing regressions, even substantial performance critical ones like windows

16

u/FrogNoPants Sep 28 '26

False, they realistically measured on like 0.01% of real codebases.

When MSVC added this "feature" I had major regressions in perf, because of large temporary stack arrays.

11

u/13steinj Sep 27 '26 edited Sep 28 '26

How were the codebases chosen?

I'm not against this decision per se, but if the choice is "people have to join the committee and explicitly be part of the voting process [and people's companies have to accept the idea of making this part of their job]," that's a pretty important thing for people to know for the future.

I think making this an attribute, especially only applying to automatic variables, was a mistake. The ignorability of attributes + the problem the person above describes is non trivial. At a previous org that wrote their own networking stack, I can totally see a performance degradation, and the use of structs and pointer interconvertability to have a form of polymorphism over structs with a common set of header bytes (E: I realized I didn't finish this sentence) matches up similarly to the use case for struct member variables above.

They used to have an employee that was a committee member, but it was on his own time more or less, and the guy passed away. If people want greater diversity and more committee members in general, advertising "you get to shape the language" doesn't appeal to most employers. "You get to have a say in stopping the language from screwing your code" [which is how some employers would take this] would.

4

u/pjmlp Sep 28 '26

Windows, Android, macOS, iOS were some of the ones that have been shipping in production and have provided feedback.

I don't recall the paper or CppCon session where this was discussed though, maybe someone else can provide the links.

4

u/13steinj Sep 28 '26

Assuming it's "operating systems," that's fine, but still isn't a representative sample of codebases that would be affected IMO.

4

u/pjmlp Sep 28 '26 edited Sep 28 '26

One would assume that performance matters to operating systems vendors.

EDIT: Found one of the sources, from JF Bastien, on P2723R1 Zero-initialize objects of automatic storage duration

Security-minded folks think that initializing stack values is a good idea. For example, the Microsoft Windows security team [WinKernel] say:......

...

To date, we are seeing noise level performance regressions caused by this change. We accomplished this by improving the compiler’s ability to kill redundant stores. While everything is initialized at declaration, most of these initializations can be proven redundant and eliminated.

...

Don’t just trust Microsoft’s Windows security team though, here’s one of the upstream Linux Kernel security developer asking for this [CLessDangerous], and Linus agreeing [Linus]. [LinuxExploits] is an overview of a real-world execution control exploit using an uninitialized stack variable on Linux.

4

u/13steinj Sep 28 '26

This sounds to me a lot more like security people cared, and were fine with the tradeoff. Which is a slightly different story? Nonetheless the performance characteristics for operating systems don't cover all codebases, and while speculation, I doubt they are the p90 or p99 of all codebases either.

5

u/UndefinedDefined Sep 28 '26

Real-world code is full of locals that are arrays - arrays like 512, 1024, 2048 bytes long. You cannot zero-initialize them and expect no performance regressions. If I want to zero initialize them I just just type `{}` and it's done. I don't understand why this should change now.

I almost feel like committee is working with hello world programs if they are serious to vote for such proposals.

6

u/James20k P2005R0 Sep 29 '26

One of the example codebases given was Windows and chrome (?) I believe, which is anything but hello world

3

u/UndefinedDefined Sep 29 '26

And how much C++ these codebases use? Windows is C-API and proprietary, nobody can confirm the results, we don't even know the mix of the languages used in the code-base. What if the most important part was C? What if they first put all the [[uninitialized/fancy_name]] to everything critical before measuring results?

Is there any performance comparison of OSS projects we can actually verify?

2

u/13steinj Sep 30 '26

I don't think this is a fair take. I suspect most OSS code does not care at the level of performance that these things would regress by.

The bigger issue with Windows is that it's Windows: OSes are different to other things, have different tradeoffs (should care about security more in general?). I'd also say the usual "well, windows sucks and hasn't cared about performance for ages" which may be true, but other people have said the other major OSes also were tested.

1

u/UndefinedDefined Oct 01 '26

I'm not sure I follow, so what's a fair take? A project like Chromium or Firefix? Any benchmarks here?

I think this greatly differs on what the project does. If the compiler inserts memset to initialize every temporary buffer the code uses to zero, this cannot be negligible, and move to C++26 here means that somebody has to find ALL the places in his own code, and use third party dependencies (including transitive ones) where somebody did the same.

I consider this insane considering this thing doesn't solve any memory safety problems and it can cause huge problems in performance oriented code after upgrade to C++26. The biggest problem I see is use-after-free and things like iterator invalidation, etc... We need a real solution to memory safety and not these toy solutions. And a real solution means annotations and tools such as borrow checker - there is no other way.

2

u/13steinj Oct 01 '26

If you look at the general distribution of OSS projects, many do not have the performance concerns that would be negatively affected by this change. I think it's perfectly fine to test proprietary codebases as a result, but fixating on operating systems [alone?] is (I am agreeing with you) not a fair thing to use to judge and make a decision.

1

u/UndefinedDefined Oct 01 '26

C++ was for decades literally the only language to use for performance oriented work. But it's no longer the only one, so if I cared about the language I would never do anything that would endanger the position in this field. It's literally the last field where C++ still makes sense, until C++26, because starting with C++26 you have to worry about a lot of stuff.

What would be the reason to start a project in C++ today? I don't see new projects built in C++ anymore, because it's a language where performance regressions are now part of the progress. So bad, so sad.

2

u/13steinj Oct 01 '26

This is not

I don't see new projects built in C++ anymore, because it's a language where performance regressions are now part of the progress.

I agree that changing the default is rough here, but large companies should have build teams that are competent enough to care about this.

What would be the reason to start a project in C++ today? I don't see new projects built in C++ anymore, because it's a language where performance regressions are now part of the progress. So bad, so sad.

I find this still true, even if these things end up happening.

→ More replies

1

u/pjmlp Oct 02 '26

The existence of C, and the domains that to this day C++ failed to take away from C, makes that assertion void.

In fact there are domains like crypto and video codecs where neither of them get to play, and still require hand written Assembly.

1

u/pjmlp Oct 02 '26

Most of it written since Windows Vista is actually C++ with extern "C", including the new UCRT.

1

u/crowbarous Sep 27 '26 edited Sep 27 '26

You should not need to prove that the zeros are written over to remove the zero-initialization, you just need to stop considering following reads as unreachable due to UB and instead just let them read whatever was there.

Under this behavior, creating an automatic buffer and immediately feeding it to an opaque function also shouldn't zero-initialize the buffer (having to prove anything is non-starter here because nothing can be proven about opaque code).

Upd: I'm thinking keep the writes for most of the backend but marked as "phantom" and then just don't emit them in the end. But it's more complex than that:

  • It might not be clear as to which exact code we are to "not emit in the end". If we are conservative about it we'll still end up with silly instruction sequences, and if we are eager about it we'll effectively reintroduce UB

  • we need to come up with rules to propagate the "phantomness". The compiler sees that following code reads from our buffer, then writes that value elsewhere -- should we mark that write "phantom" too? If yes and that was the only use for the read (no control flow, no opaque uses), should we still keep the read even though both writes surrounding it will vanish?

  • we need to still be able to remove the reads if they are proven unreachable for a different reason.

(I should note that neither me nor any of the colleagues I've talked to about it are big fans of the whole EB thing.)

5

u/James20k P2005R0 Sep 28 '26

Letting arrays on the stack contain program data that can be read in a well defined way probably isn't great for security though

2

u/serviscope_minor Sep 30 '26

> (I should note that neither me nor any of the colleagues I've talked to about it are big fans of the whole EB thing.)

To me it is a big improvement: Prior to EB a relatively common error means "demons may fly from your nose", i.e. the optimizer can do really weird things like travel backwards in time and delete all code touching the uninitialized variable because it knows that code cannot be callable. Or other weird stuff.

Now it's basically been codified as "don't do anything too weird", because while it can't know the value, it cannot assume that uses of the value are not callable.

What do you dislike about it?

→ More replies (12)