-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
[libc++] std::expected
: operations may overwrite bytes of unrelated objects
#70494
Comments
…ternally llvm/llvm-project#70494 points out that `[[no_unique_address]]` on a union member can cause its placement-new construction to clobber data in its tail padding, as ctors can and do write their entire `sizeof()` size, not just their data-size. So we do some wild things to prevent this from occurring: The `T` and `E` union are grouped with the state into a struct, which we call `Inner`. This structure is always constructed together as a group when changing the state to `Ok` or `Err`. This ensures that we construct `T` or `E` and then set the state (which follows in the struct) thus avoiding clobbering of the state. Second, the `Inner` struct needs to be protected from clobbering other things in its tail padding. So it is wrapped in another struct that conditionally applies `[[no_unique_address]]` to it, which we call `MaybeNoUniqueAddress`. We mark `Inner` as `[[no_unique_address]]` only when there is no tail padding in `T` or `E`. Then the tail padding of `Inner` which is introduced by the state flag can be used by things external to the Result if `T` are without tail padding. All of this is put into the outermost structure `StorageVoid` or `StorageNonVoid` for a void `T` or not, respectively. Everything is marked `[[no_unique_address]]` except `Inner` which is conditionally marked as mentioned above. In order to do this, we've rewritten the entire implementation of `Result`'s storage. In the process, copy/move operations and destructors are all trivial if the same for both `T` and `E` are trivial, which fixes chromium#403.
…ternally llvm/llvm-project#70494 points out that `[[no_unique_address]]` on a union member can cause its placement-new construction to clobber data in its tail padding, as ctors can and do write their entire `sizeof()` size, not just their data-size. So we do some wild things to prevent this from occurring: The `T` and `E` union are grouped with the state into a struct, which we call `Inner`. This structure is always constructed together as a group when changing the state to `Ok` or `Err`. This ensures that we construct `T` or `E` and then set the state (which follows in the struct) thus avoiding clobbering of the state. Second, the `Inner` struct needs to be protected from clobbering other things in its tail padding. So it is wrapped in another struct that conditionally applies `[[no_unique_address]]` to it, which we call `MaybeNoUniqueAddress`. We mark `Inner` as `[[no_unique_address]]` only when there is no tail padding in `T` or `E`. Then the tail padding of `Inner` which is introduced by the state flag can be used by things external to the Result if `T` are without tail padding. All of this is put into the outermost structure `StorageVoid` or `StorageNonVoid` for a void `T` or not, respectively. Everything is marked `[[no_unique_address]]` except `Inner` which is conditionally marked as mentioned above. In order to do this, we've rewritten the entire implementation of `Result`'s storage. In the process, copy/move operations and destructors are all trivial if the same for both `T` and `E` are trivial, which fixes chromium#403.
…ternally llvm/llvm-project#70494 points out that `[[no_unique_address]]` on a union member can cause its placement-new construction to clobber data in its tail padding, as ctors can and do write their entire `sizeof()` size, not just their data-size. So we do some wild things to prevent this from occurring: The `T` and `E` union are grouped with the state into a struct, which we call `Inner`. This structure is always constructed together as a group when changing the state to `Ok` or `Err`. This ensures that we construct `T` or `E` and then set the state (which follows in the struct) thus avoiding clobbering of the state. Second, the `Inner` struct needs to be protected from clobbering other things in its tail padding. So it is wrapped in another struct that conditionally applies `[[no_unique_address]]` to it, which we call `MaybeNoUniqueAddress`. We mark `Inner` as `[[no_unique_address]]` only when there is no tail padding in `T` or `E`. Then the tail padding of `Inner` which is introduced by the state flag can be used by things external to the Result if `T` are without tail padding. All of this is put into the outermost structure `StorageVoid` or `StorageNonVoid` for a void `T` or not, respectively. Everything is marked `[[no_unique_address]]` except `Inner` which is conditionally marked as mentioned above. In order to do this, we've rewritten the entire implementation of `Result`'s storage. In the process, copy/move operations and destructors are all trivial if the same for both `T` and `E` are trivial, which fixes chromium#403.
…ternally llvm/llvm-project#70494 points out that `[[no_unique_address]]` on a union member can cause its placement-new construction to clobber data in its tail padding, as ctors can and do write their entire `sizeof()` size, not just their data-size. So we do some wild things to prevent this from occurring: The `T` and `E` union are grouped with the state into a struct, which we call `Inner`. This structure is always constructed together as a group when changing the state to `Ok` or `Err`. This ensures that we construct `T` or `E` and then set the state (which follows in the struct) thus avoiding clobbering of the state. Second, the `Inner` struct needs to be protected from clobbering other things in its tail padding. So it is wrapped in another struct that conditionally applies `[[no_unique_address]]` to it, which we call `MaybeNoUniqueAddress`. We mark `Inner` as `[[no_unique_address]]` only when there is no tail padding in `T` or `E`. Then the tail padding of `Inner` which is introduced by the state flag can be used by things external to the Result if `T` are without tail padding. All of this is put into the outermost structure `StorageVoid` or `StorageNonVoid` for a void `T` or not, respectively. Everything is marked `[[no_unique_address]]` except `Inner` which is conditionally marked as mentioned above. In order to do this, we've rewritten the entire implementation of `Result`'s storage. In the process, copy/move operations and destructors are all trivial if the same for both `T` and `E` are trivial, which fixes chromium#403.
…ternally llvm/llvm-project#70494 points out that `[[no_unique_address]]` on a union member can cause its placement-new construction to clobber data in its tail padding, as ctors can and do write their entire `sizeof()` size, not just their data-size. So we do some wild things to prevent this from occurring: The `T` and `E` union are grouped with the state into a struct, which we call `Inner`. This structure is always constructed together as a group when changing the state to `Ok` or `Err`. This ensures that we construct `T` or `E` and then set the state (which follows in the struct) thus avoiding clobbering of the state. Second, the `Inner` struct needs to be protected from clobbering other things in its tail padding. So it is wrapped in another struct that conditionally applies `[[no_unique_address]]` to it, which we call `MaybeNoUniqueAddress`. We mark `Inner` as `[[no_unique_address]]` only when there is no tail padding in `T` or `E`. Then the tail padding of `Inner` which is introduced by the state flag can be used by things external to the Result if `T` are without tail padding. All of this is put into the outermost structure `StorageVoid` or `StorageNonVoid` for a void `T` or not, respectively. Everything is marked `[[no_unique_address]]` except `Inner` which is conditionally marked as mentioned above. In order to do this, we've rewritten the entire implementation of `Result`'s storage. In the process, copy/move operations and destructors are all trivial if the same for both `T` and `E` are trivial, which fixes #403.
@huixie90 and I just spent some time going through this issue again while working on #70506, which is basically the same issue but for First, is the following code valid?
If this code is valid, the EBO is arguably broken. One the one hand Second question: Is the following code valid?
This is clearly not a great way to implement This is very similar to the case brought up by @huixie90 in #68733 (comment):
There, @jwakely had said that it wasn't (in his opinion) something we want to support, and I agree. However, while the use case immediately above is hard to defend, something like an assignment operator using a destroy-and-construct pattern is probably much more common (although I don't know yet if it's technically legal). If it is legal, then I suspect we can't use I'm going to CC some folks here who might have knowledge/opinions about how this should be handled, sorry if this ends up being a bit spammy but we need to statute on this before we can make progress with either this issue or #70506. CC @jiixyj @huixie90 @philnik777 @frederick-vs-ja @zygoloid @jwakely Edit: Added |
@huixie90 and I went over https://eel.is/c++draft/basic.life#8, and it seems to say that the answer to question 2 is "Yes, this is valid". If that's true, then we can kiss the For now, I'm hoping we read https://eel.is/c++draft/basic.life#8 wrong but I am looking forward to hearing what other people think. |
I'm pretty sure this is UB. The standard seems to specifically exclude potentially-overlapping subobjects from any guarantees it makes regarding the value representation of objects. The thing you're most likely trying to do here is ending the lifetime of
I'd say no, for the same reason outlined above. |
But as long as we access the new object via the pointer we obtained from |
No, because |
Maybe to make it a bit clearer: [basic.life]#8 only holds if your object is transparently replacable. This is not the case for |
Okay, let's say we agree on that. But it still means that whether the Whether we're allowed to make this a potentially-overlapping subobject in the standard library is not clear to me, and it seems like we might need some wording to make that clear. And assuming that's what we want, because while it produces a great object representation, the flipside is that code written naively by users may instead exhibit rather subtle undefined behavior. |
Given that standard libraries already do this for decades seems to imply that we can. I'm also not aware of the standard giving any general restrictions on how standard library objects are represented other than it being a representation within the bounds of the standard (e.g. we can't change the alignment). I think some clarification would be good - especially around containers, but I don't think that should stop us from using a good representation.
Yes, to some degree. But I would also argue that |
I'm not sure whether this makes me want to use |
the user does not need to write ‘std::launder’ to make its code valid. It is just valid on its own. It is us who make it potentially overlapping. the very paragraph you quoted in the standard has almost exact same example as Foo without std::launder and with comment “well defined” |
It seems to me that:
So I think it's fine to store objects of user-defined types as potentially-overlapping subobjects, and the burden is on the library user to not trample outside their own object. If libc++ wants to support user-defined types that |
That's true. But we should call it out explicitly, which I don't think we do right now. And honestly I'd be curious to hear what the general LEWG constituency would have to say about this if they were here -- I suspect they might find it surprising. Basically, it feels like this "affordance" is given to us by means of a subtle under-specification of the library -- it's not clear to me that this is the "design intent".
100%
I think I don't understand where the Standard says that the lifetime of the enclosing object will end if the lifetime of its potentially-overlapping subobject ends and then another object is put in its place. I looked at all of Is it http://eel.is/c++draft/basic.life#1.5 which says:
and somehow a potentially-overlapping subobject is not considered to be nested within its enclosing object for that purpose?
I think the major difference here is that an empty object doing Don't get me wrong -- I want us to have the efficient representation for I'd be really curious to hear what @jwakely has to say here -- do you think I'm being overprotective of users doing something stupid? Would you take a LWG issue that asks to clarify this? Or perhaps this would need to be an actual paper? |
Definitely, it seems like a good idea to me for the standard to explicitly call out that library types can hold user-defined types as potentially-overlapping subobjects -- or, better, to say exactly when that's permitted. Maybe the elements of a
If we set aside
[*]: I say the exact same, but that's not quite true. Prior to
That's one half of the rule. The other half is the "transparent replacement" rule in [basic.life]/8, which describes the circumstances under which a new object (eg, created by The (intended) upshot is: if you create an object in some storage, then either:
(There are actually a few more cases here, but those are the main ones. For example, you can create an object inside a byte array without ending the lifetime of the array or things enclosing it. And there are cases where the new object is a member subobject of an existing object, but the name of the member can't be used to access it -- or at least, there were, when we didn't allow transparent replacement of const subobjects.) For potentially-overlapping objects, you can't transparently replace them ([basic.life]/8.4), so you end up in case 2. |
Yes, I agree with this and I am not questioning the design intent when it comes to EBO. All implementations do it and it's expected by users -- in fact a library that doesn't do it would be considered to have a poor QOI. For
I see, thanks a lot for the clear explanation, I appreciate it. So basically, we're doing a non-transparent replacement of the sub-object, which means that we're not considered "nested" inside the Okay, so I suggest the following plan:
|
Currently std::expected can have some padding bytes in its tail due to [[no_unique_address]]. Those padding bytes can be used by other objects. For example, in the current implementation: sizeof(std::expected<std::optional<int>, bool>) == sizeof(std::expected<std::expected<std::optional<int>, bool>, bool>) As a result, the data layout of an std::expected<std::expected<std::optional<int>, bool>, bool> can look like this: +-- optional "has value" flag | +--padding /---int---\ | | 00 00 00 00 01 00 00 00 | | | +- "outer" expected "has value" flag | +- expected "has value" flag This is problematic because `emplace()`ing the "inner" expected can not only overwrite the "inner" expected "has value" flag (issue #68552) but also the tail padding where other objects might live. This patch fixes the problem by ensuring that std::expected has no tail padding, which is achieved by conditional usage of [[no_unique_address]] based on the tail padding that this would create. This is an ABI breaking change because the following property changes: sizeof(std::expected<std::optional<int>, bool>) < sizeof(std::expected<std::expected<std::optional<int>, bool>, bool>) Before the change, this relation didn't hold. After the change, the relation does hold, which means that the size of std::expected in these cases increases after this patch. The data layout will change in the following cases where tail padding can be reused by other objects: class foo : std::expected<std::optional<int>, bool> { bool b; }; or using [[no_unique_address]]: struct foo { [[no_unique_address]] std::expected<std::optional<int>, bool> e; bool b; }; The vendor communication is handled in #70820. Fixes: #70494 Co-authored-by: philnik777 <nikolasklauser@berlin.de> Co-authored-by: Louis Dionne <ldionne.2@gmail.com>
Some operations, like
emplace()
, might overwrite bytes of unrelated objects (example from #68733 (comment)):This happens because currently,
std::__libcppdatasize<std::expected<bool_with_padding, bool>>::value == 2
: One byte because ofstd::__libcppdatasize<bool_with_padding>::value == 1
and one additional byte forexpected
's "has value flag.However, the
emplace()
operation destructs/constructs anotherbool_with_padding
in place -- and that will overwrite allsizeof(bool_with_padding) == 8
bytes of the object, clobbering the six unrelatedbool
s that are stored inexpected
's tail padding.std::expected
has to make sure that all of its tail padding (if it has some) cannot be touched by any destructor/constructor calls of the value/error.There is a PR up that has a work in progress fix and some more discussion: #69673
The text was updated successfully, but these errors were encountered: