Skip to content

Commit 2c79137

Browse files
committed
Prevent concurrent access to data structure in resolve methods
- As reported in #3054, the resolve methods in InMemoryPerProcess are not acquiring a lock/mutex to prevent concurrent access to the data structures that may be modified at the same time from other threads, and thus triggering undefined behaviour. - Replace inheritance of std::unordered_multimap data structure with data member to prevent potential clients to use it without acquiring the mutex to protect concurrent access. - Replace pthreads lock with std C++11 std::shared_mutex - Provides exclusive/shared lock access so that multiple readers can access the data at the same time, but only one writer. this is used to favor query performance by allowing more concurrent access to the data until an update needs to be performed. - Simplifies acquisition and disposal of lock/mutex with std::lock_guard, which has RAII semantics. - NOTE: Because std::shared_mutex is not recursive, calls to another function that tries to acquire the lock will fail. Introduced __store & __updateFirst helper methods to workaround this.
1 parent e8db92e commit 2c79137

File tree

2 files changed

+140
-126
lines changed

2 files changed

+140
-126
lines changed

src/collection/backend/in_memory-per_process.cc

Lines changed: 131 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -39,93 +39,108 @@ namespace backend {
3939

4040
InMemoryPerProcess::InMemoryPerProcess(const std::string &name) :
4141
Collection(name) {
42-
this->reserve(1000);
43-
pthread_mutex_init(&m_lock, NULL);
42+
m_map.reserve(1000);
4443
}
4544

4645
InMemoryPerProcess::~InMemoryPerProcess() {
47-
this->clear();
48-
pthread_mutex_destroy(&m_lock);
46+
m_map.clear();
4947
}
5048

51-
void InMemoryPerProcess::store(std::string key, std::string value) {
52-
pthread_mutex_lock(&m_lock);
53-
this->emplace(key, value);
54-
pthread_mutex_unlock(&m_lock);
55-
}
5649

50+
template<typename Map>
51+
inline void __store(Map &map, std::string key, std::string value) {
52+
// NOTE: should be called with write-lock previously acquired
5753

58-
bool InMemoryPerProcess::storeOrUpdateFirst(const std::string &key,
59-
const std::string &value) {
60-
if (updateFirst(key, value) == false) {
61-
store(key, value);
62-
}
63-
return true;
54+
map.emplace(key, value);
6455
}
6556

6657

67-
bool InMemoryPerProcess::updateFirst(const std::string &key,
58+
template<typename Map>
59+
inline bool __updateFirst(Map &map,
60+
const std::string &key,
6861
const std::string &value) {
69-
pthread_mutex_lock(&m_lock);
62+
// NOTE: should be called with write-lock previously acquired
7063

71-
if (auto search = this->find(key); search != this->end()) {
64+
if (auto search = map.find(key); search != map.end()) {
7265
search->second.setValue(value);
73-
pthread_mutex_unlock(&m_lock);
7466
return true;
7567
}
7668

77-
pthread_mutex_unlock(&m_lock);
7869
return false;
7970
}
8071

8172

73+
void InMemoryPerProcess::store(const std::string &key, const std::string &value) {
74+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
75+
__store(m_map, key, value);
76+
}
77+
78+
79+
bool InMemoryPerProcess::storeOrUpdateFirst(const std::string &key,
80+
const std::string &value) {
81+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
82+
if (__updateFirst(m_map, key, value) == false) {
83+
__store(m_map, key, value);
84+
}
85+
return true;
86+
}
87+
88+
89+
bool InMemoryPerProcess::updateFirst(const std::string &key,
90+
const std::string &value) {
91+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
92+
return __updateFirst(m_map, key, value);
93+
}
94+
95+
8296
void InMemoryPerProcess::del(const std::string& key) {
83-
pthread_mutex_lock(&m_lock);
84-
this->erase(key);
85-
pthread_mutex_unlock(&m_lock);
97+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
98+
m_map.erase(key);
8699
}
87100

88101
void InMemoryPerProcess::delIfExpired(const std::string& key) {
89-
pthread_mutex_lock(&m_lock);
102+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
90103
// Double check the status while within the mutex
91-
auto iter = this->find(key);
92-
if ((iter != this->end()) && (iter->second.isExpired())) {
93-
this->erase(key);
104+
const auto iter = std::find_if(m_map.begin(), m_map.end(),
105+
[&key](const auto &x) { return x.first == key && x.second.isExpired(); });
106+
if (iter != m_map.end()) {
107+
m_map.erase(key);
94108
}
95-
pthread_mutex_unlock(&m_lock);
96109
}
97110

98111
void InMemoryPerProcess::setExpiry(const std::string& key, int32_t expiry_seconds) {
99-
pthread_mutex_lock(&m_lock);
112+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
100113

101-
if (auto search = this->find(key); search != this->end()) {
114+
if (const auto search = m_map.find(key); search != m_map.end()) {
102115
search->second.setExpiry(expiry_seconds);
103-
pthread_mutex_unlock(&m_lock);
104116
return;
105117
}
106118

107119
// We allow an expiry value to be set for a key that has not (yet) had a value set.
108-
auto iter = this->emplace(key, CollectionData());
120+
const auto iter = m_map.emplace(key, CollectionData());
109121
iter->second.setExpiry(expiry_seconds);
110-
111-
pthread_mutex_unlock(&m_lock);
112122
}
113123

114124

115125
void InMemoryPerProcess::resolveSingleMatch(const std::string& var,
116126
std::vector<const VariableValue *> *l) {
117127
std::list<std::string> expiredVars;
118-
auto range = this->equal_range(var);
119-
120-
for (auto it = range.first; it != range.second; ++it) {
121-
if (it->second.isExpired()) {
122-
expiredVars.push_back(it->first);
123-
} else if (it->second.hasValue() == false) {
124-
// No-op. A non-expired expiry exists for the key but there is no actual value
125-
} else {
126-
l->push_back(new VariableValue(&m_name, &it->first, &it->second.getValue()));
127-
}
128+
129+
{
130+
const std::shared_lock lock(m_mutex); // read lock (shared access)
131+
132+
const auto range = m_map.equal_range(var);
133+
for (auto it = range.first; it != range.second; ++it) {
134+
if (it->second.isExpired()) {
135+
expiredVars.push_back(it->first);
136+
} else if (it->second.hasValue() == false) {
137+
// No-op. A non-expired expiry exists for the key but there is no actual value
138+
} else {
139+
l->push_back(new VariableValue(&m_name, &it->first, &it->second.getValue()));
140+
}
141+
}
128142
}
143+
129144
for (const auto& expiredVar : expiredVars) {
130145
delIfExpired(expiredVar);
131146
}
@@ -134,40 +149,45 @@ void InMemoryPerProcess::resolveSingleMatch(const std::string& var,
134149

135150
void InMemoryPerProcess::resolveMultiMatches(const std::string& var,
136151
std::vector<const VariableValue *> *l, variables::KeyExclusions &ke) {
137-
size_t keySize = var.size();
152+
const auto keySize = var.size();
138153
l->reserve(15);
139154
std::list<std::string> expiredVars;
140155

141-
if (keySize == 0) {
142-
for (auto &i : *this) {
143-
if (ke.toOmit(i.first)) {
144-
continue;
156+
{
157+
const std::shared_lock lock(m_mutex); // read lock (shared access)
158+
159+
if (keySize == 0) {
160+
for (auto &i : m_map) {
161+
if (ke.toOmit(i.first)) {
162+
continue;
163+
}
164+
if (i.second.isExpired()) {
165+
expiredVars.push_back(i.first);
166+
} else if (i.second.hasValue() == false) {
167+
// No-op. A non-expired expiry exists for the key but there is no actual value
168+
} else {
169+
l->insert(l->begin(), new VariableValue(&m_name, &i.first,
170+
&i.second.getValue()));
171+
}
145172
}
146-
if (i.second.isExpired()) {
147-
expiredVars.push_back(i.first);
148-
} else if (i.second.hasValue() == false) {
149-
// No-op. A non-expired expiry exists for the key but there is no actual value
150-
} else {
151-
l->insert(l->begin(), new VariableValue(&m_name, &i.first,
152-
&i.second.getValue()));
153-
}
154-
}
155-
} else {
156-
auto range = this->equal_range(var);
157-
for (auto it = range.first; it != range.second; ++it) {
158-
if (ke.toOmit(var)) {
159-
continue;
173+
} else {
174+
const auto range = m_map.equal_range(var);
175+
for (auto it = range.first; it != range.second; ++it) {
176+
if (ke.toOmit(var)) {
177+
continue;
178+
}
179+
if (it->second.isExpired()) {
180+
expiredVars.push_back(it->first);
181+
} else if (it->second.hasValue() == false) {
182+
// No-op. A non-expired expiry exists for the key but there is no actual value
183+
} else {
184+
l->insert(l->begin(), new VariableValue(&m_name, &var,
185+
&it->second.getValue()));
186+
}
160187
}
161-
if (it->second.isExpired()) {
162-
expiredVars.push_back(it->first);
163-
} else if (it->second.hasValue() == false) {
164-
// No-op. A non-expired expiry exists for the key but there is no actual value
165-
} else {
166-
l->insert(l->begin(), new VariableValue(&m_name, &var,
167-
&it->second.getValue()));
168-
}
169188
}
170189
}
190+
171191
for (const auto& expiredVar : expiredVars) {
172192
delIfExpired(expiredVar);
173193
}
@@ -176,47 +196,30 @@ void InMemoryPerProcess::resolveMultiMatches(const std::string& var,
176196

177197
void InMemoryPerProcess::resolveRegularExpression(const std::string& var,
178198
std::vector<const VariableValue *> *l, variables::KeyExclusions &ke) {
179-
180-
//if (var.find(":") == std::string::npos) {
181-
// return;
182-
//}
183-
//if (var.size() < var.find(":") + 3) {
184-
// return;
185-
//}
186-
//std::string col = std::string(var, 0, var.find(":"));
187-
//std::string name = std::string(var, var.find(":") + 2,
188-
// var.size() - var.find(":") - 3);
189-
//size_t keySize = col.size();
190199
Utils::Regex r(var, true);
191200
std::list<std::string> expiredVars;
192201

193-
for (const auto& x : *this) {
194-
//if (x.first.size() <= keySize + 1) {
195-
// continue;
196-
//}
197-
//if (x.first.at(keySize) != ':') {
198-
// continue;
199-
//}
200-
//if (std::string(x.first, 0, keySize) != col) {
201-
// continue;
202-
//}
203-
//std::string content = std::string(x.first, keySize + 1,
204-
// x.first.size() - keySize - 1);
205-
int ret = Utils::regex_search(x.first, r);
206-
if (ret <= 0) {
207-
continue;
208-
}
209-
if (ke.toOmit(x.first)) {
210-
continue;
202+
{
203+
const std::shared_lock lock(m_mutex); // read lock (shared access)
204+
205+
for (const auto& x : m_map) {
206+
const auto ret = Utils::regex_search(x.first, r);
207+
if (ret <= 0) {
208+
continue;
209+
}
210+
if (ke.toOmit(x.first)) {
211+
continue;
212+
}
213+
if (x.second.isExpired()) {
214+
expiredVars.push_back(x.first);
215+
} else if (x.second.hasValue() == false) {
216+
// No-op. A non-expired expiry exists for the key but there is no actual value
217+
} else {
218+
l->insert(l->begin(), new VariableValue(&m_name, &x.first, &x.second.getValue()));
219+
}
211220
}
212-
if (x.second.isExpired()) {
213-
expiredVars.push_back(x.first);
214-
} else if (x.second.hasValue() == false) {
215-
// No-op. A non-expired expiry exists for the key but there is no actual value
216-
} else {
217-
l->insert(l->begin(), new VariableValue(&m_name, &x.first, &x.second.getValue()));
218-
}
219221
}
222+
220223
for (const auto& expiredVar : expiredVars) {
221224
delIfExpired(expiredVar);
222225
}
@@ -225,18 +228,29 @@ void InMemoryPerProcess::resolveRegularExpression(const std::string& var,
225228

226229
std::unique_ptr<std::string> InMemoryPerProcess::resolveFirst(
227230
const std::string& var) {
228-
auto range = equal_range(var);
229-
for (auto it = range.first; it != range.second; ++it) {
230-
if (it->second.isExpired()) {
231-
delIfExpired(it->second.getValue());
232-
} else if (it->second.hasValue() == false) {
233-
// No-op. A non-expired expiry exists for the key but there is no actual value
234-
} else {
235-
return std::unique_ptr<std::string>(new std::string(it->second.getValue()));
236-
}
231+
std::unique_ptr<std::string> ret;
232+
std::list<std::string> expiredVars;
233+
234+
{
235+
const std::shared_lock lock(m_mutex); // read lock (shared access)
236+
237+
const auto range = m_map.equal_range(var);
238+
for (auto it = range.first; it != range.second; ++it) {
239+
if (it->second.isExpired()) {
240+
expiredVars.push_back(it->first);
241+
} else if (it->second.hasValue() == false) {
242+
// No-op. A non-expired expiry exists for the key but there is no actual value
243+
} else {
244+
ret = std::make_unique<std::string>(it->second.getValue());
245+
}
246+
}
247+
}
248+
249+
for (const auto& expiredVar : expiredVars) {
250+
delIfExpired(expiredVar);
237251
}
238252

239-
return NULL;
253+
return ret;
240254
}
241255

242256

src/collection/backend/in_memory-per_process.h

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
* directly using the email address security@modsecurity.org.
1313
*
1414
*/
15-
#include <pthread.h>
1615

1716

1817
#ifdef __cplusplus
@@ -24,6 +23,7 @@
2423
#include <vector>
2524
#include <algorithm>
2625
#include <memory>
26+
#include <shared_mutex>
2727
#endif
2828

2929

@@ -71,13 +71,11 @@ struct MyHash{
7171
};
7272

7373
class InMemoryPerProcess :
74-
public std::unordered_multimap<std::string, CollectionData,
75-
/*std::hash<std::string>*/MyHash, MyEqual>,
7674
public Collection {
7775
public:
7876
explicit InMemoryPerProcess(const std::string &name);
7977
~InMemoryPerProcess();
80-
void store(std::string key, std::string value);
78+
void store(const std::string &key, const std::string &value);
8179

8280
bool storeOrUpdateFirst(const std::string &key,
8381
const std::string &value) override;
@@ -103,21 +101,23 @@ class InMemoryPerProcess :
103101
variables::KeyExclusions &ke) override;
104102

105103
/* store */
106-
virtual void store(std::string key, std::string compartment,
104+
virtual void store(const std::string &key, std::string &compartment,
107105
std::string value) {
108-
std::string nkey = compartment + "::" + key;
106+
const auto nkey = compartment + "::" + key;
109107
store(nkey, value);
110108
}
111109

112110

113-
virtual void store(std::string key, std::string compartment,
111+
virtual void store(const std::string &key, const std::string &compartment,
114112
std::string compartment2, std::string value) {
115-
std::string nkey = compartment + "::" + compartment2 + "::" + key;
113+
const auto nkey = compartment + "::" + compartment2 + "::" + key;
116114
store(nkey, value);
117115
}
118116

119117
private:
120-
pthread_mutex_t m_lock;
118+
std::unordered_multimap<std::string, CollectionData,
119+
/*std::hash<std::string>*/MyHash, MyEqual> m_map;
120+
std::shared_mutex m_mutex;
121121
};
122122

123123
} // namespace backend

0 commit comments

Comments
 (0)