Skip to content

Commit 687856f

Browse files
rlyerlymeta-codesync[bot]
authored andcommitted
Add AsanSList as an ASAN-compatible drop-in for SList
Summary: We're debugging crashes in a number of services. One culprit we see is the `freedAllocations_` list getting corrupted - it has a non-zero size but null head & tail pointers. Claude pointed to a potential root cause, see P2258999571 for details. Essentially we think something has a dangling Item* and is overwriting the instrusive list hook, truncating the rest of the list. A series of pops is eventually propagating a nullptr into head, leading to a nullptr dereference. This diff provides AsanSList, an API-compatible drop in for SList that can be used in ASAN builds. This is necessary because in order to get ASAN coverage, we need to poison *all* of the item's memory (including the hook pointer) to see who is writing to it. That means we need to externalize the storage and deal with serialization/deserialization. Follow-on diffs will use this in the freedAllocations_ list. Reviewed By: AlnisM Differential Revision: D99208062 fbshipit-source-id: 042e60a14f6ee9274ba4f8610cbcd66eba2e255a
1 parent 68ace55 commit 687856f

1 file changed

Lines changed: 167 additions & 0 deletions

File tree

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#pragma once
18+
19+
#include <sanitizer/asan_interface.h>
20+
21+
#include <vector>
22+
23+
#include "cachelib/allocator/datastruct/SList.h"
24+
25+
namespace facebook::cachelib {
26+
27+
/**
28+
* A non-intrusive drop-in replacement for SList that stores allocation
29+
* pointers in an external vector instead of using intrusive hooks embedded
30+
* in the freed memory. This allows the freed memory to be fully
31+
* ASAN-poisoned, catching any stale writes to allocations on the free list.
32+
*
33+
* When allocSize > 0 and ASAN is enabled:
34+
* - insert() poisons the allocation memory after storing the pointer
35+
* - pop() unpoisons the allocation memory before removing the pointer
36+
* - saveState() temporarily unpoisons to build an in-memory SList that
37+
* persists in shared memory
38+
* - the restore constructor reconstructs the external list from the in-memory
39+
* SList and poisons each allocation
40+
*
41+
* When allocSize == 0 (the default), poisoning is a no-op. This is useful
42+
* for temporary lists (e.g., in pruneFreeAllocs) that shuffle already-
43+
* poisoned allocations between containers without changing their ASAN state.
44+
*/
45+
template <typename T, SListHook<T> T::* HookPtr>
46+
class AsanSList {
47+
public:
48+
using CompressedPtrType = typename T::CompressedPtrType;
49+
using PtrCompressor = typename T::PtrCompressor;
50+
using SListObject = serialization::SListObject;
51+
52+
// Movable but not copyable
53+
AsanSList(const AsanSList&) = delete;
54+
AsanSList& operator=(const AsanSList&) = delete;
55+
AsanSList(AsanSList&& rhs) noexcept
56+
: compressor_(rhs.compressor_),
57+
allocSize_(rhs.allocSize_),
58+
entries_(std::move(rhs.entries_)) {}
59+
AsanSList& operator=(AsanSList&& rhs) noexcept {
60+
if (&rhs != this) {
61+
// PtrCompressor doesn't support move assignment, so we mirror SList's
62+
// approach: assert compressors match and only move the data fields.
63+
assert(compressor_ == rhs.compressor_);
64+
// Note: don't copy allocSize_ so we don't change ASAN poisoning semantics
65+
// of either list (allocSize_ of 0 disables poisoning)
66+
entries_ = std::move(rhs.entries_);
67+
}
68+
return *this;
69+
}
70+
71+
explicit AsanSList(PtrCompressor compressor, uint32_t allocSize = 0) noexcept
72+
: compressor_(std::move(compressor)), allocSize_(allocSize) {}
73+
74+
AsanSList(const SListObject& object,
75+
PtrCompressor compressor,
76+
uint32_t allocSize = 0)
77+
: compressor_(std::move(compressor)), allocSize_(allocSize) {
78+
// Restore our vector by walking the in-memory chain
79+
SList<T, HookPtr> tempList(object, compressor_);
80+
entries_.reserve(tempList.size());
81+
while (!tempList.empty()) {
82+
void* mem = reinterpret_cast<void*>(tempList.getHead());
83+
tempList.pop();
84+
entries_.push_back(mem);
85+
ASAN_POISON_MEMORY_REGION(mem, allocSize_);
86+
}
87+
}
88+
89+
~AsanSList() = default;
90+
91+
/**
92+
* Adds a node to the list. If allocSize > 0, poisons the allocation memory.
93+
*/
94+
void insert(T& node) {
95+
entries_.push_back(reinterpret_cast<void*>(&node));
96+
ASAN_POISON_MEMORY_REGION(&node, allocSize_);
97+
}
98+
99+
/**
100+
* Removes the most recently inserted node. If allocSize > 0, unpoisons
101+
* the allocation memory.
102+
*
103+
* @throw std::logic_error if called on empty list.
104+
*/
105+
void pop() {
106+
if (empty()) {
107+
throw std::logic_error("Attempting to pop an empty list");
108+
}
109+
void* mem = entries_.back();
110+
entries_.pop_back();
111+
ASAN_UNPOISON_MEMORY_REGION(mem, allocSize_);
112+
}
113+
114+
/**
115+
* Returns a pointer to the most recently inserted node, or nullptr if empty.
116+
*/
117+
T* getHead() const noexcept {
118+
return empty() ? nullptr : reinterpret_cast<T*>(entries_.back());
119+
}
120+
121+
bool empty() const noexcept { return entries_.empty(); }
122+
123+
size_t size() const noexcept { return entries_.size(); }
124+
125+
bool operator==(const AsanSList& other) const noexcept {
126+
return entries_ == other.entries_;
127+
}
128+
129+
/**
130+
* Transfer all elements from 'other' to this list. Executes in linear time
131+
* proportional to the size of 'other'. ASAN poison state of the underlying
132+
* allocations is preserved (only pointers are copied, memory is untouched).
133+
*/
134+
void splice(AsanSList&& other) {
135+
if (other.empty()) {
136+
return;
137+
}
138+
entries_.insert(entries_.end(), other.entries_.begin(),
139+
other.entries_.end());
140+
other.entries_.clear();
141+
}
142+
143+
/**
144+
* Exports the current state as a thrift SListObject for serialization.
145+
* Temporarily unpoisons allocations to construct an SList with intrusive
146+
* hooks, then serializes it. Does not re-poison afterwards since
147+
* saveState() is only called during shutdown.
148+
*/
149+
SListObject saveState() const {
150+
SList<T, HookPtr> tempList{compressor_};
151+
// Iterate in reverse: SList::insert prepends at head, so reverse iteration
152+
// produces head→tail order matching entries_[0..n]. The restore constructor
153+
// pops from head, reproducing the original entries_ order.
154+
for (auto it = entries_.rbegin(); it != entries_.rend(); ++it) {
155+
ASAN_UNPOISON_MEMORY_REGION(*it, allocSize_);
156+
tempList.insert(*reinterpret_cast<T*>(*it));
157+
}
158+
return tempList.saveState();
159+
}
160+
161+
private:
162+
PtrCompressor compressor_;
163+
uint32_t allocSize_{0};
164+
std::vector<void*> entries_;
165+
};
166+
167+
} // namespace facebook::cachelib

0 commit comments

Comments
 (0)