Skip to content

Commit 1b0d4ac

Browse files
committed
fix UB revealed by gcc release build
1 parent 7923eac commit 1b0d4ac

1 file changed

Lines changed: 162 additions & 51 deletions

File tree

src/map/TinyRoomIdSet.h

Lines changed: 162 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,185 @@ struct NODISCARD TinySet
1818
{
1919
private:
2020
using Type = Type_;
21-
using Set = Set_;
22-
using PtrToSet = std::unique_ptr<Set>;
23-
24-
// REVISIT: if the type can spare a bit, then they can be equal.
25-
/*
26-
static_assert(sizeof(Type) < sizeof(uintptr_t));
27-
static_assert(sizeof(uintptr_t) >= sizeof(PtrToSet));
28-
*/
21+
using BigSet = Set_; // big as opposed to tiny
22+
using UniqueBigSet = std::unique_ptr<BigSet>;
2923

3024
private:
31-
uintptr_t m_storage = 0;
25+
// This Variant class is basically just a "compiler approved" union of Type and UniqueBig,
26+
// where the least significant bit decides which type it is.
27+
//
28+
// Note that Storage does not control the lifetime of the pointed-to-set,
29+
// but TinySet does control the lifetime of that object.
30+
struct NODISCARD Variant final
31+
{
32+
private:
33+
static inline constexpr size_t BUF_SIZE = sizeof(uintptr_t);
34+
static inline constexpr size_t BUF_ALIGN = alignof(uintptr_t);
35+
static_assert(BUF_SIZE == sizeof(UniqueBigSet));
36+
static_assert(BUF_SIZE >= sizeof(size_t));
37+
static_assert(sizeof(size_t) >= sizeof(Type)); /* note: must be at least one bit larger */
38+
alignas(BUF_ALIGN) char m_buf[BUF_SIZE]{0};
39+
40+
public:
41+
NODISCARD bool operator==(const Variant &other) const noexcept
42+
{
43+
return std::memcmp(m_buf, other.m_buf, BUF_SIZE) == 0;
44+
}
45+
46+
private:
47+
// holds_xxx() functions are based on this.
48+
//
49+
// Note: The copy is made to avoid the following dreaded sequence of events:
50+
// 1) auto *ptr = std::launder(reinterpret_cast<UniqueBigSet*>(buf));
51+
// 2) auto tmp = *std::launder(reinterpret_cast<uintptr_t*>(buf)); // invalidates ptr
52+
// 3) use(ptr); // UB
53+
//
54+
// The reason this is UB is because of strict pointer aliasing rule that essentially
55+
// allows the compiler to assume there can only be only one laundered "object"
56+
// (or primitive type) at a given address, with the notable exception that it's also
57+
// legal to have a pointer to a type that provides storage (e.g. char).
58+
//
59+
// So this means if the compiler can detect the start of the lifetime of a new object
60+
// at the same address, then the old object's lifetime has ended. This means we cannot
61+
// simultaneously maintain a pointer in the storage -and- also examine it as a uintptr_t.
62+
// (Hopefully this rule will be relaxed for primitive types in a future c++ standard.)
63+
//
64+
// Another option would be to detect if the platform is big or little endian,
65+
// and then just read the buffer's high or low byte to know the type.
66+
// (I'm too lazy to check the assembly to see if either option is actually better,
67+
// but I assume that the compiler will figure out both, so I'd expect both
68+
// to yield the same assembly code.)
69+
NODISCARD uintptr_t get_uintptrt() const noexcept
70+
{
71+
const Variant copy = *this; // compiler performs a memcpy
72+
return *std::launder(reinterpret_cast<const uintptr_t *>(copy.m_buf));
73+
}
74+
75+
public:
76+
// empty is effectively just holding "(UniqueBig*)nullptr"
77+
NODISCARD bool holds_nothing() const noexcept { return get_uintptrt() == 0u; }
78+
NODISCARD bool holds_single_value() const noexcept { return (get_uintptrt() & 1u) == 1u; }
79+
NODISCARD bool holds_unique_bigset() const noexcept
80+
{
81+
return !holds_nothing() && !holds_single_value();
82+
}
83+
84+
public:
85+
void clear() noexcept
86+
{
87+
if (holds_unique_bigset()) {
88+
assign_unique_bigset(nullptr);
89+
} else {
90+
// would it be okay to write "*this = {};" here?
91+
*std::launder(reinterpret_cast<uintptr_t *>(m_buf)) = 0u;
92+
}
93+
assert(holds_nothing());
94+
}
95+
96+
public:
97+
NODISCARD Type get_single_value() const noexcept
98+
{
99+
// copy is unnecessary but should be optimized away;
100+
// it's only here as a defense mechanism.
101+
assert(holds_single_value());
102+
const Variant copy = *this;
103+
const auto ui = *std::launder(reinterpret_cast<const uintptr_t *>(copy.m_buf)) >> 1u;
104+
const auto size = static_cast<size_t>(ui);
105+
return IdHelper_::fromBits(size);
106+
}
107+
void set_single_value(const Type value) noexcept
108+
{
109+
if (holds_unique_bigset()) {
110+
assert(!holds_unique_bigset());
111+
std::abort(); // crash
112+
}
113+
114+
constexpr size_t MASK = ~size_t{0} >> 1u;
115+
const auto raw_val = static_cast<size_t>(IdHelper_::getBits(value));
116+
const auto masked_val = static_cast<uintptr_t>(raw_val & MASK);
117+
assert(raw_val == masked_val);
118+
const auto ui = (masked_val << 1u) | 1u;
119+
assert(holds_nothing() || holds_single_value());
120+
*std::launder(reinterpret_cast<uintptr_t *>(m_buf)) = ui;
121+
assert(holds_single_value());
122+
assert(get_single_value() == value);
123+
}
124+
125+
private:
126+
NODISCARD UniqueBigSet &getUniqueBigSet() noexcept
127+
{
128+
assert(holds_nothing() || holds_unique_bigset()); // used in assignment
129+
return *std::launder(reinterpret_cast<UniqueBigSet *>(m_buf));
130+
}
131+
NODISCARD const UniqueBigSet &getUniqueBigSet() const noexcept
132+
{
133+
assert(holds_unique_bigset());
134+
return *std::launder(reinterpret_cast<const UniqueBigSet *>(m_buf));
135+
}
136+
137+
public:
138+
NODISCARD BigSet &get_big() noexcept
139+
{
140+
assert(holds_unique_bigset());
141+
return *getUniqueBigSet();
142+
}
143+
NODISCARD const BigSet &get_big() const noexcept
144+
{
145+
assert(holds_unique_bigset());
146+
return *getUniqueBigSet();
147+
}
148+
// newValue is allowed to be nullptr
149+
void assign_unique_bigset(UniqueBigSet newValue) noexcept
150+
{
151+
assert(holds_nothing() || holds_unique_bigset());
152+
UniqueBigSet &ref = getUniqueBigSet();
153+
using std::swap;
154+
swap(ref, newValue);
155+
assert(holds_nothing() || holds_unique_bigset());
156+
}
157+
};
158+
Variant m_var;
32159

33160
private:
34-
NODISCARD bool isEmpty() const noexcept { return m_storage == 0u; }
35-
NODISCARD bool hasOne() const noexcept { return (m_storage & 0x1u) == 0x1u; }
36-
NODISCARD bool hasBig() const noexcept { return !isEmpty() && (m_storage & 0x1u) == 0x0u; }
161+
NODISCARD bool isEmpty() const noexcept { return m_var.holds_nothing(); }
162+
NODISCARD bool hasOne() const noexcept { return m_var.holds_single_value(); }
163+
NODISCARD bool hasBig() const noexcept { return m_var.holds_unique_bigset(); }
37164

38165
private:
39-
NODISCARD Set &getBig()
166+
NODISCARD BigSet &getBig()
40167
{
41168
if (!hasBig()) {
42169
throw std::runtime_error("bad type");
43170
}
44-
return **std::launder(reinterpret_cast<PtrToSet *>(&m_storage));
171+
return m_var.get_big();
45172
}
46-
NODISCARD const Set &getBig() const
173+
NODISCARD const BigSet &getBig() const
47174
{
48175
if (!hasBig()) {
49176
throw std::runtime_error("bad type");
50177
}
51-
return **std::launder(reinterpret_cast<const PtrToSet *>(&m_storage));
178+
return m_var.get_big();
52179
}
53180
NODISCARD Type getOnly() const
54181
{
55182
if (!hasOne()) {
56183
throw std::runtime_error("bad type");
57184
}
58185

59-
// NOTE: Only allows 31-bit roomids on 32-bit platforms.
60-
return IdHelper_::fromBits(static_cast<size_t>(m_storage) >> 1u);
186+
return m_var.get_single_value();
61187
}
62188

63189
private:
64-
void assign(PtrToSet newValue)
190+
void assign(UniqueBigSet newValue)
65191
{
66192
if (!hasBig()) {
67-
m_storage = 0;
193+
m_var.clear();
68194
}
69195

70-
const bool wasNullptr = newValue == nullptr;
71-
PtrToSet *const ptr = std::launder(reinterpret_cast<PtrToSet *>(&m_storage));
72-
using std::swap;
73-
swap(*ptr, newValue);
196+
const bool should_expect_empty = newValue == nullptr;
197+
m_var.assign_unique_bigset(std::exchange(newValue, {}));
74198

75-
if (wasNullptr) {
199+
if (should_expect_empty) {
76200
assert(isEmpty());
77201
} else {
78202
assert(hasBig());
@@ -87,17 +211,13 @@ struct NODISCARD TinySet
87211
}
88212

89213
// NOTE: Only allows 31-bit roomids on 32-bit platforms.
90-
m_storage = (IdHelper_::getBits(value) << 1u) | 1u;
214+
m_var.set_single_value(value);
91215
assert(hasOne());
92216
}
93217

94218
void clear() noexcept
95219
{
96-
if (hasBig()) {
97-
assign(nullptr);
98-
} else {
99-
m_storage = 0;
100-
}
220+
m_var.clear();
101221
assert(isEmpty());
102222
}
103223

@@ -111,22 +231,22 @@ struct NODISCARD TinySet
111231
}
112232

113233
~TinySet() { clear(); }
114-
TinySet(TinySet &&other) noexcept { std::swap(m_storage, other.m_storage); }
234+
TinySet(TinySet &&other) noexcept { std::swap(m_var, other.m_var); }
115235

116236
TinySet(const TinySet &other)
117237
{
118238
if (other.hasBig()) {
119-
assign(std::make_unique<Set>(other.getBig()));
239+
assign(std::make_unique<BigSet>(other.getBig()));
120240
} else {
121241
clear();
122-
m_storage = other.m_storage;
242+
m_var = other.m_var;
123243
}
124244
}
125245

126246
TinySet &operator=(TinySet &&other) noexcept
127247
{
128248
if (std::addressof(other) != this) {
129-
std::swap(m_storage, other.m_storage);
249+
std::swap(m_var, other.m_var);
130250
}
131251
return *this;
132252
}
@@ -139,22 +259,13 @@ struct NODISCARD TinySet
139259
return *this;
140260
}
141261

142-
explicit TinySet(Set &&other)
143-
{
144-
if (other.empty()) {
145-
/* nop */
146-
} else if (other.size() == 1) {
147-
*this = TinySet(other.first());
148-
} else {
149-
set(std::make_unique<Set>(std::move(other)));
150-
}
151-
}
262+
explicit TinySet(BigSet &&other) = delete;
152263

153264
public:
154265
struct NODISCARD ConstIterator final
155266
{
156267
private:
157-
using SetConstIt = typename Set::ConstIterator;
268+
using SetConstIt = typename BigSet::ConstIterator;
158269
SetConstIt m_setIt{};
159270
Type m_val{};
160271
enum class NODISCARD StateEnum : uint8_t { Empty, One, Big };
@@ -273,7 +384,7 @@ struct NODISCARD TinySet
273384

274385
NODISCARD bool operator==(const TinySet &rhs) const
275386
{
276-
if (m_storage == rhs.m_storage) {
387+
if (m_var == rhs.m_var) {
277388
return true;
278389
}
279390

@@ -342,14 +453,14 @@ struct NODISCARD TinySet
342453
return;
343454
}
344455

345-
// convert to Big
346-
PtrToSet ptrToBig = std::make_unique<Set>();
347-
auto &big = *ptrToBig;
456+
// convert to BigSet
457+
UniqueBigSet uniqueBigSet = std::make_unique<BigSet>();
458+
auto &big = *uniqueBigSet;
348459
big.insert(getOnly());
349460
big.insert(id);
350461

351462
assert(hasOne());
352-
assign(std::exchange(ptrToBig, {})); // conversion happens here
463+
assign(std::exchange(uniqueBigSet, {})); // conversion to BigSet happens here
353464
assert(hasBig());
354465
}
355466

0 commit comments

Comments
 (0)