-
Notifications
You must be signed in to change notification settings - Fork 59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Must static DATA: UnsafeCell = expr;
still == expr
by main()
?
#397
Comments
I think that mutable statics still need to have the values they were promised to have in the initializer unless an AM-visible write occurred to them. That write might have been caused by FFI or asm!, but if the value isn't there at lang_start then it's not clear where the value goes at all, in which case a compiler which just ignores all static mut initializers would be conforming and that's not ok. Regarding the linked thread, I think the appropriate model is to use an extern static and allocate/uninitialize it using a linker script, and then use as the AM invariant that when main() is called that static is initialized with a random bit pattern (not I don't see a way to make this work with a rust-allocated static without lying to the compiler to some extent. For example, we could use a mutable static (initialized to 0, say) with a |
What if you did this: use std::arch::asm;
static mut FOO: i32 = 0;
pub fn main() {
unsafe {
asm!("/* {} */", in(reg) &mut FOO);
}
// Rest of program...
} The ASM block does nothing but the compiler must behave as though it may have written a value into the static. I guess this is what you were suggesting @digama0 ? |
Yes, that is what I meant. The drawback is that the compiler is technically allowed to read (Also, we should replace |
It's not completely unreasonable to have a step between codegen where the initial starting parameters of the AM are serialized and the Notably, the host can expose some known memory locations' provenance, such that But I still agree that the proper way to model the original use case here is almost certainly an extern static allocated by the host linker shenanigans rather than managed within the AM. |
I think for as long as |
I don't think this is about special casing, so much as the issue that there is no "life before main", even in the AM. As far as I am aware the AM semantics start at As I mentioned before, the issue with saying that the initialization value of a static doesn't need to be in that static when the AM takes its first step (wherever we decide to put that) is that it becomes impossible for the AM to distinguish between The way the analogy breaks down with cdylibs is that a call to a rust function from FFI is not the AM's first step. There is still some subtlety to define what exactly was happening in the AM before the first line of rust code, but I imagine that it would be something like "arbitrary RAM-legal operations, such that the state at the call satisfies the appropriate calling convention". Since it's an unsafe interface we can freely add additional safety constraints like "and |
No that's not correct. Just because the user is allowed to change the value via arbitrary means before lang_start, doesn't mean the compiler is allowed to do that. For the same reason your "observably equivalent" argument doesn't hold up. This issue is not about giving the compiler more choice, it is purely about giving the user more choice. @CAD97 expressed it pretty well I think, it's basically a user-decided machine step that happens before the lang_start stack frame is pushed. |
I mean, I'm fine with building the equivalent of an empty A completely user-decided machine step at program start is also a really heavy hammer. If you aren't careful, you might make it impossible for the compiler to produce a working program under those conditions since the compiler doesn't know what the user intends there. |
I'm confused about why you are bringing up this question since it doesn't change with this issue at all? In the initial machine state (before that new user-defined step we are discussing) the static mut has the value given by the initializer.
That seems to be hard to do without a C toolchain, or something? I don't claim to understand the usecase that triggered the discussion here.^^ |
This user-defined step probably needs quite some work to unpack, since it is basically what we have been calling "linker shenanigans". Somehow the spec has to interact with linker scripts, and I guess it is this code that would actually see the values of static initializers and have an opportunity to change them. As it relates to the title question, I think the answer should still be "yes" provided there are no linker scripts (or only the default one). A program like: static mut FOO: u8 = 0;
fn main() {
println!("{}", unsafe { FOO });
} with no special linking stuff should be required to print 0 (and the user should not be able to get this to print 1 by wishing on a star). Anything which changes the output of this program needs to involve changing an actual input to |
If true, this seems like a language issue. It should be possible to declare "extern statics" with link_section using only rust and a linker script (not that I know much about the latter). I think the OP was talking about using C to allocate the static itself, which would be just as incorrect as it is in rust. |
That is satisfied by my suggestion in #397 (comment) to require that only exported statics can be mutated by external processes. If you were to add |
Ah, sorry I should have made it |
In case of |
I agree, but that's more of a compiler optimization perspective than what the AM actually does. The compiler doesn't know what the dynamic linker is doing either, so it can only do e.g. constant folding if it knows or has an agreement with all such code that the relevant statics will not be touched, but the AM can say more precisely "The program prints 0 as long as libc et al don't mutate FOO". |
Sure, but in the same way we can also say a static is equal to the value in the initializer at the start of the program so long as no external process mutated it rather than a blanket ban on all mutation from external processes. |
One thing about external mutations of a static mut is that it would be a data race under most circumstances, unless the program start is considered as synchronizing with said process (probably true for linkers and false for actually concurrent mutation). So depending on how the static is accessed in rust you might still be able to say some things about what can happen to the value without UB. |
In the specific use case that started this discussion the "mutation" synchronized with the program start. But I agree that it can be a data race in the general case. |
Yes of course? I's a user-defined step that is added. It models the user doing linker shenanigans. If the user does nothing funny then it implicitly picks the identity step and then of course this prints 0. I feel I must be missing something since this all seems obvious and it's the easy case. The case I was hoping to discuss here is the one where the step actually does something. The question is whether the compiler gets to assume that the initial state of the AM looks a certain way or not. The more control we want to give the user over this initial state, the less the compiler can assume. If we say nothing, then the initial state is fixed by the Rust program written (e.g. in MiniRust, this happens here), which gives the compiler the license to make assumptions about it; this discussion is about whether we are fine with that or whetver we want to add the infrastructure that is required to not have a single fixed initial state. |
I'm saying the user should not be able to perform such a step without actually doing something that has an impact on the literal bytes going in to the compiler. That is, it can't be something that we just say occurs as a result of (the equivalent of) an empty asm block. When the user actually does linker shenanigans there will be a change to a linker script and this script is an input to the compiler (construed broadly).
The part that makes it not easy is that if we say that the user can do linker shenanigans with their mind then the compiler can't link programs correctly that have no indication that linker shenanigans are happening. That is, the situation I am talking about is one where the user says that linker shenanigans are happening but the compiler is not made aware of this in any way and hence fails to account for it.
Put another way, I would like us to define things such that as long as from the compiler's perspective it is a regular program, in the sense that all the inputs indicate that no funny business is happening, then the compiler should also be licensed to assume that no magic user-defined step breaks this, and optimize accordingly. This is a nontrivial constraint - it means that if the user wants to do linker shenanigans they need to use some kind of attribute or asm block or something. These kind of user-defined semantics need to be opt-in. |
I think it's essentially a requirement that we permit some amount of extra AM state to be provided by the host. @digama0: what benefit do you actually see requiring code to say "some arbitrary shenanigans happen before main" having? Note that std's initialization in (With a disclaimer that I'm not doing and know very little about embedded or any other donations that require AM-opaque behavior, and that the footnotes are painfully rambling about this field I know little about (so probably ignore them),) While we could say that any host-provided resource needs to be declared to the Rust AM via some However, if we require the AM to enter Starting with some addresses exposed is, to be fair, nearly a trivial capability7, since it's effectively purely additive (and also only impacts code already reliant on angelic nondeterminism). But still, if we take that capability as a given, then I (as stated before) see no reason that it should be allowed, but modifying the initial state of externally linked Because Rust is a "low level" language without a runtime9, the definition can get away with being that simple. The output of compilation can be as vague as "something which can be linked," and the linker is then in charge of (doing any linker shenanigans and then) turning that into a format which the target is capable of executing. The difference between a This doesn't cover the further linker shenanigans that people would like to be able to do12, but those are irrelevant to the topic at hand (initial AM state) and significantly more involved. Footnotes
|
There is a lot to respond to there, but I guess a lot of it would take us off topic so I will restrict attention to
I agree that in the majority of cases the compiler won't be able to propagate the initial value of a static. That is a natural consequence of there being so many potential middle men before getting to the Another example was brought up by @bjorn3 : propagating the value of a private static. I think there is still some work to be done to define the aliasing model to allow language-private statics to actually be UB to access without permission so that this is feasible, but I think we will want something like that anyway to enable moving
This brings me to "Rust allocations". The basic premise is that some memory is AM-managed and some is provided externally. Heap and stack memory are rust allocations, An external allocation is memory which is made available to the AM without there having been an actual allocation event. They may be available at the start of execution, or they may be made available during the course of execution as a result of an OS call. These are usually (always?) exposed addresses, although it includes things like the top of the stack (argv, envp) which are regular memory which is "just there" at startup. We usually cannot make any guarantees about what external allocations exist, this is a function of the host parameters. (Although one would hope that the allocator at least knows where they are and how to avoid them!) They don't need to be declared in advance, and this is how I would justify
I assume by "serialized initialization state" you mean something like an abstraction of the binary executable generated by While I generally approve of the strategy of separating the serialized initial state from the runtime initial state, I think we will need to be very careful about introducing too much flexibility in this step, because it has almost complete debugger-like freedom to observe anything at all about how the binary is structured, and this is an optimization killer. The direction I am advocating here is that "Rust allocations" have to be read and written using mechanisms that the compiler is aware of, while "external allocations" are set up in a host-dependent way and the compiler has little insight into them. |
@digama0 I still don't understand what you are concerned about here. Of course if the user declares there to be some "initial step" that is happening, it has the responsibility of actually arranging the physical machine state to match. This works exactly like asm blocks, where the user declares which state change they incur to the AM, and has to ensure the actual asm content matches that.
What is the benefit of making them opt-in?
The compiler as it works today implements the suggested spec correctly (to my knowledge). So it is indeed trivial to link such programs correctly. You just have to not make any arguments based on "I know the initial value of this Are we even talking about the same situation? I am still puzzled about you perceiving the situation so differently. You keep claiming this spec is impossible to implement, but it is in fact already implemented by today's rustc. |
Can you explain in more detail how you would argue that a compiler that puts the wrong values in static mut initializers and says "user, you deal with it" is not conforming?
I don't think it is impossible to implement this, but it does make some future things impossible that I would rather not bake into the spec. With this, Miri and other closed-world targets have to buy in to nondeterminism even when none is requested. (I hope you would agree that starting every Miri program with
This makes the difference between this kind of indeterminate value being a very rare situation confined to people who like to play with linker settings, to something that every single rust program (containing a |
That's absolutely not what anyone is saying? If you don't do any shenanigans, the initial value of When we say "isn't required," we're talking about the guarantee from the user to the compiler, not the compiler to the user. A compiler would still be wrong to initialize It's not "you have to accept compilers will change the value," it's "you're allowed to change the value." |
Miri will only support the case where the statics match the value declared in Rust. Developers that decide to use another initial state won't be able to directly use Miri. This is not fundamentally new; developers that run Rust in a larger context (e.g. with threads running non-Rust code) similarly cannot use Miri.
That's not accurate. Only developers that want to use this feature have to even consider this. I thought we were all pretty clear about that? You are entirely misrepresenting what we are suggesting to do here, and I don't understand where this misunderstanding comes from. |
And in what way is that conforming to the spec? Suppose the user does it anyway and gets a "wrong" result because the initializer value didn't actually change after some linker shenanigans that "should have worked". Did they cause UB, or is the target not conforming? Can targets say "it is UB to do linker shenanigans" and thereby enable compiler optimizations that assume the absence of such?
This is not a question of the user side of things. I agree that this not something most users will have to consider. I'm talking about the possibilities for compilers on the "analyze and propagate everything" side of the spectrum, on platforms which are sufficiently locked down to make it feasible to do that, like Miri.
Or to flip the perspective, "compilers have to accept you will change the value". Hence Miri is not correct here for not accepting a change to the value. I'm arguing that compilers/targets should be permitted to say "we do not accept changes to static initializers" as an implementation-defined matter, as there are some targets (like Miri) which cannot reasonably support this. |
I do not think that it is useful to discuss Miri here. |
As I've been saying, the issue is not the flags that are passed to |
(I can't resist responding to this, sorry...) It is not so much that I don't believe linkers exist but rather that the purpose of the spec is to relate executions directly to source code, which means that the whole process "rustc -> linker -> dynamic linker -> execution on target" that turns source code into behavior is within the domain of the spec, and the division between "rustc" and "linker" is an implementation defined thing which is not any more special than the division between "MIR" and "codegen" or other internal compiler stratifications. Indeed as you point out Miri is an interpreter which means that there is no such division, it goes straight from source code (well there is some pre-compilation to MIR) to behavior without all the intervening steps. If the user decides to stick their hands in the middle of that carefully orchestrated sequence and do something to the binary (before, during, or after linking), obviously we need to control what they are legally allowed to do because you can get arbitrary behavior pretty easily by doing that. "Thou shalt not modify rust allocations without a linker attribute" seems like a sensible rule in this regard, possibly requiring refinement to cover plausible use cases.
I agree. By default, if you don't put any fancy attributes on a static then linker shenanigans should be quite limited (dare I say prohibited?) such that they can be treated roughly like a I don't think ...Actually nevermind, |
Optimizations of C/C++ Example: https://youtu.be/p9nH2vZ2mNo?t=345 Note that Teresa says "the compiler tells the linker that I know this link isn't exactly what you're looking for, we'd probably want to re-create this situation, and I'm not certain that LTO is part of this discussion. |
sigh that's what I get for trying to type out a response on a phone I guess (I tried to scroll and closed the issue) |
Rust already can do a lot of these sorts of optimizations, due to its compilation model giving it more information out of the box than C++ compilers have with LTO. TBH, I don't know how many we're leaving on the table that this would change. That said, I think it's reasonable to require things like |
Also, a common optimization that C++ compilers want to do there is turn a mutable static that's never written to into a constant. I suspect that Rust has far fewer of these in the first place — people don't tend to use mutable statics in Rust if they're actually read-only. |
Private static mut X: i32 = 42;
pub fn foo() -> i32 { unsafe { X } } the generated code just returns 42 directly, because LLVM sees that |
Right, there's no need for LTO to determine that such a thing for private variables. LTO is used to tell that that's globally true. |
(footnotes are ignorable context) To summarize my understanding of the consensus here (and I think everyone did end up agreeing on these points):
I'd say that the question of whether Whether it's valid to assume Footnotes
|
(I agree with the summary.)
Just to be clear, I think this would block the LTO optimization demonstrated earlier, right? Is there ever a point at which it is possible to look at the "whole program" and say that even things that are |
We explicitly tell the linker which symbols are exported and pass the same list to LLVM when doing LTO. For dylib this is all symbols exported from any codegen unit, for cdylib and staticlib this is just the |
At least somewhat interesting that that doesn't include I will maintain a preference for matching the definition of symbols that would be from exported staticlib/cdylib over my looser concept of what (The |
Related: #215 |
Curious if the current Rust language definition, as of 28-Jan-2025, can make any guarantees about not optimizing relative to the initializer expression for static mut objects. We have basically exactly the same issue that prompted the original question. On an embedded platform, we have a log that was populated by the boot process and stuffed into SRAM. It is treated as a All that is to say, what I'm doing seems to work, but I worry that that may not always be true. I'm curious if the use of the UnsafeCell is basically resulting in a guarantee that compiler/optimizer can't assume it knows the memory contents even though it sees the initializer expression (which isn't actually used). |
It sounds like an extern static could be used in your situation? That is the intended way to inform Rust about static memory that comes in from the outside.
|
I will experiment, but my understanding is that extern static won't result in allocation (which would be necessary for creating the section in the generated object file) and is instead more like a C extern declaration (which just says somebody somewhere is providing this symbol of this type). |
In your linker script you can allocate some memory in RAM and then define the symbol corresponding to the static to be at the start of this memory. You need a linker script anyway to ensure that it doesn't get cleared on reboot. |
If the data is initialized already, then it must also already be allocated. IIUC, in your case the data is already there before Rust starts, but because Rust happens to assume that memory is 0-initialized, and since you are writing a 0-initializer in Rust (or an initializer that leaves the memory uninit), it happens to be the case that if you do not zero-initialize the memory then you can read whatever the actual contents of this section are. That's definitely UB, we don't guarantee that we'll do an optimization like that -- the program might in fact write out the initializer value upon startup even if it is all-0. This issue is about potential other code running after program loading but before |
We absolutely have a linker script (what embedded SW doesn't? :) ). But the linker doesn't do allocation, it just assigns sections to memory. The sections are coming from the object files though. But perhaps this is the problem (because you're correct that I can do "allocation" in the linker script). I think with Rust extern static that would be required, but I still need to run those experiments. The data absolutely is there before Rust starts. In the same way that |
I think you can do something like
with 1024 replaced by whatever size you want. |
Rust generates an object file instructing the linker to fill a particular region of memory with 0s. But then actually you set things up in a way that the memory contains other data. Is that correct? That does seem questionable, though maybe one can argue that it is equivalent to first having it initialized with 0s and then having some outside-of-Rust code run that writes the data you actually want to see there. So as long as the static is (interior) mutable, this will probably work... but I would argue |
Yes this would be the kind of thing one would do. Unfortunately this means the size info now has to be declared in two distinct places (the linker file and the Rust code file). For more complicated composite types this could get very annoying. For simple byte arrays, still not ideal but not a show-stopper. This is why it is much preferred to have the allocation, via declaration and compilation, happen in code. And this is typically what embedded C guys do. |
If we can get a documented guarantee from LLVM that this kind of thing is sound to do, it should be fairly easy to also provide this guarantee on the Rust level. |
Some godbolt output for 32-bit ARM (common embedded target): C version: https://godbolt.org/z/rvxr86v7z So the allocation doesn't happen in the last case, as predicted. The Rust warning in that case is also a bit interesting ( |
Well I ended up going with something like this: https://godbolt.org/z/5qjaesz9z I looked at the MIR output for the two cases (explicit init vs MaybeUninit::uninit) and the alloc statements for the arrays do show the init vs uninit difference. This seems as close as I can get to the C case where the array is declared global without an initializer. |
You could use an extern static right? Then you wouldn't need to provide an initializer, and this issue doesn't affect you. |
As mentioned previously |
Well, you are doing FFI. Some other component is filling that static with data -- clearly an FFI usecase.
FWIW for rustc it makes little difference whether the |
"Okay I think we first and foremost have a Rust Abstract Machine / @rust-lang/opsem question here, not a Miri question:
If a mutable (or interior mutable) static is initialized in Rust to a certain value, then can the Rust AM assume that it does have that value when the program starts? Or is it okay to have some "before main setup" actually change the value of that static?
I would say for an immutable static, this is clearly UB -- using linker script tricks like what has been described here is just not allowed for those statics. But for statics that can be mutated, we already can't in general optimize assuming their values did not change, so... it seems reasonable to allow this?
With this way of thinking about the question, obviously Miri has no way of knowing that you are modifying the value of the static before Rust code starts running. On the Rust side you are saying this static is filled with uninit memory, but then you are making some outside-of-Rust assumptions to justify that actually when the program starts the static has a different value, in particular that the bytes now are initialized. So I don't think there is anything actionable on the Miri side here, and the discussion (with the clarified question) should move to https://github.com/rust-lang/unsafe-code-guidelines/ ."
Originally posted by @RalfJung in rust-lang/miri#2807 (comment)
The text was updated successfully, but these errors were encountered: