-
Notifications
You must be signed in to change notification settings - Fork 471
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
Soundness of AtomicCell::compare_exchange is dubious #315
Comments
If you don't mind me asking, is the |
Well, the code checks that the sizes match. Ignoring complicated issues around pointers, the only way in which transmuting a 4-byte piece of data to |
It seems C/C++ standard committee members tend to agree that all bit patterns are valid for |
@RalfJung That sounds overly restrictive to me. IMO, it should be possible to transmute a value of any type |
@stjepang To be very pedantic (sorry!), And yet I agree with your sentiment: I believe representing an object of any type as |
I am not aware of any precedent for u8 being special in Rust, and in fact all the discussion of the invariants of integer types I'm aware of treats all integer types alike. It's true that in C and C++, |
Thanks for the clarification! I agree with you and would expect |
This is Rust, not C++. :)
Remember that every bit can be 0, 1 or uninitialized. Even the C++ standards committee acknowledges this, though just indirectly -- namely, it acknowledges that "indeterminate values" can be "unstable" in the sense that If we allow that, we have to answer other hard questions like what happens when you do arithmetic or comparison involving uninitialized bits. That's why I think that any uninitialized bits (outside of padding) should only exist in a
|
@RalfJung Fair enough, this is difficult. :( It might be helpful to look at what C/C++ do on atomic compare-exchange with arbitrary (non-POD) data. This is interesting:
Source: https://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange Boost atomics will do |
Yeah, I figured this would be related to
|
@Amanieu @stjepang with the newly proposed |
That doesn't exactly solve the problem though: However since the padding bytes in |
@RalfJung Awesome news! @Amanieu Are you sure? We have this if statement that should solve the issue you're describing. Note that |
@stjepang Ah right. So you won't get spurious failures, but you could in theory get stuck in an infinite loop. |
@Amanieu Hmm, not sure I understand how. If we do Could you perhaps sketch out a simple example? I don't see the theoretical infinite loop here. |
Let us assume some 2-byte type
Now that I look at it this way, I think this is fixable if you keep the previous value as a |
Thanks! I agree about keeping the previous value as a |
Ah good point, yes you need to do the entire loop without round-tripping through It is a bit dissatisfying that we cannot also have Pragmatically speaking, an |
Any thoughts on replacing |
These could freeze, so yes, they could be implemented in a sound way in this model. |
332: Deprecate AtomicCell::get_mut r=stjepang a=stjepang This is an essential step in solving #315 - there is no way to make `AtomicCell::get_mut()` sound, unfortunately. Let's deprecate it and remove in the next release. Co-authored-by: Stjepan Glavina <[email protected]>
I believe (1) Rust should not blame padding bytes as Paddings are not |
AFAIK padding bytes are indeterminate in C/C++. That's the same status as uninitialized memory, and it is very different from unspecified. Whether or not computations on indeterminate values are UB is not clear from the standard, but:
Source: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1793.pdf In particular, comparing indeterminate values with anything is meaningless, because they do not have a stable bit pattern. This is a problem for Also, another point not clear from the standard is what happens when you |
According to C17 6.2.6p6, "padding bytes take unspecified values":
|
Interesting. However, we also have C17 6.7.1 p9:
and 6.7.2.1 p15 + p17:
Also, to my knowledge, making padding indeterminate is needed to make SROA (scalar replacement of aggregates) a valid optimization. |
How would it make any sense that those members do not get initialized?
Given that padding is explicitly called out as "unnamed", I disagree with that reading. Notice that this interpretation is not just mine, I took it from https://wiki.sei.cmu.edu/confluence/display/c/EXP42-C.+Do+not+compare+padding+data which explicitly quotes the above parts of the standard when talking about padding.
I don't follow. Uninitialized variables are indeterminate. Why would their padding not be...? The standard is generally extremely vague about all this indeterminate/unspecified/trap representation business. I think it is much clearer to follow the approach proposed for LLVM, where "indeterminate" becomes |
|
I see where you are coming from. OTOH, I feel if the people doing the CERT C Coding Standard interpret it the way I quoted, that probably means they are not alone. So at this point it probably stops being useful to stare at the vague C standard.
Yeah, unfortunately the entire "trap representation" stuff in C/C++ is really poorly specified. For example, integers also don't have "trap representations" (I believe the next standard will even say that), and yet in the mainstream C compilers there are "weird" integers like So it is probably more useful to look at what compilers actually do. The only one I am familiar with and the most relevant for Rust is LLVM. LLVM trunk is inconsistent in what it does, but there is a proposal for a consistent model. In that model, structs and struct fields can definitely be So, in the
Similar things are said in this bugreport. @rkruppe do you know if the LLVM IR specifies this padding stuff properly anywhere? Since a mere read access to some location in memory cannot even "know" if it is reading padding or whatever, the only way I see to realize "reading padding gives Also see this where more people agree with me that inspecting padding is UB in modern compilers (they say that violates the standard, which I am not sore sure about, but given the ambiguity of the standard that does not seem like a useful discussion). |
I think the difference is on the viewpoint. I, as a systems programmer, think that To summarize my viewpoint, (1) @stjepang For this reason, I'd like to propose to revert #332 (Deprecating AtomicCell::get_mut). |
I would like to support it! However, the way we ended up in this messy situation with C/C++ is by ignoring the fact that a language must have a single consistent semantics, and relying on mutually incompatible axioms and intuition instead. In my view, your argument is flawed. From a systems programmer's viewpoint, one could argue that all sorts of things are "sane" -- things like benign data races or OOB loads on a definitely mapped page or implementing small atomic accesses with bigger accesses. The issue is that these arguments mix up intuition formed by looking at "what the hardware does", with "what the programming language is supposed to do". This is not a sensible way to argue, and it leads to miscompiled programs quickly. What you are doing here is closing your eyes in the face of the problem and hoping that that makes it go away. It won't go away. It's not just "the language's fault"---we can make So, in an attempt to make this conversation more constructive, what kind of feature would we need in the language to make So, we could have fn store(&self, mut val: T) {
ptr::freeze(&mut val);
// do the atomic store, *at a type that preserves padding*
}
fn compare_exchange(&self, mut val1: T, mut val2: T) {
ptr::atomic_freeze(self.value.get());
ptr::freeze(&mut val1);
ptr::freeze(&mut val2);
// do the CAS loop, *at a type that preserves padding*
} @Amanieu does that make sense to you? I am mostly worried about what it means that we now might perform a "write" operation even if the CAS failed. |
I think an argument could be made that In the end I think we should just do whatever C++ does (where |
I was told that LLVM is looking into transformations that will replace atomic operations by non-atomic operations when the compiler can prove that there are no race conditions. That however means that we cannot say that atomic operations read frozen values. |
@RalfJung I still think my previous comment quite clearly summarizes what the standard says about padding (they are just random bits and safe to access), and I think actually it's quite good semantics for everyone. As far as I understand, existing optimizations, as well as system programmer's idioms, are supported. In particular, So to say |
PR crossbeam-rs#332 deprecated after the discussion of issue crossbeam-rs#315, but it is a measure not yet necessary to take. Revert the deprecation and wait for us to reach a consensus.
PR crossbeam-rs#332 deprecated after the discussion of issue crossbeam-rs#315, but it is a measure not yet necessary to take. Revert the deprecation and wait for us to reach a consensus.
PR crossbeam-rs#332 deprecated after the discussion of issue crossbeam-rs#315, but it is a measure not yet necessary to take. Revert the deprecation and wait for us to reach a consensus.
Well, it's not how LLVM implements padding, so it's clearly not good for everyone, and certainly not good for Rust which uses LLVM as backend. I have heard your argument, there is no point in repeating it. I just don't agree, this is not how I interpret the C standard. I won't repeat my arguments now as you already know them. And anyway the C standard is only relevant insofar as we might use it to deduce things about the LLVM semantics. And there I can easily craft examples that show that padding can indeed turn into pub fn test(x: (u32, u64), f: fn(&(u32, u64))) {
let y = x;
f(&y);
} gets compiled to
As you can see, this creates a local allocation This can only be explained in Rust by saying that the I think this conclusively proves that in the LLVM model,
So your definition of "soundness" is "you can't give a counterexample"? I am afraid that is not a definition I can accept. You can't make a function "sound" by messing with it until the current compiler compiles it the way you want. I am honestly shocked to see you even propose this. I think there is a wide consensus in the Rust community that "soundness" has nothing to do with compiler optimizations. It is about whether your program has UB according to the spec. The spec doesn't exist yet but we can try to infer what pieces of it have to be, like I did in the beginning of this post. You don't get to redefine this term. Or maybe you gave up on soundness, and only care about this function doing the right thing de facto. That's a fair position, albeit not one I share. What is an interesting question is constructing a sequence of reasonable-looking optimizations that break |
Here's another example demonstrating that the padding of any struct has to be considered #![crate_type="cdylib"]
#[repr(C)]
pub struct X {
a: u32,
b: u64
}
pub fn foo(x: X) -> u32 {
unsafe {
return std::ptr::read((&x as *const X as *const u32).add(1));
}
} becomes
(Thanks to @nagisa.) |
People reading this might also be interested in the |
If Rust decides to guarantee that padding will always be |
That issue is about adding the ability for types to opt-in to guaranteed-0 padding. There is no proposal on the table that would make all padding always 0, and I doubt people would like that idea. |
The first line of that issue description is "one option to gain more layout optimizations would be (either for all repr(Rust) types or with per-type opt-in) to use a different kind of padding." But I agree that it might be hard to do it for all |
1: Remove miri hack r=taiki-e a=taiki-e Use currently use a hack to avoid rust-lang/rust#69488 and to make sure that Miri errors for atomic load/store of integers containing uninitialized bytes (which is probably not a problem and uncharted territory at best [1] [2] [3], and can be detected by `-Zmiri-check-number-validity` [4]), do not mask Miri errors for the use of uninitialized bytes (which is definitely a problem). https://github.com/taiki-e/atomic-memcpy/blob/3507fef17534e4825b2b303d04702b4678e29dd0/src/lib.rs#L426-L450 [1]: crossbeam-rs/crossbeam#315 [2]: rust-lang/unsafe-code-guidelines#158 [3]: rust-lang/unsafe-code-guidelines#71 [4]: rust-lang/miri#1904 However, this actually causes another "unsupported operation" Miri error. ``` error: unsupported operation: unable to turn pointer into raw bytes --> /Users/taiki/.rustup/toolchains/nightly-x86_64-apple-darwin/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:701:9 | 701 | copy_nonoverlapping(src, tmp.as_mut_ptr(), 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unable to turn pointer into raw bytes | = help: this is likely not a bug in the program; it indicates that the program performed an operation that the interpreter does not support ``` Co-authored-by: Taiki Endo <[email protected]>
AtomicCell<T>::compare_exchange
will, if I understand the code correctly, useAtomucU*::compare_exchange_weak
ifT
has the right size. But this means that e.g. for(u8, u16)
, which has size 4, it will transmute these pairs intou32
and compare them as integers. However, such pairs contain a padding byte, meaning that theu32
contains some uninitialized memory. Having uninitialized memory in au32
is uncharted territory at best, but performing actual operations on such a datum (as opposed to just load/store) brings us very close to UB. I am not sure what the rules are in LLVM for this case, but under the proposedpoison
semantics I think the comparison will returnpoison
and hence using it in anif
is UB. For Rust, I think the intention is to make any operation on uninitialized data UB (like it is in C), meaning the comparison would already return UB.Strictly speaking, this affects not just the fast path, but also the arbitrarily-sized case:
bytes_eq
might compare uninitialized bytes.The text was updated successfully, but these errors were encountered: