-
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++] Fix UB in <expected> related to "has value" flag (#68552) #68733
Conversation
libcxx/test/std/utilities/expected/expected.expected/observers/has_value_clobber.pass.cpp
Outdated
Show resolved
Hide resolved
✅ With the latest revision this PR passed the C/C++ code formatter. |
Thanks for working on this! @ldionne This looks like a case where we want to have an optimized test run - especially, since none of the static analyzers or sanitizers caught this. |
Sure, I can try! Are you thinking of something like this? _LIBCPP_HIDE_FROM_ABI constexpr expected(const expected& __other) noexcept(
is_nothrow_copy_constructible_v<_Tp> && is_nothrow_copy_constructible_v<_Err>) // strengthened
requires(is_copy_constructible_v<_Tp> && is_copy_constructible_v<_Err> &&
!(is_trivially_copy_constructible_v<_Tp> && is_trivially_copy_constructible_v<_Err>))
: __union_([&] {
return __other.__has_val_ ? __union_t<_Tp, _Err>(__construct_val_tag{}, __other.__union_.__val_)
: __union_t<_Tp, _Err>(__construct_unex_tag{}, __other.__union_.__unex_);
}()),
__has_val_(__other.__has_val_) {} ...i.e. using the |
Yes, something like that was on my mind. My only nit is that we might want to add a few static member functions to avoid a bunch of logic inside the initializer. |
libcxx/test/std/utilities/expected/expected.expected/observers/has_value_clobber.pass.cpp
Outdated
Show resolved
Hide resolved
libcxx/test/std/utilities/expected/expected.expected/observers/has_value_clobber.pass.cpp
Outdated
Show resolved
Hide resolved
libcxx/test/std/utilities/expected/expected.expected/observers/has_value_clobber.pass.cpp
Outdated
Show resolved
Hide resolved
Nevermind, this is fudging terrifying. I didn't realize [[no_unique_address]] could have such large foot-guns aimed at our face. |
b4d2dec
to
9832a97
Compare
I followed your advice and updated the PR, please have a look :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for spotting the issue and send the PR. I am still a bit nervous about object overlapping. Since expected
can expose its data through several members like operator*
, operator->
, value()
, error()
, etc.... Could users trigger this UB by other means that we can not stop from. like
std::expected<T, U> e = ...;
e->someMemberThatCouldWriteToObjectThatChangesHasValue()
or even,
std::expected<T, U> e = ...;
std::construct_at( std::address_of(*e), args...);
What do you think?
I guess
I now wonder if static_assert(sizeof(std::expected<std::optional<int>, int>) == sizeof(std::optional<int>));
static_assert(sizeof(std::expected<std::expected<std::optional<int>, int>, int>) == sizeof(std::optional<int>));
static_assert(sizeof(std::expected<std::expected<std::expected<std::optional<int>, int>, int>, int>) == sizeof(std::optional<int>)); ...i.e. that Maybe the initial UB wasn't from the order of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if a user have the following
struct Foo {
[[no_unique_address]] std::expected<std::optional<int>, int> x_;
[[no_unique_address]] bool y_;
};
would the operations on the x_
overwrite y_
? from what I understand it will
struct tail_clobberer { | ||
constexpr tail_clobberer() { | ||
if (!std::is_constant_evaluated()) { | ||
// This `memset` might actually be UB (?) but suffices to reproduce bugs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should be fine. IIUC the type is allowed to overwrite padding bytes during construction.
libcxx/test/std/utilities/expected/expected.expected/observers/has_value.pass.cpp
Outdated
Show resolved
Hide resolved
This is a simplified example. I don't think we can fix it. It is a mistake to use |
I think I agree. It's probably still early enough to fix the ABI (especially given that these bugs are pretty severe). In LLVM 16 C++23 features were still experimental and we can backport this to LLVM 17. @ldionne do you agree? Should we add an ABI tag? Is this severe enough to warrant a 17.1 release? (CC @tru @tstellar) I think we can still put at least |
Ah, so then As an alternative, I wonder if the current ABI is salvageable by saving/restoring tail padding in all "dangerous" functions (i.e. swap, emplace, assign, etc.) with something like: struct __tail_padding_saver {
static constexpr auto __padding_offset = __libcpp_datasizeof<expected>::value;
static constexpr auto __padding_size = sizeof(expected) - __padding_offset;
expected *__e;
unsigned char __pad[__padding_size];
explicit __tail_padding_saver(expected *__e) : __e(__e) {
__builtin_memcpy(__pad, reinterpret_cast<unsigned char *>(__e) + __padding_offset, __padding_size);
}
~__tail_padding_saver() {
__builtin_memcpy(reinterpret_cast<unsigned char *>(__e) + __padding_offset, __pad, __padding_size);
}
}; Or even have some helper functions construct_at_until(std::addressof(__union_.__val_), &__has_val_, std::forward<_Args>(__args)...);
destroy_at_until(std::addressof(__union_.__val_), &__has_val_, std::forward<_Args>(__args)...); ...that save/restore all needed bytes "under the hood"? |
I think we are simply talking about
so that
I think this fixes the problem but break the ABI. Another point is that if we are going to break the ABI anyway, maybe another option is to get rid of |
We could get rid of |
I tried something similar on a separate branch as a POC: jiixyj@86fe890#diff-99bc4a85cb97f33ff277458a531ca47376ceba6ea7d84329cabf02857941ed3eR976 All Could that be a way out of this? |
But I guess it won't work in constant expression and it does not work in multithreading cases as the memory is changed then changed it back later |
Ah yes, the multithreading issues might be a deal breaker for this approach :/ |
I implemented the proposed (ABI changing) fix here and all tests pass! With that patch |
Is the following valid user code? If so, we will still have a problem
|
libcxx/test/std/utilities/expected/expected.expected/swap/free.swap.pass.cpp
Show resolved
Hide resolved
libcxx/test/std/utilities/expected/expected.expected/swap/member.swap.pass.cpp
Show resolved
Hide resolved
@ldionne : I think I addressed all review comments -- thanks again to all reviewers! I have added some "TailClobberer" tests for @huixie90 : There is an unresolved conversation/comment from you here about the design of the "TailClobberer" class. Do you think we can leave it like it is in this PR and do further test refactoring, if needed, in followup PRs? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the work on this!
This one is actually not an ABI break, so I would cherry-pick it back to LLVM 17 regardless of what we end up doing with the tail padding issue (which is the actual ABI breaking issue). I can handle that.
libcxx/test/std/utilities/expected/expected.expected/swap/free.swap.pass.cpp
Show resolved
Hide resolved
…68733) The calls to std::construct_at might overwrite the previously set __has_value_ flag in the case where the flag is overlapping with the actual value or error being stored (since we use [[no_unique_address]]). To fix this issue, this patch ensures that we initialize the __has_value_ flag after we call std::construct_at. Fixes #68552 (cherry picked from commit 134c915)
The calls to
std::construct_at
might overwrite the previously set__has_value_
flag, so reverse the order everywhere.Fixes: #68552