Skip to content

Commit 9f3d4d7

Browse files
goldvitalycopybara-github
authored andcommitted
Add a verification for access of being destroyed table. Also enabled access after destroy check in ASAN optimized mode.
ASAN optimized mode is often used as canary binary and such errors are often happen due to data races that are hard to detect on unit tests. PiperOrigin-RevId: 729037054 Change-Id: I08728b7ce01608c14e5eeb63e2fbcc7a3e4d4a43
1 parent 7971b4a commit 9f3d4d7

File tree

2 files changed

+88
-13
lines changed

2 files changed

+88
-13
lines changed

absl/container/internal/raw_hash_set.h

+35-9
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@ constexpr bool SwisstableGenerationsEnabled() { return false; }
280280
constexpr size_t NumGenerationBytes() { return 0; }
281281
#endif
282282

283+
// Returns true if we should assert that the table is not accessed after it has
284+
// been destroyed or during the destruction of the table.
285+
constexpr bool SwisstableAssertAccessToDestroyedTable() {
286+
#ifndef NDEBUG
287+
return true;
288+
#endif
289+
return SwisstableGenerationsEnabled();
290+
}
291+
283292
template <typename AllocType>
284293
void SwapAlloc(AllocType& lhs, AllocType& rhs,
285294
std::true_type /* propagate_on_container_swap */) {
@@ -1358,6 +1367,13 @@ class CommonFields : public CommonFieldsGenerationInfo {
13581367
CommonFields(const CommonFields&) = delete;
13591368
CommonFields& operator=(const CommonFields&) = delete;
13601369

1370+
// Copy with guarantee that it is not SOO.
1371+
CommonFields(non_soo_tag_t, const CommonFields& that)
1372+
: capacity_(that.capacity_),
1373+
size_(that.size_),
1374+
heap_or_soo_(that.heap_or_soo_) {
1375+
}
1376+
13611377
// Movable
13621378
CommonFields(CommonFields&& that) = default;
13631379
CommonFields& operator=(CommonFields&&) = default;
@@ -2807,9 +2823,9 @@ class raw_hash_set {
28072823

28082824
~raw_hash_set() {
28092825
destructor_impl();
2810-
#ifndef NDEBUG
2811-
common().set_capacity(InvalidCapacity::kDestroyed);
2812-
#endif
2826+
if constexpr (SwisstableAssertAccessToDestroyedTable()) {
2827+
common().set_capacity(InvalidCapacity::kDestroyed);
2828+
}
28132829
}
28142830

28152831
iterator begin() ABSL_ATTRIBUTE_LIFETIME_BOUND {
@@ -3540,10 +3556,17 @@ class raw_hash_set {
35403556
void destroy_slots() {
35413557
ABSL_SWISSTABLE_ASSERT(!is_soo());
35423558
if (PolicyTraits::template destroy_is_trivial<Alloc>()) return;
3543-
IterateOverFullSlots(common(), sizeof(slot_type),
3544-
[&](const ctrl_t*, void* slot) {
3545-
this->destroy(static_cast<slot_type*>(slot));
3546-
});
3559+
auto destroy_slot = [&](const ctrl_t*, void* slot) {
3560+
this->destroy(static_cast<slot_type*>(slot));
3561+
};
3562+
if constexpr (SwisstableAssertAccessToDestroyedTable()) {
3563+
CommonFields common_copy(non_soo_tag_t{}, this->common());
3564+
common().set_capacity(InvalidCapacity::kDestroyed);
3565+
IterateOverFullSlots(common_copy, sizeof(slot_type), destroy_slot);
3566+
common().set_capacity(common_copy.capacity());
3567+
} else {
3568+
IterateOverFullSlots(common(), sizeof(slot_type), destroy_slot);
3569+
}
35473570
}
35483571

35493572
void dealloc() {
@@ -3781,8 +3804,11 @@ class raw_hash_set {
37813804
assert(capacity() != InvalidCapacity::kReentrance &&
37823805
"Reentrant container access during element construction/destruction "
37833806
"is not allowed.");
3784-
assert(capacity() != InvalidCapacity::kDestroyed &&
3785-
"Use of destroyed hash table.");
3807+
if constexpr (SwisstableAssertAccessToDestroyedTable()) {
3808+
if (capacity() == InvalidCapacity::kDestroyed) {
3809+
ABSL_RAW_LOG(FATAL, "Use of destroyed hash table.");
3810+
}
3811+
}
37863812
if (SwisstableGenerationsEnabled() &&
37873813
ABSL_PREDICT_FALSE(capacity() >= InvalidCapacity::kMovedFrom)) {
37883814
if (capacity() == InvalidCapacity::kSelfMovedFrom) {

absl/container/internal/raw_hash_set_test.cc

+53-4
Original file line numberDiff line numberDiff line change
@@ -3890,19 +3890,68 @@ TEST(Table, ReentrantCallsFail) {
38903890
#endif
38913891
}
38923892

3893+
// TODO(b/328794765): this check is very useful to run with ASAN in opt mode.
38933894
TEST(Table, DestroyedCallsFail) {
38943895
#ifdef NDEBUG
3895-
GTEST_SKIP() << "Destroyed checks only enabled in debug mode.";
3896-
#elif !defined(__clang__) && defined(__GNUC__)
3897-
GTEST_SKIP() << "Flaky on GCC.";
3896+
ASSERT_EQ(SwisstableAssertAccessToDestroyedTable(),
3897+
SwisstableGenerationsEnabled());
38983898
#else
3899+
ASSERT_TRUE(SwisstableAssertAccessToDestroyedTable());
3900+
#endif
3901+
if (!SwisstableAssertAccessToDestroyedTable()) {
3902+
GTEST_SKIP() << "Validation not enabled.";
3903+
}
3904+
#if !defined(__clang__) && defined(__GNUC__)
3905+
GTEST_SKIP() << "Flaky on GCC.";
3906+
#endif
38993907
absl::optional<IntTable> t;
39003908
t.emplace({1});
39013909
IntTable* t_ptr = &*t;
39023910
EXPECT_TRUE(t_ptr->contains(1));
39033911
t.reset();
3904-
EXPECT_DEATH_IF_SUPPORTED(t_ptr->contains(1), "");
3912+
std::string expected_death_message =
3913+
#if defined(ABSL_HAVE_MEMORY_SANITIZER)
3914+
"use-of-uninitialized-value";
3915+
#else
3916+
"destroyed hash table";
3917+
#endif
3918+
EXPECT_DEATH_IF_SUPPORTED(t_ptr->contains(1), expected_death_message);
3919+
}
3920+
3921+
TEST(Table, DestroyedCallsFailDuringDestruction) {
3922+
if (!SwisstableAssertAccessToDestroyedTable()) {
3923+
GTEST_SKIP() << "Validation not enabled.";
3924+
}
3925+
#if !defined(__clang__) && defined(__GNUC__)
3926+
GTEST_SKIP() << "Flaky on GCC.";
3927+
#endif
3928+
// When EXPECT_DEATH_IF_SUPPORTED is not executed, the code after it is not
3929+
// executed as well.
3930+
// We need to destruct the table correctly in such a case.
3931+
// Must be defined before the table for correct destruction order.
3932+
bool do_lookup = false;
3933+
3934+
using Table = absl::flat_hash_map<int, std::shared_ptr<int>>;
3935+
absl::optional<Table> t = Table();
3936+
Table* t_ptr = &*t;
3937+
auto destroy = [&](int* ptr) {
3938+
if (do_lookup) {
3939+
ASSERT_TRUE(t_ptr->contains(*ptr));
3940+
}
3941+
delete ptr;
3942+
};
3943+
t->insert({0, std::shared_ptr<int>(new int(0), destroy)});
3944+
auto destroy_with_lookup = [&] {
3945+
do_lookup = true;
3946+
t.reset();
3947+
};
3948+
std::string expected_death_message =
3949+
#ifdef NDEBUG
3950+
"destroyed hash table";
3951+
#else
3952+
"Reentrant container access";
39053953
#endif
3954+
EXPECT_DEATH_IF_SUPPORTED(destroy_with_lookup(), expected_death_message);
39063955
}
39073956

39083957
TEST(Table, MovedFromCallsFail) {

0 commit comments

Comments
 (0)