pw_containers: Intrusive list item auto-removal
Previously, it was easy to accidentally have dangling use-after-free
errors with intrusive list items. This patch changes the logic so that
items remove themselves from their parent list during destruction.
Additionally, this changes IntrusiveList<T>::Item's semantics so that
instead of a nullptr to indicating an unlisted element, a self-cycle
indicates unlisted. This leads to more elegant logic.
Change-Id: I5de663f7c67112b4b9465373d64c0d57b05bd892
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/16260
Reviewed-by: Armando Montanez <amontanez@google.com>
Commit-Queue: Keir Mierle <keir@google.com>
diff --git a/pw_containers/BUILD.gn b/pw_containers/BUILD.gn
index b637bd8..0b7e768 100644
--- a/pw_containers/BUILD.gn
+++ b/pw_containers/BUILD.gn
@@ -40,7 +40,7 @@
"public/pw_containers/internal/intrusive_list_impl.h",
"public/pw_containers/intrusive_list.h",
]
- deps = [ "$dir_pw_assert" ]
+ deps = [ dir_pw_assert ]
sources = [ "intrusive_list.cc" ]
}
diff --git a/pw_containers/intrusive_list.cc b/pw_containers/intrusive_list.cc
index 0ddfbe8..49ecdf1 100644
--- a/pw_containers/intrusive_list.cc
+++ b/pw_containers/intrusive_list.cc
@@ -18,28 +18,39 @@
namespace pw::intrusive_list_impl {
+List::Item::~Item() { unlist(); }
+
+void List::Item::unlist(Item* prev) {
+ if (prev == nullptr) {
+ prev = previous();
+ }
+ // Skip over this.
+ prev->next_ = next_;
+
+ // Retain the invariant that unlisted items are self-cycles.
+ next_ = this;
+}
+
+List::Item* List::Item::previous() {
+ // Follow the cycle around to find the previous element; O(N).
+ Item* prev = next_;
+ while (prev->next_ != this) {
+ prev = prev->next_;
+ }
+ return prev;
+}
+
void List::insert_after(Item* pos, Item& item) {
- PW_CHECK_PTR_EQ(
- item.next_,
- nullptr,
+ PW_CHECK(
+ item.unlisted(),
"Cannot add an item to a pw::IntrusiveList that is already in a list");
item.next_ = pos->next_;
pos->next_ = &item;
}
-void List::erase_after(Item* pos) {
- Item* const item_to_remove = pos->next_;
- pos->next_ = item_to_remove->next_;
- item_to_remove->next_ = nullptr;
-}
+void List::erase_after(Item* pos) { pos->next_->unlist(pos); }
-List::Item* List::before_end() noexcept {
- Item* pos = before_begin();
- while (pos->next_ != end()) {
- pos = pos->next_;
- }
- return pos;
-}
+List::Item* List::before_end() noexcept { return before_begin()->previous(); }
void List::clear() {
while (!empty()) {
@@ -54,7 +65,6 @@
return true;
}
}
-
return false;
}
diff --git a/pw_containers/intrusive_list_test.cc b/pw_containers/intrusive_list_test.cc
index 3d666e4..7a9b293 100644
--- a/pw_containers/intrusive_list_test.cc
+++ b/pw_containers/intrusive_list_test.cc
@@ -142,24 +142,25 @@
TEST(IntrusiveList, PushOne) {
constexpr int kMagicValue = 31;
- IntrusiveList<TestItem> test_items;
TestItem item1(kMagicValue);
- test_items.push_back(item1);
- EXPECT_FALSE(test_items.empty());
- EXPECT_EQ(test_items.front().GetNumber(), kMagicValue);
+ IntrusiveList<TestItem> list;
+ list.push_back(item1);
+ EXPECT_FALSE(list.empty());
+ EXPECT_EQ(list.front().GetNumber(), kMagicValue);
}
TEST(IntrusiveList, PushThree) {
- IntrusiveList<TestItem> test_items;
TestItem item1(1);
TestItem item2(2);
TestItem item3(3);
- test_items.push_back(item1);
- test_items.push_back(item2);
- test_items.push_back(item3);
+
+ IntrusiveList<TestItem> list;
+ list.push_back(item1);
+ list.push_back(item2);
+ list.push_back(item3);
int loop_count = 0;
- for (auto& test_item : test_items) {
+ for (auto& test_item : list) {
loop_count++;
EXPECT_EQ(loop_count, test_item.GetNumber());
}
@@ -167,35 +168,38 @@
}
TEST(IntrusiveList, IsEmpty) {
- IntrusiveList<TestItem> test_items;
- EXPECT_TRUE(test_items.empty());
-
TestItem item1(1);
- test_items.push_back(item1);
- EXPECT_FALSE(test_items.empty());
+
+ IntrusiveList<TestItem> list;
+ EXPECT_TRUE(list.empty());
+
+ list.push_back(item1);
+ EXPECT_FALSE(list.empty());
}
TEST(IntrusiveList, InsertAfter) {
+ // Create a test item to insert midway through the list.
constexpr int kMagicValue = 42;
+ TestItem inserted_item(kMagicValue);
+
+ // Create initial values to fill in the start/end.
TestItem item_array[20];
- IntrusiveList<TestItem> test_list;
+
+ IntrusiveList<TestItem> list;
// Fill the list with TestItem objects that have a value of zero.
for (size_t i = 0; i < PW_ARRAY_SIZE(item_array); ++i) {
item_array[i].SetNumber(0);
- test_list.push_back(item_array[i]);
+ list.push_back(item_array[i]);
}
- // Create a test item to insert midway through the list.
- TestItem inserted_item(kMagicValue);
-
- // Move an iterator to the middle of the list, and then insert the item.
- auto it = test_list.begin();
+ // Move an iterator to the middle of the list, and then insert the magic item.
+ auto it = list.begin();
size_t expected_index = 1; // Expected index is iterator index + 1.
for (size_t i = 0; i < PW_ARRAY_SIZE(item_array) / 2; ++i) {
it++;
expected_index++;
}
- it = test_list.insert_after(it, inserted_item);
+ it = list.insert_after(it, inserted_item);
// Ensure the returned iterator from insert_after is the newly inserted
// element.
@@ -203,7 +207,7 @@
// Ensure the value is in the expected location (index of the iterator + 1).
size_t i = 0;
- for (TestItem& item : test_list) {
+ for (TestItem& item : list) {
if (item.GetNumber() == kMagicValue) {
EXPECT_EQ(i, expected_index);
} else {
@@ -218,68 +222,69 @@
TEST(IntrusiveList, PushFront) {
constexpr int kMagicValue = 42;
+ TestItem pushed_item(kMagicValue);
+
TestItem item_array[20];
- IntrusiveList<TestItem> test_list;
+ IntrusiveList<TestItem> list;
// Fill the list with TestItem objects that have a value of zero.
for (size_t i = 0; i < PW_ARRAY_SIZE(item_array); ++i) {
item_array[i].SetNumber(0);
- test_list.push_back(item_array[i]);
+ list.push_back(item_array[i]);
}
// Create a test item to push to the front of the list.
- TestItem pushed_item(kMagicValue);
- test_list.push_front(pushed_item);
- EXPECT_EQ(test_list.front().GetNumber(), kMagicValue);
+ list.push_front(pushed_item);
+ EXPECT_EQ(list.front().GetNumber(), kMagicValue);
}
TEST(IntrusiveList, Clear_Empty) {
- IntrusiveList<TestItem> test_list;
- EXPECT_TRUE(test_list.empty());
- test_list.clear();
- EXPECT_TRUE(test_list.empty());
+ IntrusiveList<TestItem> list;
+ EXPECT_TRUE(list.empty());
+ list.clear();
+ EXPECT_TRUE(list.empty());
}
TEST(IntrusiveList, Clear_OneItem) {
- IntrusiveList<TestItem> test_list;
TestItem item(42);
- test_list.push_back(item);
- EXPECT_FALSE(test_list.empty());
- test_list.clear();
- EXPECT_TRUE(test_list.empty());
+ IntrusiveList<TestItem> list;
+ list.push_back(item);
+ EXPECT_FALSE(list.empty());
+ list.clear();
+ EXPECT_TRUE(list.empty());
}
TEST(IntrusiveList, Clear_TwoItems) {
- IntrusiveList<TestItem> test_list;
TestItem item1(42);
TestItem item2(42);
- test_list.push_back(item1);
- test_list.push_back(item2);
- EXPECT_FALSE(test_list.empty());
- test_list.clear();
- EXPECT_TRUE(test_list.empty());
+ IntrusiveList<TestItem> list;
+ list.push_back(item1);
+ list.push_back(item2);
+ EXPECT_FALSE(list.empty());
+ list.clear();
+ EXPECT_TRUE(list.empty());
}
TEST(IntrusiveList, Clear_ReinsertClearedItems) {
std::array<TestItem, 20> item_array;
- IntrusiveList<TestItem> test_list;
- EXPECT_TRUE(test_list.empty());
- test_list.clear();
- EXPECT_TRUE(test_list.empty());
+ IntrusiveList<TestItem> list;
+ EXPECT_TRUE(list.empty());
+ list.clear();
+ EXPECT_TRUE(list.empty());
// Fill the list with TestItem objects.
for (size_t i = 0; i < item_array.size(); ++i) {
item_array[i].SetNumber(0);
- test_list.push_back(item_array[i]);
+ list.push_back(item_array[i]);
}
// Remove everything.
- test_list.clear();
- EXPECT_TRUE(test_list.empty());
+ list.clear();
+ EXPECT_TRUE(list.empty());
// Ensure all the removed elements can still be added back to a list.
for (size_t i = 0; i < item_array.size(); ++i) {
item_array[i].SetNumber(0);
- test_list.push_back(item_array[i]);
+ list.push_back(item_array[i]);
}
}
@@ -287,64 +292,63 @@
constexpr int kValue1 = 32;
constexpr int kValue2 = 4083;
- IntrusiveList<TestItem> test_list;
- EXPECT_TRUE(test_list.empty());
-
TestItem item1(kValue1);
TestItem item2(kValue2);
- test_list.push_front(item2);
- test_list.push_front(item1);
- test_list.pop_front();
- EXPECT_EQ(test_list.front().GetNumber(), kValue2);
- EXPECT_FALSE(test_list.empty());
- test_list.pop_front();
- EXPECT_TRUE(test_list.empty());
+ IntrusiveList<TestItem> list;
+ EXPECT_TRUE(list.empty());
+
+ list.push_front(item2);
+ list.push_front(item1);
+ list.pop_front();
+ EXPECT_EQ(list.front().GetNumber(), kValue2);
+ EXPECT_FALSE(list.empty());
+ list.pop_front();
+ EXPECT_TRUE(list.empty());
}
TEST(IntrusiveList, PopFrontAndReinsert) {
constexpr int kValue1 = 32;
constexpr int kValue2 = 4083;
- IntrusiveList<TestItem> test_list;
- EXPECT_TRUE(test_list.empty());
-
TestItem item1(kValue1);
TestItem item2(kValue2);
- test_list.push_front(item2);
- test_list.push_front(item1);
- test_list.pop_front();
- test_list.push_front(item1);
- EXPECT_EQ(test_list.front().GetNumber(), kValue1);
+ IntrusiveList<TestItem> list;
+ EXPECT_TRUE(list.empty());
+
+ list.push_front(item2);
+ list.push_front(item1);
+ list.pop_front();
+ list.push_front(item1);
+ EXPECT_EQ(list.front().GetNumber(), kValue1);
}
TEST(IntrusiveList, ListFront) {
- IntrusiveList<TestItem> test_items;
-
TestItem item1(1);
TestItem item2(0);
TestItem item3(0xffff);
- test_items.push_back(item1);
- test_items.push_back(item2);
- test_items.push_back(item3);
+ IntrusiveList<TestItem> list;
+ list.push_back(item1);
+ list.push_back(item2);
+ list.push_back(item3);
- EXPECT_EQ(&item1, &test_items.front());
- EXPECT_EQ(&item1, &(*test_items.begin()));
+ EXPECT_EQ(&item1, &list.front());
+ EXPECT_EQ(&item1, &(*list.begin()));
}
TEST(IntrusiveList, IteratorIncrement) {
TestItem item_array[20];
- IntrusiveList<TestItem> test_list;
+ IntrusiveList<TestItem> list;
for (size_t i = 0; i < PW_ARRAY_SIZE(item_array); ++i) {
item_array[i].SetNumber(i);
- test_list.push_back(item_array[i]);
+ list.push_back(item_array[i]);
}
- auto it = test_list.begin();
+ auto it = list.begin();
int i = 0;
- while (it != test_list.end()) {
+ while (it != list.end()) {
if (i == 0) {
// Test pre-incrementing on the first element.
EXPECT_EQ((++it)->GetNumber(), item_array[++i].GetNumber());
@@ -358,12 +362,12 @@
// For this test, items are checked to be non-zero.
TestItem item1(1);
TestItem item2(99);
- IntrusiveList<TestItem> test_items;
+ IntrusiveList<TestItem> list;
- const IntrusiveList<TestItem>* const_list = &test_items;
+ const IntrusiveList<TestItem>* const_list = &list;
- test_items.push_back(item1);
- test_items.push_back(item2);
+ list.push_back(item1);
+ list.push_back(item2);
auto it = const_list->begin();
while (it != const_list->end()) {
@@ -378,12 +382,12 @@
TEST(IntrusiveList, ConstIteratorModify) {
TestItem item1(1);
TestItem item2(99);
- IntrusiveList<TestItem> test_items;
+ IntrusiveList<TestItem> list;
- const IntrusiveList<TestItem>* const_list = &test_items;
+ const IntrusiveList<TestItem>* const_list = &list;
- test_items.push_back(item1);
- test_items.push_back(item2);
+ list.push_back(item1);
+ list.push_back(item2);
auto it = const_list->begin();
while (it != const_list->end()) {
@@ -517,5 +521,45 @@
EXPECT_EQ(list.end(), it);
}
+TEST(IntrusiveList, ItemsRemoveThemselvesFromListsWhenDestructed) {
+ // Create a list with some items it.
+ TestItem a, b, c, d;
+ IntrusiveList<TestItem> list;
+ list.push_back(a);
+ list.push_back(b);
+ list.push_back(c);
+ list.push_back(d);
+
+ // Insert items that will be destructed before the list.
+ {
+ TestItem x, y, z, w;
+ list.push_back(x);
+ list.push_back(z);
+ list.push_front(y);
+ list.push_front(w);
+
+ auto it = list.begin();
+ EXPECT_EQ(&w, &(*it++));
+ EXPECT_EQ(&y, &(*it++));
+ EXPECT_EQ(&a, &(*it++));
+ EXPECT_EQ(&b, &(*it++));
+ EXPECT_EQ(&c, &(*it++));
+ EXPECT_EQ(&d, &(*it++));
+ EXPECT_EQ(&x, &(*it++));
+ EXPECT_EQ(&z, &(*it++));
+ EXPECT_EQ(list.end(), it);
+
+ // Here, x, y, z, w are removed from the list for the destructor.
+ }
+
+ // Ensure we get back our original list.
+ auto it = list.begin();
+ EXPECT_EQ(&a, &(*it++));
+ EXPECT_EQ(&b, &(*it++));
+ EXPECT_EQ(&c, &(*it++));
+ EXPECT_EQ(&d, &(*it++));
+ EXPECT_EQ(list.end(), it);
+}
+
} // namespace
} // namespace pw
diff --git a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
index fe7e07d..c2ea07a 100644
--- a/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
+++ b/pw_containers/public/pw_containers/internal/intrusive_list_impl.h
@@ -71,7 +71,17 @@
public:
class Item {
protected:
- constexpr Item() : Item(nullptr) {}
+ constexpr Item() : Item(this) {}
+
+ bool unlisted() const { return this == next_; }
+
+ // Unlink this from the list it is apart of, if any. Specifying prev saves
+ // calling previous(), which requires looping around the cycle.
+ void unlist(Item* prev = nullptr);
+
+ Item* previous(); // Note: O(n) since it loops around the cycle.
+
+ ~Item();
private:
friend class List;
@@ -81,6 +91,7 @@
constexpr Item(Item* next) : next_(next) {}
+ // The next pointer. Unlisted items must be self-cycles (next_ == this).
Item* next_;
};
@@ -95,8 +106,6 @@
List(const List&) = delete;
List& operator=(const List&) = delete;
- ~List() { clear(); }
-
template <typename Iterator>
void assign(Iterator first, Iterator last) {
clear();