#Explain the static order initialization fiasco please.

139 messages Β· Page 1 of 1 (latest)

modest swallow
#

Where does it happen? How do I fix it in my code?

compact rampartBOT
#

When your question is answered use !solved to mark the question as resolved.

Remember to ask specific questions, provide necessary details, and reduce your question to its simplest form. For tips on how to ask a good question use !howto ask.

hybrid thistle
#

not sure how helpful that is

gaunt cobalt
#

what does your code look like?

hybrid thistle
#

oh i interpreted this as a theoretical question

cloud niche
#

If you have a specific question after you have read those we can help you

modest swallow
# cloud niche There are good examples and explanations online

So, someone wants to use some variable created in some piece memory in some translation unit, but there hasn't been a constructor yet called to actually initialize anything inside of that piece of memory, so the result is, read from a corrupted piece of memory, kinda like, use after out of scope, or read from an invalid pointer

#

And the fix , supposedly is, reorder the translation units a.k.a. cpp files and the order in which they are created

modest swallow
modest swallow
#

No what

cloud niche
#

That might work in some cases with some compilers, but that's not guaranteed

modest swallow
#

ahaaaaa

#

k I need examples

#

hold on...

cloud niche
modest swallow
#

^ well, I don't really care about probability of occuring, I want to know how to detect and fix such a disaster if I ever see it

#

Last time I saw something vaguely simmilar, it was related to recursive types, and using some correctly written forward declarations to make the linker happy
Where struct A has pointer A and pointer B as fields, and struct B had a pointer B and pointer A as fields

#
// Static initialization order problem
// File1.h
class A {
....
  void doSomething() {
    ...
  } 
}
extern A aObj;

//File1.cpp


static A aObj;

// File2.cpp

class B {
B() {
 aObj.doSomething();// Not okay! aObj may not have been constructed
}
....
}

static B bObj;

So I stole this snippet from FreeCodeCamp

#

let me run asan on it...

#

in theory I know that B::B() can crash, let me try triggering that crash

modest swallow
#

hahahaha these goddamn clowns can't even write an example that compiles

#

🀣 🀣 🀣

#

kay let me try to edit it...

#
$ find ./ -type f -exec echo $'\n'"//"{} ';' -exec cat {} ';' 

//./build.sh
#!/bin/bash

flag="-fsanitize=address"

set -eEuo pipefail

g++ -std=c++11 -c File1.cpp $flag -o File1.o
g++ -std=c++11 -c File2.cpp $flag -o File2.o
g++ -std=c++11 -c main.cpp $flag -o main.o

g++ -std=c++11 File2.o File1.o main.o $flag -o a.out

//./File2.cpp
#include "File2.h"
#include "File1.h"

B::B()
{
    std::cout<<"B()\n";
    ::aObj.doSomething();
}

static B bObj;

//./File1.h
#pragma once
#include <string>
#include <iostream>

class A
{
    std::string s;
    public:

    A() : s("something large enough to avoid small string optimization")
    {
        std::cout<<"A()\n";
    }

    void doSomething()
    {
        std::cout<<s<<"\n";
    }
};

extern A aObj;

//./File1.cpp
#include "File1.h"

A aObj;

//./main.cpp
#include "File1.h"
#include "File2.h"

int main()
{
    B instance;
}

//./File2.h
#pragma once

class B
{
    public:
    B();
};

#

in the build script, at link time, if File2.o is first, we get use-before-construct issue as seen from the corrupted output

#

but, asan doesn't catch this???

cloud niche
modest swallow
#

so, FIle1.o first and File2.o second is the fix?

cloud niche
#

Why would it? πŸ˜›

#

The standard has no notion of such silliness.

modest swallow
#

I don't know

cloud niche
#

The standard has no notion of any β€œsymbol table”

modest swallow
cloud niche
#

I didn’t say anything about ignoring it

#

The standard also doesn’t guarantee anything about the linker πŸ˜‰

modest swallow
#

is that it?

cloud niche
#

That’s how everything works

#

The linker can do whatever, few things are guaranteed

modest swallow
cloud niche
#

It’s a lost cause, just don’t do this lol

#

If you have to, define in the same TU

modest swallow
cloud niche
#

Otherwise, use functions to enforce order

cloud niche
modest swallow
#

so, if we get LUCKY, the linker will use the CORRECT order, but the linker docs do not say anything about initialization of extern globals

#

but, this is just bad

cloud niche
#

Just don’t do stuff like this

modest swallow
modest swallow
modest swallow
#

I gave an example, the globals will cause issues from the 2 threads

cloud niche
modest swallow
cloud niche
#

Yes

#

See how that solves it?

modest swallow
#

why do you think this solves the fiasco

#

the mutex which guards these static locals in C++11 is a separate mutex for each of the variables, it's not some global holy divine mutex or something

#

if 3 threads try to touch the same singleton, okay that's fine

#

but if 2 threads touch 2 separate local static variables a.k.a. singletons, then we get the fiasco

cloud niche
#

How about you write out an example where using static locals has a static init order fiasco

modest swallow
#

should be pretty ez

cloud niche
#

Using static locals instead of globals

modest swallow
#

why do you make me do it?

cloud niche
#

Do it and we’ll look at a concrete example

modest swallow
#

sigh

#

k

cloud niche
#

(And while you’re writing it you may come to understand why it solves it)

cloud niche
# modest swallow sigh

Keep in mind you’re asking us to spend our time helping you learn, you don’t get to sigh at us πŸ˜‰

modest swallow
# cloud niche Keep in mind you’re asking us to spend our time helping you learn, you don’t get...

ok, so, due to the fact that, you aren't touching raw variables, but instead you are accesing lvalue references which function calls like getInstance are returning, you can't get a read-before-construct, BUT you CAN get botomless recursion between A::getInstance() and B::getInstance() leading to a certain stack overflow leading to segfault
and, if we go back to the normal extern globals, if A::A() uses objB and also, B::B() uses objA we get again infinite recursion, but more importantly, no matter how we reorder the .o files this fiasco is impossible to fix.

#

was that the explanation?

#

and the other thing the blogpost suggests is C++20 constinit, which well, doesn't work for complex globals which use pointers, heap and other non-compile-time things

modest swallow
#

maybe for other linkers made by other compiler devs the rules are different, okay that's fine, these are the results for GCC at least

cloud niche
modest swallow
cloud niche
modest swallow
cloud niche
modest swallow
#

__gnu_cxx::recursive_init_error
is this from the standard???

cloud niche
#

It's gcc being nice

cloud niche
#

I linked a specific response

modest swallow
#

oh

modest swallow
cloud niche
#

It's not

modest swallow
#

indeed

modest swallow
# cloud niche As I said here ^

so, if in real life, if some colleague tries to enforce init order with the .o file name ordering, and blindy believes that this fixes anything, and based on luck it seems to work correctly, and acts on the foolish principle "if it works don't touch it", do I need to read the assembly and decrypt what ordering is used in the raw executable file? How do I check the ordering?

Or do I just say, fuck it, and rewrite the code without the globals and thus solve the fiasco easily?

cloud niche
#

Nothing else needed

#

No assembly nonsense, nothing

#

You don't check the order

#

You just don't do this lol

modest swallow
#

(this is a fictional scenario, not real at my job lmao)

cloud niche
#

Then it's a miracle it hasn't broken in those 5 years. Fix it.

modest swallow
#

let me try...

#

still won't find the error

#

damn

#

not even valgrind reports issues

cloud niche
#

you can try ubsan or msan

modest swallow
#

still nothing

#

tried both

#

separately

cloud niche
#

well, then we're back to it being best to just not write code like this in the first place

modest swallow
#

yep πŸ™‚

#

Thank you for explaining soif πŸ‘

#

!solved

compact rampartBOT
#

Thank you and let us know if you have any more questions!

This thread is now set to auto-hide after an hour of inactivity

modest swallow
torpid cypress
#

I had to fix something like this involving static initialization across ~20ishared libs back in the day, where the order that they load (on windows at least) isn't stable - not quite random, but never guaranteed to load in a specific order... and they all depended on one another in some way. the codebase (even just code within each library) was too big/risky to redesign so we built a dependency manager that would lazy load every library's primary resource management class in an explicit depth first order so the lowest level data was loaded first, then it walked up the tree from there until everything was safely initialized. It took a while to work out some of the crazy edge cases we came across, but it eventually stabilized. before implementing this there would be sporadic crashes on startup or teardown of the exe (since the same unstable ordering also applied to unloading the dlls). It's not the same exact issue, but this gave me flashbacks sweating at my desk as a pretty junior dev at the time haha.

@modest swallow - question - why is this design needed? just so 1 translation unit contains the definition? i'd be curious if you just thought about redesigning to avoid this much static initialization (unless it's literally just the two classes in some specific valid use-case)... specifically to the extent where you need to redefine the extern variable as static.

if you're going to stick with this design regardless, you prob want to just avoid the strings (or anything else that needs to be dynamically initialized). really anything that isn't a primitive. constinit might help, but you may still run into deallocation issues even if initialization happens to work out. you can also declare them as pointers then initialize dynamically from a function that returns a reference to a static instance scoped to the function that only gets invoked once at runtime (best approach).

the last part i'll reiterate is that the only way for this not to haunt into the future will probably just be a redesign. i'd be extremely surprised if there isn't a way to do what you want without needing to resort to nonstandard functionality like this.

torpid cypress
#

just came across this blog post that should help break down what's happening and how to fix it (the author recommends a lot of what was also discussed above):
https://pabloariasal.github.io/2020/01/02/static-variable-initialization/#the-red-zone---static-initialization-order-fiasco

modest swallow
#
 isn't stable - not quite random, but never guaranteed to load in a specific order

Could that be some evil hidden environment variable that forces some bad order when starting an .exe file on windows compiled with msvc++ compiler and its linker?

 was too big/risky to redesign

Some shitty dev left globals in their library, and then the dependency hell began growing like a tumor I guess? πŸ”₯ ☠️ πŸ”₯

 so we built a dependency manager that would lazy load every library's primary resource management class in an explicit depth first order so the lowest level data was loaded first,

uhhhhhh, did you split your program into multiple .exe files or something? What exactly is this dependency manager? Or did you instead choose to explicitly open dlls and explicitly load functions from them?

 but this gave me flashbacks sweating at my desk as a pretty junior dev at the time haha.

You solved this disaster on your own as a dev?

constinit might help

it doesn't work for non-constexpr things

you can also declare them as pointers then initialize dynamically from a function that returns a reference to a static instance scoped to the function that only gets invoked once at runtime (best approach). 

You haven't read the chat from above, I already figured that out. Yes, this is nice except when some goof triggers an infinite recursion between A::getInstance() and B::getInstance()

will probably just be a redesign

Yea

to do what you want without needing to resort to nonstandard functionality like this.

There are workarounds for the fiasco, but as you found out, there's no guarantees, the only way is to just avoid the fiasco with normal local variables and pass-by-reference.

torpid cypress
#

Could that be some evil hidden environment variable that forces some bad order when starting an .exe file on windows compiled with msvc++ compiler and its linker?
no, just windows. at least at the time it was. not sure if this behaves any different in modern versions when it comes to this one.

Some shitty dev left globals in their library, and then the dependency hell began growing like a tumor I guess?
no, it's rare that a single dev could do something like this singlehandedly. this was slowly done over like 20 years by dev teams. i don't think the issue appeared until shortly after I started working there, probably as the company grew in size and something was introduced that exposed the crashing on init/teardown or just made it happen often enough where it became a problem. some applications like our mobile apps didn't have to worry about this since they statically linked to all of those libraries instead.

uhhhhhh, did you split your program into multiple .exe files or something? What exactly is this dependency manager? Or did you instead choose to explicitly open dlls and explicitly load functions from them?
no, but it was already split into many different binaries (the libraries), and there were probably ~100ish executables that depended on them at the time (that number is probably double by now). all global statically initialized variables were converted to pointers and just properly dynamically initialized at runtime in the right order. it was just an explicit ordering that was defined so that no higher level library could initialize itself until everything it depended on in other libraries were also initialized already. this had to be handled per library in a way that each was aware of every other library it depended on.

You solved this disaster on your own as a dev?
no it was a pretty massive undertaking to make sure nothing was broken since the codebase is ~5M LOC of just the shared native c++ code. it was a mix of a few senior devs and a few jr devs. the tech lead for my team at the time took notice that i enjoyed the challenge of some tricky bugs so i just got a lot more of them after that haha.

it doesn't work for non-constexpr things
this one is a little tricky to understand since it's not super commonly used. it's not really the same as constexpr other than the compile time initialization part. constinit is just for statically initialized variables, but the variables declared with it don't have to be const and they don't need to support constexpr destructors. I added a link below that'll be worth a read or skim at least... the first thing you'll notice is it starts off by discussing static order init fiasco. the thing that causes it to break down for you is this sentence on cppreference's page for it: "If a variable declared with constinit has dynamic initialization (even if it is performed as static initialization), the program is ill-formed"... which means the std::string members probably break that rule. keep in mind the code you have in place is also ill-formed though.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1143r2.html

#

There are workarounds for the fiasco, but as you found out, there's no guarantees, the only way is to just avoid the fiasco with normal local variables and pass-by-reference.
I guess my biggest question is why these even have to be statically initialized in the first place. describing what you're actually using these objects for might help paint the picture in my mind to get a better idea of what you're trying to do. the more info you can provide, the more any of us can help come up with something that might be a better design solution as a whole.

modest swallow
torpid cypress
#

haha it was really the only option with that codebase

modest swallow
# torpid cypress haha it was really the only option with that codebase

wait, how did you do the globals cleanup? Did you just go:

LET THEM LEAK
🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊 🌊

torpid cypress
#

haha, nah. it wouldn't be a leak in this case since unload order wasn't guaranteed either. if a shared library got unloaded while something that depended on it was still alive it'd crash the application on exit. deinit just happened in reverse order. initialization happened depth first in the dependency tree, and teardown just went in reverse order - starting with the root and working it's way down from there

modest swallow
#

kinda hard in real life tho