Skip to content
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 "has value" flag getting clobbered unexpectedly #68552

Closed
jiixyj opened this issue Oct 9, 2023 · 6 comments · Fixed by #68733
Closed

[libc++] std::expected "has value" flag getting clobbered unexpectedly #68552

jiixyj opened this issue Oct 9, 2023 · 6 comments · Fixed by #68733
Assignees
Labels
ABI Application Binary Interface libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.

Comments

@jiixyj
Copy link
Contributor

jiixyj commented Oct 9, 2023

In some situations it seems that the "has value" flag of a std::expected gets overwritten when the value is constructed. Example:

#undef NDEBUG
#include <cassert>

#include <expected>
#include <optional>

std::expected<std::optional<int>, long> f1()
{
    return 0;
}

std::expected<std::optional<int>, int> f2()
{
    return f1().transform_error([](auto) { return 0; });
}

int main()
{
    auto r = f2();
    assert(r.has_value());

Godbolt: https://godbolt.org/z/nbcPr9e5f

I would expect that the assert is not triggered, but it is. I'm testing with commit d408770.

I strongly suspect code like this, i.e. where the call to std::construct_at happens after setting the "has value" flag:

template <class... _Args>
requires is_constructible_v<_Tp, _Args...>
_LIBCPP_HIDE_FROM_ABI constexpr explicit expected(in_place_t, _Args&&... __args)
noexcept(is_nothrow_constructible_v<_Tp, _Args...>) // strengthened
: __has_val_(true) {
std::construct_at(std::addressof(__union_.__val_), std::forward<_Args>(__args)...);
}

In the above example, __has_val_ (a bool) and __union_.__val_ (the std::optional<int>) overlap. I think std::construct_at is allowed to overwrite __has_val_ in this case by the language rules as the std::construct_at has no idea that the __has_val_ is there. It just assumes it has full 8 bytes to work with to construct the std::optional<int>.

The data layout of the problematic std::expected<std::optional<int>, int> looks like this:

            +-- optional "has value" flag
            |     +--+--padding
/---int---\ |     |  |
00 00 00 00 01 00 00 00
               |
               |
               +- expected "has value" flag

The expected's "has value" flag reuses one byte of the padding. It should be "1" in this case but is "0".

Suggested fix: move all assignments to the "has value" flag after the std::construct_at calls like this:

  template <class... _Args>
    requires is_constructible_v<_Tp, _Args...>
  _LIBCPP_HIDE_FROM_ABI constexpr explicit expected(in_place_t, _Args&&... __args)
    noexcept(is_nothrow_constructible_v<_Tp, _Args...>) // strengthened
  {
    std::construct_at(std::addressof(__union_.__val_), std::forward<_Args>(__args)...);
    __has_val = true;
  }

I wonder if this analysis is correct or if I'm missing some subtleties with std::construct_at.

@github-actions github-actions bot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Oct 9, 2023
@frederick-vs-ja
Copy link
Contributor

frederick-vs-ja commented Oct 9, 2023

It seems to me that the current implementation stategy triggers core language undefined behavior, although the related CWG issue is still open (CWG2757).

libc++ is making several subobjects potentially-overlapping by _LIBCPP_NO_UNIQUE_ADDRESS, so the std::construct_at call doesn't transparently replaces __union_.__val_ and thus reuses the storage of the whole expected ([basic.life]/8).

A safe resolution may be just removing _LIBCPP_NO_UNIQUE_ADDRESS...

@ldionne
Copy link
Member

ldionne commented Oct 10, 2023

@huixie90 Can you take a look at this? It seems fairly important to fix this.

@jiixyj
Copy link
Contributor Author

jiixyj commented Oct 10, 2023

I created a PR for a possible fix: #68733

It solves my problem at least. I also added a test (more or less the reproducer from above), although this is really hard to test for properly. Sanitizers (asan, ubsan) couldn't catch this in my (admittedly limited) testing. Even valgrind didn't catch this.

jiixyj added a commit to jiixyj/llvm-project that referenced this issue Oct 11, 2023
The calls to `std::construct_at` might overwrite the previously set
`__has_value_` flag, so reverse the order everywhere. Where possible,
avoid calling `std::construct_at` and construct the value/error
directly into the union.
@jiixyj
Copy link
Contributor Author

jiixyj commented Oct 11, 2023

libc++ is making several subobjects potentially-overlapping by _LIBCPP_NO_UNIQUE_ADDRESS, so the std::construct_at call doesn't transparently replaces __union_.__val_ and thus reuses the storage of the whole expected ([basic.life]/8).

A safe resolution may be just removing _LIBCPP_NO_UNIQUE_ADDRESS...

I wonder if this note then applies for libc++'s std::expected implementation:

[Note 5: If these conditions are not met, a pointer to the new object can be obtained from a pointer that represents the address of its storage by calling std​::​launder ([ptr.launder]).
— end note]

Does this mean that there needs to be std::launder sprinkled over the current std::expected implementation?

@huixie90
Copy link
Contributor

@huixie90 Can you take a look at this? It seems fairly important to fix this.

sure I apologies for the delay and I am reviewing the PR that @jiixyj created

@ldionne
Copy link
Member

ldionne commented Oct 17, 2023

In case some folks are watching this but not the PR (or in case someone sees this a long time from now and wants to see more details), there's been a lot of interesting discussion on the PR thread: #68733

jiixyj added a commit to jiixyj/llvm-project that referenced this issue Oct 30, 2023
The calls to `std::construct_at` might overwrite the previously set
`__has_value_` flag, so reverse the order everywhere. Where possible,
avoid calling `std::construct_at` and construct the value/error
directly into the union.
ldionne pushed a commit that referenced this issue Oct 30, 2023
…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
ldionne pushed a commit to ldionne/llvm-project that referenced this issue Oct 30, 2023
llvm#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 llvm#68552

(cherry picked from commit 134c915)
tru pushed a commit that referenced this issue Nov 14, 2023
…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)
@ldionne ldionne added the ABI Application Binary Interface label Nov 30, 2023
ldionne added a commit to ldionne/llvm-project that referenced this issue Jan 8, 2024
This patch adds a configuration of the libc++ test suite that enables
optimizations when building the tests. It also adds a new CI configuration
to exercise this on a regular basis. This is added in the context of [1],
which requires building with optimizations in order to hit the bug.

[1]: llvm#68552
ldionne added a commit that referenced this issue Jan 9, 2024
This patch adds a configuration of the libc++ test suite that enables
optimizations when building the tests. It also adds a new CI
configuration to exercise this on a regular basis. This is added in the
context of [1], which requires building with optimizations in order to
hit the bug.

[1]: #68552
ldionne added a commit that referenced this issue Jan 22, 2024
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>
justinfargnoli pushed a commit to justinfargnoli/llvm-project that referenced this issue Jan 28, 2024
This patch adds a configuration of the libc++ test suite that enables
optimizations when building the tests. It also adds a new CI
configuration to exercise this on a regular basis. This is added in the
context of [1], which requires building with optimizations in order to
hit the bug.

[1]: llvm#68552
blueboxd pushed a commit to blueboxd/libcxx that referenced this issue Feb 1, 2024
This patch adds a configuration of the libc++ test suite that enables
optimizations when building the tests. It also adds a new CI
configuration to exercise this on a regular basis. This is added in the
context of [1], which requires building with optimizations in order to
hit the bug.

[1]: llvm/llvm-project#68552

NOKEYCHECK=True
GitOrigin-RevId: ca06c330fd07f05e65a638892c32ca1474d47b5e
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ABI Application Binary Interface libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants