Repository navigation
Description
Rust does not provide facilities for doing work before fn main(), but we can achieve it anyhow on a lot of platforms using linker tricks and global constructors, as done in ctor, inventory and the standard library itself.
Is this safe to do, or should it require unsafe? If so, what are the safety requirements that one needs to uphold inside a global constructor?
In isolation, this feature seems to be safe (?), but one reason to define it as unsafe could be to allow libraries to rely on never being used in a constructor themselves? Consider the following library:
static mut FOO: i32 = 0;
/// Do some "heavy" work to initialize `FOO`.
#[ctor]
fn setup_foo() {
// SAFETY: `get_foo` is not running at the same time as this constructor, so an unsynchronized write is fine.
unsafe { FOO = 42 };
}
/// Get the value from `FOO`.
pub fn get_foo() -> NonZero<i32> {
// SAFETY: The constructor has run before `get_foo`
let foo = unsafe { FOO };
// SAFETY: The variable was initialized to a non-zero value.
unsafe { NonZero::new_unchecked(foo) }
}The safety comments here rely on:
- The constructor not executing concurrently with anything else.
- The constructor executing before
get_foo.
But those assumptions are actually false, since the user may write:
#[ctor]
fn user_constructor() {
std::thread::spawn(|| {
get_foo();
});
}And, depending on linker ordering, user_constructor may be run first (unless we can somehow guarantee that setup_foo runs before user_constructor?), in which case the unsynchronized read+write is not actually okay, and the variable may not actually be initialized to a non-zero value.
The correct way to write it then would be:
static FOO: AtomicI32 = AtomicI32::new(0);
#[ctor]
fn setup_foo() {
// Must use atomics.
FOO.store(42, Ordering::Relaxed);
}
pub fn get_foo() -> NonZero<i32> {
let foo = FOO.load(Ordering::Relaxed);
// Must check that the variable is actually initialized.
NonZero::new(foo).unwrap()
}Though this does decrease the utility of #[ctor] by a fair margin as the performance improvements of using a global constructor decreases, and you'd probably just want to use OnceLock now instead.
One noteworthy tidbit is that when using dynamic libraries (e.g. linking libc or libX11), this is a non-issue, as constructors are executed in libraries first, so any global constructors that a dynamic library has are guaranteed to be executed before any code that depends on that dynamic library.
For context, I'm opening this because the ctor crate has made the #[ctor] attribute require unsafe, see mmastrac/linktime#159, but deciding "running code before main is unsafe for xyz reason" is a decision that affects more than just ctor (e.g. it affects inventory as well), so I thought it might be something that t-opsem / the UCG has opinions on?
See also #397.
Activity
RalfJung commentedon Jan 13, 2026
madsmtm commentedon Jan 13, 2026
I guess part of my question here is "are global constructors unsound"? Stated differently, is there some interaction with the Rust AM or the standard library or similar where global constructors would be a problem? If I were to write an equivalent C function with
__attribute__((constructor)), and then make that call a Rust method over FFI, would that be unsound?Yup,
#[unsafe(link_section = "...")](which your RFC made happen ofc ;) ).CAD97 commentedon Jan 14, 2026
Theoretically, the system calling
__attribute__((constructor))functions would be no different than calling from FFI main. The actual concrete difference, imo, is to ask whether calling the lang start item after making other calls into AM operations is allowed.My initial instinct would be to say that at the AM level, lang start is just a normal function that gets called by system convention, but that std is allowed to make the assumption that no Rust code is called before that, if it wants to (or IOW that it controls any before main code if it controls main).
madsmtm commentedon Jan 14, 2026
That's my instinct as well.
Well, I think that the presence of the
stddocs on life-before-main in itself is a fairly strong hint that such usage isn't unsound, just likely to break in other ways. But definitely more of at-libsquestion, and I should probably open a PR (after we figure out this issue) to document thatstddoesn't rely on no-code-before-main for soundness.mmastrac commentedon Jan 14, 2026
As the author of the
ctorcrate, I think the answer is very much "it depends". Basic, non-allocating low-level primitives like atomics? Sure.no_std, non-alloc code? Maybe. Can you prove that a given#[ctor]is safe? No — not without some sort of stdlib-level annotation or guarantee.I do think there’s room for a more "official" feature with explicit stdlib cooperation. Even something as simple as a documented guarantee about which CRT initializer slots the runtime will use would be a meaningful improvement.
I’d genuinely love for this crate to die. Unfortunately, the initialization-ordering problem is extremely hairy, and I don’t see a way to guarantee that any particular use of
#[ctor]or any similar API is safe in the general case. Even if I tried to allow-list a handful of APIs, I’d effectively be making policy decisions on behalf of the entire Rust ecosystem, and that’s not a role I want to take on.What does seem reasonable is for downstream crates to wrap
#[ctor]in narrowly-scoped ways that are provably safe for their specific use cases. That, in my view, is the best path toward pushing unsafety out ofctoritself.ChrisDenton commentedon Jan 14, 2026
From the libs perspective, the current docs are the result of the last time this was discussed. In short it's supported on a "best effort basis". That does at least imply life before main can be used soundly.
VorpalBlade commentedon Jan 14, 2026
@mmastrac This seems to be a Windows specific concept (a platform I know almost nothing about)? How would this work on / translate to ELF based platforms, or on Mac?
Does it make sense to make guarantees that aren't useful in a portable way? Wouldn't it just means those guarantees aren't very useful in practice or actively hinder the portability of crates relying on them.
mmastrac commentedon Jan 14, 2026
Yeah, I used a Windows-ism there as shorthand - sorry. To be clearer, I mean guarantees about ordering in general, using platform-appropriate mechanisms.
For example, on ELF platforms this could be expressed via constructor priorities (e.g.
__attribute__((constructor(N)))->.init_array.N). On macOS the mechanism is different (e.g.__mod_init_func), and it’s admittedly less clear how strong or portable priority guarantees actually are there. †Even more speculatively, this could take the form of a custom
.rust.init.array(or similar) section that the runtime guarantees to execute after stdlib setup but before main, or other linker-script–level schenanigans that provide comparable guarantees (hand-wave gesture).Some guarantees on some platforms is certainly more valuable than no guarantees at all.
† EDIT: confirmed, dyld doesn't support it (llvm/llvm-project#15363), but custom sections could be an option there.
ChrisDenton commentedon Jan 14, 2026
I think this discussion is moving into RFC territory. I.e. adding actual support for running code before main and not just allowing third parties to do so.
madsmtm commentedon Jan 14, 2026
Regardless, even if it was possible to consistently order
#[ctor]s, that still wouldn't stop a user from just choosing a higher priority than the library they're using (unless you somehow defined that "priority > 100 requiresunsafe" or smth).Hmm, but do you have examples of stuff that cannot be used safely in
#[ctor]s? I'm not aware of any myself, and I struggle to imagine cases where soundness issue would occur (correctness issues and confusingness are another problem entirely).Personally, I'm still not convinced that
#[ctor]should beunsafe(or at least not the simpler version that just applies tofns). Theuser_constructorand theget_foousing atomics I showed above both seem like they're sound (assuming the Rust AM is fine with doing stuff beforelang_start).ChrisDenton commentedon Jan 14, 2026
This is platform specific behaviour. At the very least it would need to only be provided on systems that are confirmed to be safe. But even then people make safety assumptions about
mainthat#[ctor]can break. For example, that there are no other threads running.madsmtm commentedon Jan 14, 2026
Right,
std::env::set_varis supposed to be safe to use at the top ofmain.bjorn3 commentedon Jan 14, 2026
That is going to be tricky to implement for dylibs.
mmastrac commentedon Jan 14, 2026
I haven't checked if this is the case for ages, but
println!was unsound for quite some time. This may have been fixed, but it was the original inspiration for thecratemany, many years ago. 😄madsmtm commentedon Jan 16, 2026
CC @notgull who maintains
ctor-lite, and wrote the following in its safety section:6 remaining items
VorpalBlade commentedon Jan 18, 2026
Shouldn't it be "any normal code from the same shared object/dylib/dll/exe/... or dependencies thereof"? There can be multiple crates in a shared object after all.
That is a good point, but not what I was arguing about.
But I think all this confusion points to the question of this issue is then underspecified, since it wasn't clear to me what you considered life before main when there is no main, or main runs before your code is even loaded. When discussing semantics like this it is important to precisely define all terms and not use undefined shorthand and hope everyone has the same mental picture.
simonask commentedon Jan 18, 2026
@VorpalBlade Very good point about all of this being underspecified, and thank you for pointing out the additional complexities that arise from dynamic linking.
Instead of "before
main()", we can say "load-time" or "initialization-time", because this is the behavior ofdlopen()/LoadLibraryEx(): the list of static constructors is guaranteed to have been called before the loading function returns.My point is made within that context: When any crate type contains static constructors (static library, dynamic library, executable), and the target platform supports it, it is useful to say that they should be executed before any "normal" entry points in that crate are available, meaning that arbitrary code is fine as long as it cannot run any code from the library without first ensuring that it is fully loaded and initialized.
VorpalBlade commentedon Jan 18, 2026
Related but bit of a side question from this issue: is that considered a safety contract of a correctly implemented dynamic loader? Should we specify that contract as well? I'm thinking for use cases where you are writing your own dynamic loader for your own kernel / OS / libc: what guarantees does rust expect of the dynamic loader that the platform / kernel / etc provides (especially if you aren't implementing a POSIX / ELF like system)?
Is it worth opening a new issue about that? It seems like a potentially very deep rabbit hole...
RalfJung commentedon Jan 18, 2026
I assume you mean "are safe global constructors unsound".
As I tried to insinuate in my first post here, the answer is "it depends". It depends on the soundness contract that holds the ecosystem together. Because life-before-main is not an officially supported feature, there is no obvious central entity that would decide what that contract should be.
Given that I would advice to treat it as an unsafe operation, to be compatible with either choice.
It is a strong sign that such usage isn't unsound for the standard library. It's also a strong sign that such usage is considered special and hence may well be unsound for other libraries.
I think that's not quite the same since the code outside the dylib cannot invoke the code inside the dylib before it is loaded either. The only thing the library can actually tell is whether its constructors are finished running before any API interactions are done by outside parties. (Right? People mentioned threads above, but can't I spawn a thread pre-main?)
ChrisDenton commentedon Jan 18, 2026
Dynamic libraries are indeed different in that, as far as rust is concerned, there is no "main". There may possibly be code that std expects to be run before any rust code is run. Or not.
Platforms may have mechanisms for running code immediately after the library is loaded (without being explicitly called) but that's very much platform defined behaviour. E.g. on Windows doing it wrong may lead to deadlocks or crashes https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-best-practices
VorpalBlade commentedon Jan 18, 2026
Unfortunately you can (at least on Linux), though I would argue that it is discouraged. For example, a C++ dylib that you link (not dlopen1) could have spawned threads before the Rust main ever gets to run. Rust could could do the same, but people are probably more aware of the problems with doing so. That said, I don't know if this is actually done "in the wild".
An effect of this would be that
// SAFETY: We are at the top of main and haven't started threads yet, setting environment/forking is finemight not be true. I think this is another strong argument for general ctor to be unsafe in Rust. (Possibly libraries that expose more limited "register things statically" might be possible to make safe still.)Footnotes
Unless you dlopen before main, please just stop. ↩
notgull commentedon Jan 19, 2026
I stand by this, except maybe for alloc.
madsmtm commentedon Jan 30, 2026
I've become less convinced that the "
set_varinmain" thing is actually a problem?Consider a crate
access_env_in_ctor_thread:If we just follow the definition of the word (which is admittedly simplified), this library is sound, in that it is impossible for safe code to cause Undefined Behavior using it.
If the user then decides to use it like this:
I'd argue that the error is in the
SAFETYcomment infn main, because that assumption just isn't true if linkingaccess_env_in_ctor_thread.More concretely, I'd argue that if you want to use
std::env::set_varinfn main, you have to ensure that there are no constructors that spawn detached threads. This is often pretty easy, because no sane code does this, but the onus is on you. If you decide to linkaccess_env_in_ctor_thread(either directly or transitively), you haven't upheld that guarantee.RalfJung commentedon Jan 30, 2026
That's one option to resolve this, but it's also entirely reasonable to say that the safety comment is correct -- main may rely on there not being other threads, and any code that observable breaks that is unsound. To repeat what I said above: because life-before-main is not an officially supported feature, there is no obvious central entity that would decide what that contract should be.
ia0 commentedon Jan 30, 2026
Should life-before-main be added to #379?
It seems to me that
access_env_in_ctor_threadis an example of when the simplified definition of soundness from the glossary falls short of what it should be, namely that the thing under consideration (function, module, crate) is semantically well-typed. In this particular example it would manifest as the ctor being either:maingets an exclusive/full resource), and if they need such resource, they need to be made unsafe to link somehowenv::set_var) to make sure all concurrent threads are robust and don't hold any shared resource for the environment, including threads spawned from ctor functionsRalfJung commentedon Jan 31, 2026
Saying "it must be semantically well-typed" answers nothing. You'd have to first define what you mean by that, and in making that definition you have to make exactly the choice we are talking about here: either life-before-main is safe, or libraries can assume their own life-before-main runs before their public API is used (and maybe main can assume there are no other threads yet). Both of these choices lead to entirely sensible notions of "semantically well-typed".
That's why these discussions are tricky: to get an official answer to all of them, we need to fully nail down the official notion of semantic typing. The problem with that is that (a) precisely talking about semantic typing seems to require very heavy mathematical hammers, so it's hard to have this discussion while including the wider community, and (b) in nailing down that definition, we would also give definite answers to all the soundness conflicts we didn't even know existed, i.e. we'd be making choices we're not even aware of, which seems suboptimal.
That's why we currently do this on a case-by-case basis and the typical way we nail down semantic typing is indirectly, by blessing certain APIs as sound.
ia0 commentedon Jan 31, 2026
Yes exactly, it depends on type (and second class constructs like modules and crates that would have a type if they were first class) interpretation, which is why I gave at least 2 options for the type interpretation of ctor functions. The goal was not to answer anything, it was to point out that the glossary definition of soundness is not good enough for load-bearing discussions like this one.
I might be too optimistic, but I believe it's possible to some extent. The community already understands (to some extent) the concept of resources because of how pervasive borrowing is. And they also understand (to some extent) the notion of resource separation because of aliasing XOR mutability. Life-before-main can be explained with those concepts without going in the details of how to properly define resources (like Iris does). It will be informal, but still more useful than "it must not have UB in any safe context", because that's a definition of soundness that composes (essentially solving the underlying problem of #379).
I don't think it's incompatible. This can still be done in a case-by-case basis and by blessing certain APIs as sound. The difference is that when doing so, some form of informal argument regarding resources needs to be provided. In the case of life-before-main it would look like one of:
main, ctor, or regular) gets any exclusive resource (so when you writeenv::set_var()you have to argue why you own an exclusive resource to the environment, and saying you're inmainis not enough)maingets exclusive resource to all things extern to the program (like the environment but unlikestatic mutwhich belongs to the crate defining it) so life-before-main needs to make sure it uses resources appropriately in all the programs it gets linked to (essentially making such library unsafe which is not a concept we have in syntax yet, like requiringextern unsafe crate foo;to link against)mainand ctor functions get exclusive resource to all things extern to the program, but ctor functions get a version that cannot be sent to spawned threadsVorpalBlade commentedon Jan 31, 2026
I don't think this one is reasonable. That would mean you would have to audit all transitive dependencies for usage of life before main, and
cargo installwithout--lockedwould be unsound. You could essentially never use set_env at all for any non-trivial code base.ia0 commentedon Jan 31, 2026
Yes, I agree. It's definitely unrealistic in current Rust (even for safety-critical systems where they own everything top to bottom). And even in the hypothetical world where Rust has a notion of "
robustness" (as defined in the unsafe mental model), which is the dual of "unsafeness", I don't think libraries would declare themselves asrobustregarding them not claiming extern-to-program resources in ctor functions, unless there's some automated tooling (say incargo) to help doing so. I mostly provided those options as an incomplete illustration of the spectrum.Kixunil commentedon Apr 7, 2026
FTR, there is a way to make usage of
set_envsound even ifctoris safe: check the number of threads using the OS API - if there's exactly one then you are guaranteed the number won't increase, so you can call it. I did this in my library where I simply fail initialization if there are other threads running. (The library has to be initialized and it pretty much has to involve runningset_env, unfortunately.)Over time, I think these things really need an effect system. I know that effect systems have their problems but it seems that we already have the same problems, we just don't have them formalized.