Skip to content

Commit 28dd843

Browse files
fzipiclaude
andcommitted
fix: make setvar =+N / =-N an atomic update in the collection backend
`setvar:ip.counter=+1` was a read-modify-write at the action level: `SetVar::evaluate` resolved the current value, added the delta and stored the result with `storeOrUpdateFirst`. Two transactions doing the same read concurrently stored the same incremented number, so counters in persistent collections drifted under load. With LMDB it is worse: the database is opened with `MDB_DUPSORT`, so a stale read makes the `mdb_del` inside `storeOrUpdateFirst` fail and the whole write transaction is aborted, losing the increment outright. Add `Collection::incrementFirst(key, delta)` and implement it in both backends as one atomic step: the in-memory backend does the read, add and write under its existing exclusive lock, and the LMDB backend does them inside a single write transaction, which holds the LMDB writer lock across threads and processes. The base class keeps a non-atomic fallback for out-of-tree backends. `SetVar::evaluate` now calls it for the `=+N` and `=-N` forms through the `*_DynamicElement` helpers. Missing, expired or non-numeric values still count as 0, as before. Measured with a harness running 8 threads x 2000 transactions, each with `setvar:ip.counter=+1`, against an LMDB build on macOS: before this change 2001 of 16001 increments survived (three identical runs); with it, all 16001 do. Fixes #2306. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tz1sULBvqAwnRreKh5eC9h
1 parent 84b5996 commit 28dd843

14 files changed

Lines changed: 286 additions & 22 deletions

File tree

‎headers/modsecurity/collection/collection.h‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,30 @@ class Collection {
6969
std::vector<const VariableValue *> *l,
7070
variables::KeyExclusions &ke) = 0;
7171

72+
/* incrementFirst: add `delta` to the integer stored under `key` (missing,
73+
* expired or non-numeric values count as 0) and return the new value.
74+
* Backends do the read-modify-write as one atomic step; this default is
75+
* only a non-atomic fallback for backends that do not override it. */
76+
virtual int incrementFirst(const std::string &key, int delta) {
77+
int value = 0;
78+
if (auto current = resolveFirst(key)) {
79+
try {
80+
value = std::stoi(*current);
81+
} catch (...) {
82+
value = 0;
83+
}
84+
}
85+
value += delta;
86+
storeOrUpdateFirst(key, std::to_string(value));
87+
return value;
88+
}
89+
90+
virtual int incrementFirst(const std::string &key,
91+
std::string compartment, std::string compartment2, int delta) {
92+
std::string nkey = compartment + "::" + compartment2 + "::" + key;
93+
return incrementFirst(nkey, delta);
94+
}
95+
7296

7397
/* storeOrUpdateFirst */
7498
virtual bool storeOrUpdateFirst(const std::string &key,

‎src/actions/set_var.cc‎

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -90,36 +90,37 @@ bool SetVar::evaluate(RuleWithActions *rule, Transaction *t) {
9090
}
9191
goto end;
9292
} else {
93-
int pre = 0;
93+
/* =+N / =-N: the backend reads, adds and writes in one atomic step */
94+
int delta = 0;
9495
int value = 0;
9596

9697
try {
97-
pre = stoi(resolvedPre);
98+
delta = stoi(resolvedPre);
9899
} catch (...) {
99-
pre = 0;
100+
delta = 0;
100101
}
101-
102-
try {
103-
std::vector<const VariableValue *> l;
104-
RuleWithOperator *rr = dynamic_cast<RuleWithOperator *>(rule);
105-
m_variable->evaluate(t, rr, &l);
106-
if (l.size() == 0) {
107-
value = 0;
108-
} else {
109-
value = stoi(l[0]->getValue());
110-
for (auto &i : l) {
111-
delete i;
112-
}
113-
}
114-
} catch (...) {
115-
value = 0;
102+
if (m_operation == substractAndSetOperation) {
103+
delta = -delta;
116104
}
117105

118-
if (m_operation == sumAndSetOperation) {
119-
targetValue = std::to_string(value + pre);
120-
} else if (m_operation == substractAndSetOperation) {
121-
targetValue = std::to_string(value - pre);
106+
if (tx) {
107+
value = tx->incrementFirst(t, m_variableNameExpanded, delta);
108+
} else if (session) {
109+
value = session->incrementFirst(t, m_variableNameExpanded, delta);
110+
} else if (ip) {
111+
value = ip->incrementFirst(t, m_variableNameExpanded, delta);
112+
} else if (resource) {
113+
value = resource->incrementFirst(t, m_variableNameExpanded, delta);
114+
} else if (global) {
115+
value = global->incrementFirst(t, m_variableNameExpanded, delta);
116+
} else if (user) {
117+
value = user->incrementFirst(t, m_variableNameExpanded, delta);
122118
}
119+
120+
ms_dbg_a(t, 8, "Saving variable: " + m_variable->m_collectionName \
121+
+ ":" + m_variableNameExpanded + " with value: " \
122+
+ std::to_string(value));
123+
goto end;
123124
}
124125

125126
ms_dbg_a(t, 8, "Saving variable: " + m_variable->m_collectionName \

‎src/collection/backend/in_memory-per_process.cc‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,33 @@ bool InMemoryPerProcess::updateFirst(const std::string &key,
9191
}
9292

9393

94+
int InMemoryPerProcess::incrementFirst(const std::string &key, int delta) {
95+
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
96+
int value = 0;
97+
98+
auto search = m_map.find(key);
99+
if (search != m_map.end() && search->second.isExpired()) {
100+
m_map.erase(search);
101+
search = m_map.end();
102+
}
103+
if (search != m_map.end() && search->second.hasValue()) {
104+
try {
105+
value = std::stoi(search->second.getValue());
106+
} catch (...) {
107+
value = 0;
108+
}
109+
}
110+
value += delta;
111+
112+
if (search != m_map.end()) {
113+
search->second.setValue(std::to_string(value));
114+
} else {
115+
__store(m_map, key, std::to_string(value));
116+
}
117+
return value;
118+
}
119+
120+
94121
void InMemoryPerProcess::del(const std::string& key) {
95122
const std::lock_guard lock(m_mutex); // write lock (exclusive access)
96123
m_map.erase(key);

‎src/collection/backend/in_memory-per_process.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ class InMemoryPerProcess :
8383
bool updateFirst(const std::string &key,
8484
const std::string &value) override;
8585

86+
int incrementFirst(const std::string &key, int delta) override;
87+
8688
void del(const std::string& key) override;
8789

8890
void delIfExpired(const std::string& key);

‎src/collection/backend/lmdb.cc‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,77 @@ bool LMDB::storeOrUpdateFirst(const std::string &key,
355355
}
356356

357357

358+
int LMDB::incrementFirst(const std::string &key, int delta) {
359+
int rc;
360+
int value = 0;
361+
MDB_txn *txn;
362+
MDB_val mdb_key;
363+
MDB_val mdb_value;
364+
MDB_val mdb_value_ret;
365+
CollectionData previous_data;
366+
CollectionData new_data;
367+
std::string serializedData;
368+
369+
string2val(key, &mdb_key);
370+
371+
/* A write transaction holds the LMDB writer lock, so the read, the
372+
* add and the write below are one atomic step across threads and
373+
* processes. */
374+
rc = txn_begin(0, &txn);
375+
lmdb_debug(rc, "txn", "incrementFirst");
376+
if (rc != 0) {
377+
goto end_txn;
378+
}
379+
380+
rc = mdb_get(txn, m_dbi, &mdb_key, &mdb_value_ret);
381+
lmdb_debug(rc, "get", "incrementFirst");
382+
if (rc == 0) {
383+
previous_data.setFromSerialized(reinterpret_cast<char *>(mdb_value_ret.mv_data), mdb_value_ret.mv_size);
384+
if (previous_data.hasValue() && !previous_data.isExpired()) {
385+
try {
386+
value = std::stoi(previous_data.getValue());
387+
} catch (...) {
388+
value = 0;
389+
}
390+
}
391+
rc = mdb_del(txn, m_dbi, &mdb_key, &mdb_value_ret);
392+
lmdb_debug(rc, "del", "incrementFirst");
393+
if (rc != 0) {
394+
goto end_del;
395+
}
396+
}
397+
value += delta;
398+
399+
if (previous_data.hasExpiry() && !previous_data.isExpired()) {
400+
new_data = previous_data;
401+
}
402+
new_data.setValue(std::to_string(value));
403+
serializedData = new_data.getSerialized();
404+
string2val(serializedData, &mdb_value);
405+
406+
rc = mdb_put(txn, m_dbi, &mdb_key, &mdb_value, 0);
407+
lmdb_debug(rc, "put", "incrementFirst");
408+
if (rc != 0) {
409+
goto end_put;
410+
}
411+
412+
rc = mdb_txn_commit(txn);
413+
lmdb_debug(rc, "commit", "incrementFirst");
414+
if (rc != 0) {
415+
goto end_commit;
416+
}
417+
418+
end_put:
419+
end_del:
420+
if (rc != 0) {
421+
mdb_txn_abort(txn);
422+
}
423+
end_commit:
424+
end_txn:
425+
return value;
426+
}
427+
428+
358429
void LMDB::resolveSingleMatch(const std::string& var,
359430
std::vector<const VariableValue *> *l) {
360431
int rc;

‎src/collection/backend/lmdb.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ class LMDB :
103103
bool updateFirst(const std::string &key,
104104
const std::string &value) override;
105105

106+
int incrementFirst(const std::string &key, int delta) override;
107+
106108
void del(const std::string& key) override;
107109

108110
void setExpiry(const std::string& key, int32_t expiry_seconds) override;

‎src/variables/global.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ class Global_DynamicElement : public Variable {
111111
t->m_rules->m_secWebAppId.m_value, expiry_seconds);
112112
}
113113

114+
static int incrementFirst(Transaction *t, const std::string &var,
115+
int delta) {
116+
return t->m_collections.m_global_collection->incrementFirst(
117+
var, t->m_collections.m_global_collection_key,
118+
t->m_rules->m_secWebAppId.m_value,
119+
delta);
120+
}
121+
114122
static void storeOrUpdateFirst(Transaction *t, const std::string &var,
115123
const std::string &value) {
116124
t->m_collections.m_global_collection->storeOrUpdateFirst(

‎src/variables/ip.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ class Ip_DynamicElement : public Variable {
110110
t->m_rules->m_secWebAppId.m_value, expiry_seconds);
111111
}
112112

113+
static int incrementFirst(Transaction *t, const std::string &var,
114+
int delta) {
115+
return t->m_collections.m_ip_collection->incrementFirst(
116+
var, t->m_collections.m_ip_collection_key,
117+
t->m_rules->m_secWebAppId.m_value,
118+
delta);
119+
}
120+
113121
static void storeOrUpdateFirst(Transaction *t, const std::string &var,
114122
const std::string &value) {
115123
t->m_collections.m_ip_collection->storeOrUpdateFirst(

‎src/variables/resource.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,14 @@ class Resource_DynamicElement : public Variable {
111111
}
112112

113113

114+
static int incrementFirst(Transaction *t, const std::string &var,
115+
int delta) {
116+
return t->m_collections.m_resource_collection->incrementFirst(
117+
var, t->m_collections.m_resource_collection_key,
118+
t->m_rules->m_secWebAppId.m_value,
119+
delta);
120+
}
121+
114122
static void storeOrUpdateFirst(Transaction *t, const std::string &var,
115123
const std::string &value) {
116124
t->m_collections.m_resource_collection->storeOrUpdateFirst(

‎src/variables/session.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ class Session_DynamicElement : public Variable {
110110
t->m_rules->m_secWebAppId.m_value, expiry_seconds);
111111
}
112112

113+
static int incrementFirst(Transaction *t, const std::string &var,
114+
int delta) {
115+
return t->m_collections.m_session_collection->incrementFirst(
116+
var, t->m_collections.m_session_collection_key,
117+
t->m_rules->m_secWebAppId.m_value,
118+
delta);
119+
}
120+
113121
static void storeOrUpdateFirst(Transaction *t, const std::string &var,
114122
const std::string &value) {
115123
t->m_collections.m_session_collection->storeOrUpdateFirst(

0 commit comments

Comments
 (0)