miri icon indicating copy to clipboard operation
miri copied to clipboard

Evaluate global constructors (life before main)

Open oli-obk opened this issue 7 years ago • 52 comments

@eddyb is lawful evil https://github.com/neon-bindings/neon/blame/master/src/lib.rs#L49

basically we should look in platform specific link sections for statics and run their value before anything else

#[cfg_attr(target_os = "linux", link_section = ".ctors")]
#[cfg_attr(target_os = "macos", link_section = "__DATA,__mod_init_func")]
#[cfg_attr(target_os = "windows", link_section = ".CRT$XCU")]
pub static __FOOMP: extern "C" fn() = {
    extern "C" fn __foomp() {
        // life before main goes here
    }
    __foomp
}

This would also help support the inventory crate.

oli-obk avatar Sep 05 '18 17:09 oli-obk

🤮 @eddyb!

Ericson2314 avatar Sep 05 '18 17:09 Ericson2314

Are you sure about the "lawful" part? :P

More seriously though -- shouldn't we instead lobby for some way to express this in Rust proper? I already feel bad about that TLS emulation we are doing...

RalfJung avatar Sep 05 '18 17:09 RalfJung

shouldn't we instead lobby for some way to express this in Rust proper?

No, Rust as a language is uninterested in life-before-main. The only reason any of this works is because it's an OS feature we can't really stop you from opting into.

eddyb avatar Sep 06 '18 21:09 eddyb

See https://github.com/rust-lang/rust/issues/66862#issuecomment-559862805 for more information on these sections.

RalfJung avatar Nov 29 '19 19:11 RalfJung

You might find the following C program useful for experimenting with init section order:

#include <stdio.h>

typedef void (*FN)(void);

#define MKINIT(function, section) \
    void function(void) { puts(section); } \
    __attribute__((__section__(section))) FN function##_section[] = { function };

MKINIT(init_array, ".init_array")
MKINIT(init_array_00001, ".init_array.00001")
MKINIT(init_array_00002, ".init_array.00002")
MKINIT(init_array_65533, ".init_array.65533")
MKINIT(init_array_65534, ".init_array.65534")
MKINIT(fini_array, ".fini_array")
MKINIT(fini_array_00001, ".fini_array.00001")
MKINIT(fini_array_00002, ".fini_array.00002")
MKINIT(fini_array_65533, ".fini_array.65533")
MKINIT(fini_array_65534, ".fini_array.65534")
MKINIT(ctors, ".ctors")
MKINIT(ctors_00001, ".ctors.00001")
MKINIT(ctors_00002, ".ctors.00002")
MKINIT(ctors_65533, ".ctors.65533")
MKINIT(ctors_65534, ".ctors.65534")
MKINIT(dtors, ".dtors")
MKINIT(dtors_00001, ".dtors.00001")
MKINIT(dtors_00002, ".dtors.00002")
MKINIT(dtors_65533, ".dtors.65533")
MKINIT(dtors_65534, ".dtors.65534")

int main(void)
{
    puts("main");
    return 0;
}

This prints:

.ctors.65534
.init_array.00001
.ctors.65533
.init_array.00002
.ctors.00002
.init_array.65533
.ctors.00001
.init_array.65534
.init_array
.ctors
main
.dtors
.fini_array
.fini_array.65534
.dtors.00001
.fini_array.65533
.dtors.00002
.fini_array.00002
.dtors.65533
.fini_array.00001
.dtors.65534

Note that weird things happen if you try using 00000 or 65535 or numbers outside that range, or non-numeric suffixes; the entire list gets sorted strangely if you do that. I don't think you need to worry about reproducing that exact behavior, though; I think that's a linker script quirk that doesn't normally come up because GCC never emits sections with such "weird" suffixes. Also note that the aforementioned weirdness may differ between GNU ld and LLVM's lld.

joshtriplett avatar Nov 29 '19 19:11 joshtriplett

Thanks! We'll need to turn this into a Rust program as a test for this feature, but that shouldn't be too hard I guess.

Note that weird things happen if you try using 00000 or 65535 or numbers outside that range, or non-numeric suffixes; the entire list gets sorted strangely if you do that. I don't think you need to worry about reproducing that exact behavior, though; I think that's a linker script quirk that doesn't normally come up because GCC never emits sections with such "weird" suffixes. Also note that the aforementioned weirdness may differ between GNU ld and LLVM's lld.

Sounds like it would be best to just make Miri error in those cases.

RalfJung avatar Nov 29 '19 19:11 RalfJung

We also need to bail if two entries for the same link section exist

oli-obk avatar Nov 30 '19 07:11 oli-obk

@oli-obk No, you can have arbitrarily many entries in a section; it's an array of function pointers. Entries for the same section get concatenated in link order.

joshtriplett avatar Nov 30 '19 08:11 joshtriplett

How does one detect the length of the array? Do link sections have a notion of "length"?

RalfJung avatar Nov 30 '19 09:11 RalfJung

Entries for the same section get concatenated in link order.

I don't think miri can figure out link order. For now we can bail out on multiple entries for the same section, but in the future we can check that they are order independent instead

oli-obk avatar Nov 30 '19 10:11 oli-obk

in the future we can check that they are order independent instead

That sounds like fantasy to me. ;) I don't see a feasible implementation strategy for this.

RalfJung avatar Nov 30 '19 10:11 RalfJung

Do link sections have a notion of "length"?

@RalfJung yes, in some sense. AFAIK, linker scripts can define a symbol before and one after, so that code could iterate between those two symbols.

When you're operating before linking (as miri would need to do), any static in a relevant section is effectively a part of the final array, so it must contain 0, 1 or more fn pointers.

That is, ignoring the Rust type of the static, take the resulting memory allocation and divide it into pointer-sized elements that you then read out as fn pointers.

I don't think link order is a serious concern, and other than the numeric priorities which @joshtriplett explained, you can just pick some visiting order (and maybe lint the fact that a linker might differ?).

eddyb avatar Nov 30 '19 14:11 eddyb

@oli-obk @bjorn3 do you know a good way to get "all statics that are in a particular linker section"?

I suspect this issue may be responsible for https://github.com/rust-lang/rust/issues/123583. Maybe it's time to stop using this ad-hoc hack for our Windows thread-local destructors.

RalfJung avatar Apr 07 '24 10:04 RalfJung

I implemented this in https://github.com/rust-lang/rust/commit/850e1e34149a0dfa7513ae2e597434ab4b937263, but it requires a hack in the exported_symbols query override to also preserve the Instances of all #[used] symbols.

bjorn3 avatar Apr 07 '24 15:04 bjorn3

Oh, neat!

it requires a hack in the exported_symbols query override to also preserve the Instances of all #[used] symbols.

Does the regular query do that and Miri just lost it? Or do we somehow have to do something the regular query does not do? I assume regular rustc also has to preserve these statics for codegen as well...

RalfJung avatar Apr 08 '24 10:04 RalfJung

Does the regular query do that and Miri just lost it?

The regular query doesn't do that.

I assume regular rustc also has to preserve these statics for codegen as well...

Statics are codegened in the crate that defines them and as such don't need to end up in exported_symbols if they aren't exported from the current crate (and not used by an exported inlinable function). For calling global constructors however miri would need all statics even if they aren't exported for as long as they are marked as #[used].

bjorn3 avatar Apr 08 '24 11:04 bjorn3

I think it'd be better for the compiler to have some basic support for platform specific linker magic. The library code is already a bit too hacky for my liking so it'd be an improvement there too.

ChrisDenton avatar Apr 08 '24 12:04 ChrisDenton

Yeah it would be better but also a lot more work to implement. I won't stop anyone who tries to do that but I don't have the time to do it.

Statics are codegened in the crate that defines them and as such don't need to end up in exported_symbols if they aren't exported from the current crate (and not used by an exported inlinable function). For calling global constructors however miri would need all statics even if they aren't exported for as long as they are marked as #[used].

The static in question is not marked used

#[link_section = ".CRT$XLB"]
pub static p_thread_callback: unsafe extern "system" fn(c::LPVOID, c::DWORD, c::LPVOID) =
    on_tls_callback;

So there's something here I don't understand.

Statics are codegened in the crate that defines them

The query we override runs in that crate, so why is that relevant?

For finding extern functions with a link_name we also just do exactly what codegen does, I think. So it seems strange to me that here we'd have to do more than what codegen does.

RalfJung avatar Apr 09 '24 07:04 RalfJung

The static in question is not marked used

See rust-lang/rust#121596 for why the #[used] was removed.

joboet avatar Apr 09 '24 09:04 joboet

Statics are codegened in the crate that defines them

The query we override runs in that crate, so why is that relevant?

We need to override the query to include #[used] items because otherwise they wouldn't be encoded in the crate metadata as exported symbols and thus have no way to find all used statics for dependencies.

For finding extern functions with a link_name we also just do exactly what codegen does, I think. So it seems strange to me that here we'd have to do more than what codegen does.

When doing codegen the static initializer ends up an object file and codegen doesn't have to care about it anymore. Only the linker needs to find it. Miri however will need to know all static initializers of all dependencies without inspecting any (non existent) object files. As such the list of static initializers needs to be encoded in the crate metadata, which I did by ensuring they end up in the exported_symbols query.

bjorn3 avatar Apr 09 '24 10:04 bjorn3

When doing codegen the static initializer ends up an object file and codegen doesn't have to care about it anymore. Only the linker needs to find it. Miri however will need to know all static initializers of all dependencies without inspecting any (non existent) object files. As such the list of static initializers needs to be encoded in the crate metadata, which I did by ensuring they end up in the exported_symbols query.

I see... well I think I see. ;)

But it seems to me Miri should preserve all statics with a link_name, not all statics with #[used]. That is what we do for functions as well, isn't it? (I don't see a check for link_name in our exported_symbols query, but we only really care about the ones with a link_name.)

RalfJung avatar Apr 09 '24 10:04 RalfJung

We do preserve those with link_name already, but not those with link_section.

I don't see a check for link_name in our exported_symbols query, but we only really care about the ones with a link_name.

I believe that is covered by the contains_extern_indicator check.

bjorn3 avatar Apr 09 '24 11:04 bjorn3

Ah right I mixed up link_name and link_section.

Sounds like a non-used static with a magic link_section has a really strange semantics - the function may or may not be called based on optimizations. That's very fragile and tricky to emulate in Miri. I would prefer if we didn't do hacks like that...

RalfJung avatar Apr 09 '24 12:04 RalfJung

Which is one reason why I'd prefer proper compiler support for TLS destructors 😉

But it is just an optimization. Considering it to be always called is fine.

ChrisDenton avatar Apr 09 '24 14:04 ChrisDenton

But it is just an optimization. Considering it to be always called is fine.

What Miri should be doing is randomly sometimes call it and sometimes not. That reflects the fact that it is unspecified whether the static ends up in the final binary or not.

Which is one reason why I'd prefer proper compiler support for TLS destructors 😉

I don't disagree with that. But I strongly disagree with using hacks relying on unspecified behavior to work around language limitations. The fix for a missing language feature is to add the feature. If something can't be done properly then doing it improperly is not a viable alternative IMO. In this case, using #[used] is a perfectly viable option IMO; the further optimization is not worth the significant headache it causes.

RalfJung avatar Apr 10 '24 05:04 RalfJung

In this case I think the worst thing that can happen is memory leaks. Or can unsafe code somehow rely on TLS destructors being called? If yes then I would say https://github.com/rust-lang/rust/pull/121596 introduced a soundness issue, as we no longer can guarantee that TLS destructors are called.

RalfJung avatar Apr 10 '24 05:04 RalfJung

I'm not sure I understand your point. If there is ever a TLS dtor then it will be called. Always.

If there is not then it's a no-op in any case so is eligible for removal.

ChrisDenton avatar Apr 10 '24 11:04 ChrisDenton

I'm not sure I understand your point. If there is ever a TLS dtor then it will be called. Always.

You are relying on the completely unspecified assumption that these volatile reads "pull in" the static.

RalfJung avatar Apr 10 '24 12:04 RalfJung

Sure but it's not the only place in the standard library that does that. In fact within that very function there's a volatile read being used to pull in another symbol _tls_used).

ChrisDenton avatar Apr 10 '24 12:04 ChrisDenton

Relying on unspecified behavior more than once doesn't make it any or correct to do so.

RalfJung avatar Apr 10 '24 12:04 RalfJung