fix(generic): stop RM from freeing a container under a yielding iteration - #8210
fix(generic): stop RM from freeing a container under a yielding iteration#8210vyavdoshenko wants to merge 1 commit into
Conversation
PR Summary by QodoPrevent RM deletion during yielding container iteration
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Code Review by Qodo
🔴 High 1. RM permanently skips keys
|
🤖 Augment PR SummarySummary: This PR prevents
🤖 Was this summary useful? React with 👍 or 👎 |
Code Review by Qodo
🔴 High 1. Expiry bypasses iteration guard
|
There was a problem hiding this comment.
Pull request overview
This PR addresses a concurrency use-after-free where RM can delete a container key while another command is suspended mid-container-iteration (which holds raw pointers into the value across yields), leading to debug aborts or release UAF.
Changes:
- Add a per-thread “container iteration in flight” guard in
container_utilsto detect when a fiber is suspended insideIterate*helpers. - Gate
RMdeletions on that guard to avoid freeing container memory while an iterator may resume. - Add a regression test that races
SORT_RO(yielding iteration) withRMto reproduce the prior crash.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/server/generic_family.cc |
Adds RM-path check intended to avoid deleting keys while container iteration is in-flight. |
src/server/generic_family_test.cc |
Adds regression test for RM vs yielding container iteration. |
src/server/container_utils.h |
Declares IsIterationInFlight() API for iteration state detection. |
src/server/container_utils.cc |
Implements iteration-depth tracking via an RAII guard in all Iterate* helpers. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
kostasrim
left a comment
There was a problem hiding this comment.
You can just write an one liner:
if (!db_slice.CheckLock(IntentLock::EXCLUSIVE, op_args.db_cntx.db_index, key))
continue;
which we already do this pattern in heartbeat if I remember correctly. Adjust the check accordingly (see my other comment)
| return ShardFFResult{std::get<0>(res), std::get<2>(res)}; | ||
| } | ||
|
|
||
| thread_local unsigned tl_iteration_depth = 0; |
There was a problem hiding this comment.
I suspect you don't need any of these and the fix is an one liner:
if (!db_slice.CheckLock(IntentLock::EXCLUSIVE, op_args.db_cntx.db_index, key))
continue;
(adjust it to cover both lock types) that way we skip those keys that are already locked by the transaction layer.
There was a problem hiding this comment.
and that way we don't skip the command...
There was a problem hiding this comment.
Tried it verbatim, it still aborts. RmDuringContainerYield gives SIGABRT 3/3 with the same stack as the fuzzer (lpAssertValidEntry -> lpFirst -> QList::Iterate).
The lock table is empty for exactly the commands that crash: a single-shard, single-hop command runs optimistically and skips registration (transaction.cc:1293), and the lazy registration is done by the next transaction scheduled on the shard. RM is never one: it has no keys (generic_family.cc:2979) and runs through ess->Await() with a null transaction. Visible without any patch: DEBUG OBJECT on a list while SORT_RO walks it reports no lock.
The heartbeat precedent (db_slice.cc:1624) holds because a blocked client does hold a registered lock. SORT/LRANGE do not. And Check(EXCLUSIVE) already covers shared locks (intent_lock.h:29), so the mode is not the gap.
continue also would not be enough: OpScan has already advanced the cursor, so skipping loses the key, and RM could answer cursor 0 with it still there.
4442ac3 to
11619b7
Compare
11619b7 to
37fc390
Compare
37fc390 to
8a17d8d
Compare
RM deletes keys outside the transaction framework, so nothing serializes it against a command that is suspended mid-iteration. Container iteration yields every --container_iteration_yield_interval_usec (500 usec by default) while holding raw pointers into the value, so RM can free the container under the running iterator: an lpAssertValidEntry abort in debug builds, a silent use-after-free in release.
Reproducible example:
Plain main, one proactor thread, no special flags:
LRANGE and SORT_RO work as the reader too, so the victim side needs no write permission.
Found here:
https://github.com/dragonflydb/dragonfly/actions/runs/33478201692/job/99761833826
and
https://github.com/dragonflydb/dragonfly/actions/runs/33416302935/job/99567646361