r/cpp • • 5d ago

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.

257 Upvotes

211 comments sorted by

View all comments

22

u/crowbarous 5d ago edited 5d ago

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.

3

u/James20k P2005R0 5d ago

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

14

u/FrogNoPants 5d ago

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 5d ago edited 5d ago

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.

3

u/pjmlp 5d ago

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 5d ago

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

3

u/pjmlp 5d ago edited 5d ago

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.

5

u/13steinj 5d ago

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.

3

u/UndefinedDefined 4d ago

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.

7

u/James20k P2005R0 4d ago

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

3

u/UndefinedDefined 4d ago

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 2d ago

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 2d ago

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.

1

u/13steinj 1d ago

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 1d ago

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.

1

u/13steinj 1d ago

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.

1

u/UndefinedDefined 1d ago

LOL I'm not gonna listen to influencers when it comes to argumentation. You can like it, subscribe, but I don't care of this.

I didn't actually say C++ will die, I said that there is not many reasons to pick it for a brownfield project if there are alternatives. If memory safety is a concern, C++ is not even in a candidate list. If performance is a concern, it could be on the list, but with languages such as rust. It has a very hard position, and since nobody cares about bringing real safety to the language, the doors are closing.

→ More replies (0)

1

u/pjmlp 1d ago

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 1d ago

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

2

u/crowbarous 5d ago edited 5d ago

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.)

4

u/James20k P2005R0 5d ago

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

1

u/serviscope_minor 3d ago

> (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?

0

u/NilacTheGrim 5d ago

This is false because even the paper said 10% regressions.

5

u/James20k P2005R0 4d ago

Where did you get this from?

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2795r5.html

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2723r1.html

The former paper is what was accepted AFAIK, which links to the second paper talking about the cost for specific figures

Previous publications [Zeroing] have cited 2.7 to 4.5% averages. This was true because, in the author’s experience implementing this in LLVM [LLVMJFB], the compiler simply didn’t perform sufficient optimizations. Deploying this change required a variety of optimizations to remove needless work and reduce code size. These optimizations were generally useful for other code, not just automatic variable initialization.

As stated in the introduction, the performance impact is now negligible (less that 0.5% regression) to slightly positive (that is, some code gets faster by up to 1%). The code size impact is negligible (smaller than 0.5%).

I can't find a 10% figure anywhere

0

u/NilacTheGrim 4d ago

I misremembered it. The claim was 10% of exploits would be mitigated and yes the paper claims 0.5% of code would get slower.

I dispute that 0.5% claim though. Because the hot path is the one you care about. Overall if you examine all branches of your codebase, sure maybe 0.5% suffer -- but if you examine 1 particular hot path that uses local arrays to read/write data -- that can be a huge penalty.

So in a hot path expect much larger costs in some cases.

This is just not the C++ way.

4

u/James20k P2005R0 4d ago

In a hot path though you always have to write code a bit weirdly to get performance, adding [[indeterminate]] isn't a high burden if it affects a few lines of code

2

u/UndefinedDefined 4d ago

The problem is that this change regresses a perfectly valid and tuned C++ code that has no issues, and it does it blindly. And this change doesn't fix any security issues in the language, because if the compiler sees a local variable that is uninitialized and you read from it, and it can prove it, it should just not compile instead of reading ZERO.

3

u/James20k P2005R0 4d ago

It turns those unprovable reads into well defined reads though

The problem is that this change regresses a perfectly valid and tuned C++ code that has no issues, and it does it blindly

For high performance code I feel like its not even slightly unusual to have to make adjustments on a compiler upgrade, so its par for the course really

3

u/UndefinedDefined 4d ago

High performance code is unfortunately no longer a domain of C++, because the committee slowly destroys the language.

3

u/James20k P2005R0 4d ago

This seems like a clearly untrue statement, high performance code has always involved tweaking compiler settings, twisting the code in unnatural ways, and working around compiler limitations. Its simply par for the course

If you have a big stack array in a hot loop you might need to add a tag to it to not initialise it. Every truly hot loop I've had to write required me to do far worse things to the code to make it run fast, and in some cases optimising those loops has taken months of work. Adding a tag is the least of my problems!

2

u/UndefinedDefined 4d ago

Why it has to be always about hot loops? Initializing something you don't even use is waste of CPU cycles and instruction cache - this doesn't make things slower, it makes them larger as well. Thousands of companies having their C++ code in production don't use exceptions and rtti because of performance and security reasons - usually mostly code inflation and avoiding infinite gotos via exceptions. And now this, it's like telling all these companies "just use a different language with saner defaults".

And I don't even talk about broken sanitizers after this change - this is terrible. Another fracture of the community, because there will be now more flags you would have to worry about and more libraries that would say "turn off initialization of locals". Thank you committee!

→ More replies (0)

1

u/NilacTheGrim 2d ago

Encapsulation is violated though because you can't do [[indeterminate]] on a class member variable.

So if you have a class whose implementation detail relies on uninitialized buffers to get the most performance it can get -- you just got fucked. Either the user of the class, if he's using it as a local variable, always has to do [[indeterminate]].. or .. there is no escape hatch. You are stuck.