Skip to content

Commit 4f46905

Browse files
jiixyjphilnik777ldionne
authored
[libc++] Ensure that std::expected has no tail padding (#69673)
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>
1 parent bf7b8da commit 4f46905

20 files changed

+1201
-401
lines changed

libcxx/docs/ReleaseNotes/18.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,28 @@ ABI Affecting Changes
284284
against different configurations of it being used in different translation
285285
units.
286286

287+
- The amount of padding bytes available for use at the end of certain ``std::expected`` instantiations has changed in this
288+
release. This is an ABI break for any code that held a ``std::expected`` member with ``[[no_unique_address]]`` in an
289+
ABI-facing type. In those cases, the layout of the enclosing type will change, breaking the ABI. However, the
290+
``std::expected<T, E>`` member requires a few characteristics in order to be affected by this change:
291+
292+
- A type equivalent to ``union {T ; E}`` needs to have more than one byte of padding available.
293+
- The ``std::expected<T, E>`` member must have been in a situation where its padding bytes were previously reused by
294+
another object, which can happen in a few cases (this is probably not exhaustive):
295+
296+
- It is a member with ``[[no_unique_address]]`` applied to it, and it is followed by another data member, or
297+
- It is a member with ``[[no_unique_address]]`` applied to it, and it is the last member of the user-defined type,
298+
and that user-defined type is used in ways that its padding bytes can be reused, or
299+
- It is inherited from
300+
301+
We expect that this will not be a very frequent occurrence. However, there is unfortunately no technique we can use
302+
in the library to catch such misuse. Indeed, even applying an ABI tag to ``std::expected`` would not help since ABI
303+
tags are not propagated to containing types. As a result, if you notice very difficult to explain bugs around the
304+
usage of a ``std::expected``, you should consider checking whether you are hitting this ABI break. This change was
305+
done to fix `#70494 <https://github.com/llvm/llvm-project/issues/70494>`_ and the vendor communication is handled
306+
in `#70820 <https://github.com/llvm/llvm-project/issues/70820>`_.
307+
308+
287309
Build System Changes
288310
--------------------
289311

libcxx/include/__expected/expected.h

Lines changed: 750 additions & 390 deletions
Large diffs are not rendered by default.

libcxx/test/libcxx/utilities/expected/expected.expected/no_unique_address.compile.pass.cpp

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212

1313
// test [[no_unique_address]] is applied to the union
1414

15+
#include <__type_traits/datasizeof.h>
1516
#include <expected>
17+
#include <optional>
18+
#include <memory>
1619

1720
struct Empty {};
1821

@@ -23,13 +26,49 @@ struct A {
2326

2427
struct B : public A {
2528
int z_;
29+
short z2_;
2630
virtual ~B() = default;
2731
};
2832

33+
struct BoolWithPadding {
34+
explicit operator bool() { return b; }
35+
36+
private:
37+
alignas(1024) bool b = false;
38+
};
39+
2940
static_assert(sizeof(std::expected<Empty, Empty>) == sizeof(bool));
3041
static_assert(sizeof(std::expected<Empty, A>) == 2 * sizeof(int) + alignof(std::expected<Empty, A>));
31-
static_assert(sizeof(std::expected<Empty, B>) == sizeof(B) + alignof(std::expected<Empty, B>));
42+
static_assert(sizeof(std::expected<Empty, B>) == sizeof(B));
3243
static_assert(sizeof(std::expected<A, Empty>) == 2 * sizeof(int) + alignof(std::expected<A, Empty>));
3344
static_assert(sizeof(std::expected<A, A>) == 2 * sizeof(int) + alignof(std::expected<A, A>));
34-
static_assert(sizeof(std::expected<B, Empty>) == sizeof(B) + alignof(std::expected<B, Empty>));
35-
static_assert(sizeof(std::expected<B, B>) == sizeof(B) + alignof(std::expected<B, B>));
45+
static_assert(sizeof(std::expected<B, Empty>) == sizeof(B));
46+
static_assert(sizeof(std::expected<B, B>) == sizeof(B));
47+
48+
// Check that `expected`'s datasize is large enough for the parameter type(s).
49+
static_assert(sizeof(std::expected<BoolWithPadding, Empty>) ==
50+
std::__libcpp_datasizeof<std::expected<BoolWithPadding, Empty>>::value);
51+
static_assert(sizeof(std::expected<Empty, BoolWithPadding>) ==
52+
std::__libcpp_datasizeof<std::expected<Empty, BoolWithPadding>>::value);
53+
54+
// In this case, there should be tail padding in the `expected` because `A`
55+
// itself does _not_ have tail padding.
56+
static_assert(sizeof(std::expected<A, A>) > std::__libcpp_datasizeof<std::expected<A, A>>::value);
57+
58+
// Test with some real types.
59+
static_assert(sizeof(std::expected<std::optional<int>, int>) == 8);
60+
static_assert(std::__libcpp_datasizeof<std::expected<std::optional<int>, int>>::value == 8);
61+
62+
static_assert(sizeof(std::expected<int, std::optional<int>>) == 8);
63+
static_assert(std::__libcpp_datasizeof<std::expected<int, std::optional<int>>>::value == 8);
64+
65+
static_assert(sizeof(std::expected<int, int>) == 8);
66+
static_assert(std::__libcpp_datasizeof<std::expected<int, int>>::value == 5);
67+
68+
// clang-format off
69+
static_assert(std::__libcpp_datasizeof<int>::value == 4);
70+
static_assert(std::__libcpp_datasizeof<std::expected<int, int>>::value == 5);
71+
static_assert(std::__libcpp_datasizeof<std::expected<std::expected<int, int>, int>>::value == 8);
72+
static_assert(std::__libcpp_datasizeof<std::expected<std::expected<std::expected<int, int>, int>, int>>::value == 9);
73+
static_assert(std::__libcpp_datasizeof<std::expected<std::expected<std::expected<std::expected<int, int>, int>, int>, int>>::value == 12);
74+
// clang-format on

libcxx/test/libcxx/utilities/expected/expected.expected/transform_error.mandates.verify.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@
77
//===----------------------------------------------------------------------===//
88

99
// Clang-18 fixed some spurious clang diagnostics. Once clang-18 is the
10-
// minumum required version these obsolete tests can be removed.
10+
// minimum required version these obsolete tests can be removed.
1111
// TODO(LLVM-20) remove spurious clang diagnostic tests.
1212

1313
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
1414

15+
// With clang-cl, some warnings have a 'which is a Microsoft extension' suffix
16+
// which break the tests.
17+
// XFAIL: msvc
18+
1519
// Test the mandates
1620

1721
// template<class F> constexpr auto transform_error(F&& f) &;
@@ -52,6 +56,7 @@ void test() {
5256
e.transform_error(return_unexpected<int&>); // expected-error-re@*:* {{static assertion failed {{.*}}The result of {{.*}} must be a valid template argument for unexpected}}
5357
// expected-error-re@*:* 0-1 {{{{(excess elements in struct initializer|no matching constructor for initialization of)}}{{.*}}}}
5458
// expected-error-re@*:* {{static assertion failed {{.*}}[expected.object.general] A program that instantiates the definition of template expected<T, E> for {{.*}} is ill-formed.}}
59+
// expected-error-re@*:* {{union member {{.*}} has reference type {{.*}}}}
5560

5661
e.transform_error(return_no_object<int&>); // expected-error-re@*:* {{static assertion failed {{.*}}The result of {{.*}} must be a valid template argument for unexpected}}
5762
// expected-error-re@*:* 0-1 {{{{(excess elements in struct initializer|no matching constructor for initialization of)}}{{.*}}}}

libcxx/test/libcxx/utilities/expected/expected.void/no_unique_address.compile.pass.cpp

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@
1111
// XFAIL: msvc
1212

1313
// test [[no_unique_address]] is applied to the union
14+
// In particular, we ensure that we reuse tail padding in the T
15+
// to store the discriminator whenever we can.
1416

17+
#include <__type_traits/datasizeof.h>
1518
#include <expected>
19+
#include <optional>
20+
#include <memory>
1621

1722
struct Empty {};
1823

@@ -23,9 +28,40 @@ struct A {
2328

2429
struct B : public A {
2530
int z_;
31+
short z2_;
2632
virtual ~B() = default;
2733
};
2834

35+
struct BoolWithPadding {
36+
explicit operator bool() { return b; }
37+
38+
private:
39+
alignas(1024) bool b = false;
40+
};
41+
2942
static_assert(sizeof(std::expected<void, Empty>) == sizeof(bool));
3043
static_assert(sizeof(std::expected<void, A>) == 2 * sizeof(int) + alignof(std::expected<void, A>));
31-
static_assert(sizeof(std::expected<void, B>) == sizeof(B) + alignof(std::expected<void, B>));
44+
static_assert(sizeof(std::expected<void, B>) == sizeof(B));
45+
46+
// Check that `expected`'s datasize is large enough for the parameter type(s).
47+
static_assert(sizeof(std::expected<void, BoolWithPadding>) ==
48+
std::__libcpp_datasizeof<std::expected<void, BoolWithPadding>>::value);
49+
50+
// In this case, there should be tail padding in the `expected` because `A`
51+
// itself does _not_ have tail padding.
52+
static_assert(sizeof(std::expected<void, A>) > std::__libcpp_datasizeof<std::expected<void, A>>::value);
53+
54+
// Test with some real types.
55+
static_assert(sizeof(std::expected<void, std::optional<int>>) == 8);
56+
static_assert(std::__libcpp_datasizeof<std::expected<void, std::optional<int>>>::value == 8);
57+
58+
static_assert(sizeof(std::expected<void, int>) == 8);
59+
static_assert(std::__libcpp_datasizeof<std::expected<void, int>>::value == 5);
60+
61+
// clang-format off
62+
static_assert(std::__libcpp_datasizeof<int>::value == 4);
63+
static_assert(std::__libcpp_datasizeof<std::expected<void, int>>::value == 5);
64+
static_assert(std::__libcpp_datasizeof<std::expected<void, std::expected<void, int>>>::value == 8);
65+
static_assert(std::__libcpp_datasizeof<std::expected<void, std::expected<void, std::expected<void, int>>>>::value == 9);
66+
static_assert(std::__libcpp_datasizeof<std::expected<void, std::expected<void, std::expected<void, std::expected<void, int>>>>>::value == 12);
67+
// clang-format on

libcxx/test/libcxx/utilities/expected/expected.void/transform_error.mandates.verify.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212

1313
// UNSUPPORTED: c++03, c++11, c++14, c++17, c++20
1414

15+
// With clang-cl, some warnings have a 'which is a Microsoft extension' suffix
16+
// which break the tests.
17+
// XFAIL: msvc
18+
1519
// Test the mandates
1620

1721
// template<class F> constexpr auto transform_error(F&& f) &;
@@ -52,6 +56,8 @@ void test() {
5256
e.transform_error(return_unexpected<int&>); // expected-error-re@*:* {{static assertion failed {{.*}}The result of {{.*}} must be a valid template argument for unexpected}}
5357
// expected-error-re@*:* 0-1 {{{{(excess elements in struct initializer|no matching constructor for initialization of)}}{{.*}}}}
5458
// expected-error-re@*:* {{static assertion failed {{.*}}A program that instantiates expected<T, E> with a E that is not a valid argument for unexpected<E> is ill-formed}}
59+
// expected-error-re@*:* {{call to deleted constructor of {{.*}}}}
60+
// expected-error-re@*:* {{union member {{.*}} has reference type {{.*}}}}
5561

5662
e.transform_error(return_no_object<int&>); // expected-error-re@*:* {{static assertion failed {{.*}}The result of {{.*}} must be a valid template argument for unexpected}}
5763
// expected-error-re@*:* 0-1 {{{{(excess elements in struct initializer|no matching constructor for initialization of)}}{{.*}}}}

libcxx/test/std/utilities/expected/expected.expected/assign/assign.U.pass.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,20 @@ constexpr bool test() {
310310
assert(e.value().j == 8);
311311
}
312312

313+
// CheckForInvalidWrites
314+
{
315+
{
316+
CheckForInvalidWrites<true> e1(std::unexpect);
317+
e1 = 42;
318+
assert(e1.check());
319+
}
320+
{
321+
CheckForInvalidWrites<false> e1(std::unexpect);
322+
e1 = true;
323+
assert(e1.check());
324+
}
325+
}
326+
313327
return true;
314328
}
315329

libcxx/test/std/utilities/expected/expected.expected/assign/assign.copy.pass.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,29 @@ constexpr bool test() {
240240
assert(e1.error().data_ == 10);
241241
assert(oldState.copyAssignCalled);
242242
}
243+
244+
// CheckForInvalidWrites
245+
{
246+
{
247+
CheckForInvalidWrites<true> e1(std::unexpect);
248+
CheckForInvalidWrites<true> e2;
249+
250+
e1 = e2;
251+
252+
assert(e1.check());
253+
assert(e2.check());
254+
}
255+
{
256+
CheckForInvalidWrites<false> e1(std::unexpect);
257+
CheckForInvalidWrites<false> e2;
258+
259+
e1 = e2;
260+
261+
assert(e1.check());
262+
assert(e2.check());
263+
}
264+
}
265+
243266
return true;
244267
}
245268

libcxx/test/std/utilities/expected/expected.expected/assign/assign.move.pass.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,29 @@ constexpr bool test() {
258258
assert(e1.error().data_ == 10);
259259
assert(oldState.moveAssignCalled);
260260
}
261+
262+
// CheckForInvalidWrites
263+
{
264+
{
265+
CheckForInvalidWrites<true> e1(std::unexpect);
266+
CheckForInvalidWrites<true> e2;
267+
268+
e1 = std::move(e2);
269+
270+
assert(e1.check());
271+
assert(e2.check());
272+
}
273+
{
274+
CheckForInvalidWrites<false> e1(std::unexpect);
275+
CheckForInvalidWrites<false> e2;
276+
277+
e1 = std::move(e2);
278+
279+
assert(e1.check());
280+
assert(e2.check());
281+
}
282+
}
283+
261284
return true;
262285
}
263286

libcxx/test/std/utilities/expected/expected.expected/assign/emplace.pass.cpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,20 @@ constexpr bool test() {
8080
assert(e.has_value());
8181
}
8282

83+
// CheckForInvalidWrites
84+
{
85+
{
86+
CheckForInvalidWrites<true> e;
87+
e.emplace();
88+
assert(e.check());
89+
}
90+
{
91+
CheckForInvalidWrites<false> e;
92+
e.emplace();
93+
assert(e.check());
94+
}
95+
}
96+
8397
return true;
8498
}
8599

0 commit comments

Comments
 (0)