-
Notifications
You must be signed in to change notification settings - Fork 13.4k
Writing to a static is somehow allowed while initializing that same static. #142404
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
Comments
Another variant: #![allow(dead_code)]
struct Foo {
x: i32,
y: (),
}
static mut S: Foo = Foo {
x: 0,
y: unsafe {
S.x = 1;
},
};
fn main() {
println!("{}", unsafe { S.x });
} This code compiles and prints |
Fun. we even have tests for this... but apparently something is not catching it |
We thought #116564 only made it possible to read from statics while initializing them (which just gives you uninit memory). But looking at the diff a test definitely changed to allow writing to statics, too |
From my testing, reading from a static while initializing it produces a "encountered static that tried to initialize itself with itself" error. |
oh right, we had to prevent that for zsts as zsts perform no read, so we need to ensure they don't self-initialize |
Or maybe we said this was fine since the value was always getting overwritten by the finial expression anyway? Because we had no way to allow the final write to the static while forbidding other writes. Slowly some memories of thinking about this before are coming up |
Ok, yea, this was def intended (#116564 (comment)), although arguably not discussed sufficiently. I had another look at the impl and it would need a bit of extra interpreter machine logic to prevent (or more specifically to special case idk, considering
There's nothing really important to change here. Considering let mut data = (0, 0);
data = (1, {
data.0 = 5;
2
});
assert_eq!(data, (1, 2)); is fine, I don't see a good argument for rejecting the static equivalent |
Might be a good candidate for a clippy lint in that case. |
cc @rust-lang/wg-const-eval I think this is working as intended, but it is a bit surprising as it's rare that you can refer to the address of something before it is initialized |
IMO that is the main point: there's no difference between writing to But ultimately it is up to @rust-lang/lang whether they consider this problematic. |
I feel like I don't have a good tell on this behavior yet. It feels somewhat similar to the following, forbidden code: #![allow(dead_code)]
struct Foo {
x: i32,
y: (),
}
fn main() {
let s: Foo;
s = Foo {
x: 0,
y: unsafe {
(&raw const s.x).cast_mut().write(1); // Writing to an immutable static that's still being initialized!
},
};
println!("{}", s.x);
} If I understand correctly, we do not allow reading from statics in their initializer, i.e. the following errors with "could not evaluate static initializer" https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=47e4a7aebe19fdd8b8c7f4e45fa8fa81: static S: Foo = Foo {
x: 0,
y: unsafe {
let _ = (&raw const S.x).cast::<()>().read();
},
}; Is the following summary correct:
This means while the writes to the static are weird, they do not cause any opsem or soundness issues. I personally feel like we should forbid these useless writes. Afaict they are always irrelevant and I can't think of a case where it would be desirable. If we only allow initializing the static with the final value of its initializer were trivial to do, do you think we'd still want to support these irrelevant earlier writes? |
I don't want to support these irrelevant earlier writes. It's just a bit of extra complexity to prevent them, so we actually need to want to prevent them explicitly and thus take the complexity hit. |
I wouldn't mind forbidding them if there was a nice way to do it. But syntactically special-casing the |
It feels somewhat surprising to me that we even treat the static and the return place as the same thing instead of evaluating the static initializer just like another const body and then manually using its result as the value of the static 🤔 why is this the case? |
We do treat it like any other const body. Const bodies evaluate into a return destination (i.e., they return a place, not a value). For statics, we make that return destination be the static itself. For some time we had a "temporary" return destination, but that lead to the uncomfortable situation where the AllocId representing the static was different when evaluating the static vs later. |
why this? and how do we make sure that the |
I guess we also have |
We don't have two AllocIds for a static any more so there's no problem any more. :) And yes, every allocation has an AllocId -- locals, globals, heap allocations, vtables, functions (as the target of function pointers), everything. |
If we allow this to work, then we won't be able to do RVO anymore, nor would language builtin pin-init be possible. As those would avoid _0 being fully overwritten after |
We don't optimize consts so RVO isn't much of a concern. Not sure how pin-init would work in consts? |
Thanks for the report and analysis. We'll discuss. For my part, as @lcnr pointed out, it does seem a bit odd that these are inconsistent: // With local.
let x: (u8, ());
x = (0, unsafe {
(&raw const x.0).cast_mut().write(1);
//~^ ERROR used binding `x` isn't initialized
});
// With static.
static X: (u8, ()) = (0, unsafe {
(&raw const X.0).cast_mut().write(1);
//~^ OK?
}); Regarding the complexity hit here, sometimes, in the past, it's been easier to add a lint for something than to make it a hard error (since it was OK if the lint was only mostly correct). Any chance that this is one of those cases? |
This is a direct consequence of the following, fully intentional "inconsistency": struct S(&'static S);
static MY_S: S = S(&MY_S); // works
let my_s = S(&my_s); // does not work |
This comment has been minimized.
This comment has been minimized.
Sorry, I had a little typo: struct S(&'static S);
static MY_S: S = S(&MY_S); // works |
Thanks. Is the following also equivalent to your point (i.e. without that struct S(*const S);
unsafe impl Sync for S {}
fn main() {
static MY_S: S = S(&raw const MY_S);
let my_s;
my_s = S(&raw const my_s);
//~^ ERROR used binding `my_s` isn't initialized
} |
Yes. More broadly, we allow statics to recursively point to themselves. This goes waaaay back. That makes statics consistent with other items like functions and types that are also allowed to recursively refer to themselves. We don't allow the same for |
I'd expect |
Yeah, if we allowed taking a raw pointer to an uninitialized local we would likely accept that code. We'd have to ensure the local is considered "live" by MIR building the moment a raw borrow occurs. |
I tried this code:
I expected to see this happen: An error of some sort, or Miri reports UB.
Instead, this happened: The program compiles and outputs
0
. Miri does not detect UB.Note that writing to a static while initializing a different static is not allowed. For example,
this produces the following error
Meta
Reproducible on the playground with
1.89.0-nightly (2025-06-11 e703dff8fe220b78195c)
@rustbot labels +A-const-eval
The text was updated successfully, but these errors were encountered: