From f7f491b36ea60ff210f092cd7f0a63b52892dbb5 Mon Sep 17 00:00:00 2001 From: Andrey Saranchin <Andrey22102001@gmail.com> Date: Mon, 2 Sep 2024 11:54:16 +0300 Subject: [PATCH] memtx: handle all statements related to the space on DDL When DDL happens, we remove statements of concurrent transactions from MVCC. When removing statements, we set their `engine_savepoint` to `NULL` so that they won't be rolled back because we've already handled them. However, we remove statements only from stories, and not all statements can be accessed in this way. For example, when we have several delete statements of one story, and one of them gets prepared, others are unlinked. It leads to use-after-free, but it's read-only and doesn't affect anything, so only ASAN can catch it. It happens when the statement is being rolled back in `memtx_engine_rollback_statement`: we check if `space->upgrade` is not `NULL` (space can be already deleted) but this check affects instruction flow only if `stmt->new_tuple != NULL` and in our case that's not so. Anyway, let's iterate over all statements of all transactions and remove savepoints for ones related to the space that is being invalidated. It takes more time, but anyway, we are doing DDL that is heavy, so it doesn't really matter. Along the way, the commit removes helper `memtx_tx_history_remove_stmt` and all its helpers because they are not needed anymore. This helper unlinks added story from history chain, links all its delete statements to the previous story, if any, unlinks the statement from related stories and sets `engine_savepoint` to `NULL`. Since we already do all of this things except for unlinking statements from stories, let's simply call `memtx_tx_story_unlink_added[deleted]_by` instead. This change makes the code much more straightforward. Closes #10146 NO_DOC=bugfix (cherry picked from commit ac112b73192ad96271a02ee85dba3e9737fdaa9d) --- ...vcc-ddl-crashes-and-isolation-violation.md | 4 + src/box/memtx_tx.c | 129 +++--------------- test/box-luatest/mvcc_ddl_test.lua | 22 +++ 3 files changed, 46 insertions(+), 109 deletions(-) create mode 100644 changelogs/unreleased/gh-10146-mvcc-ddl-crashes-and-isolation-violation.md diff --git a/changelogs/unreleased/gh-10146-mvcc-ddl-crashes-and-isolation-violation.md b/changelogs/unreleased/gh-10146-mvcc-ddl-crashes-and-isolation-violation.md new file mode 100644 index 0000000000..dc0aa0b252 --- /dev/null +++ b/changelogs/unreleased/gh-10146-mvcc-ddl-crashes-and-isolation-violation.md @@ -0,0 +1,4 @@ +## bugfix/memtx + +* Fixed several bugs when DDL with MVCC enabled could lead to a crash + or violate isolation of other transactions (gh-10146). diff --git a/src/box/memtx_tx.c b/src/box/memtx_tx.c index 97b0ee2088..8c29c9432c 100644 --- a/src/box/memtx_tx.c +++ b/src/box/memtx_tx.c @@ -1202,53 +1202,6 @@ memtx_tx_story_link_top(struct memtx_story *new_top, rlist_splice(&new_link->read_gaps, &old_link->read_gaps); } -static void -memtx_tx_story_unlink_top_on_space_delete(struct memtx_story *story, - uint32_t idx) -{ - struct memtx_story_link *link = &story->link[idx]; - struct memtx_story *old_story = link->older_story; - if (old_story != NULL) - memtx_tx_story_unlink(story, old_story, idx); -} - -/** - * Unlink a @a story from history chain in @a index in both directions. - * Handles case when the story is not on top of the history: simply remove from - * list. - */ -static void -memtx_tx_story_unlink_both_common(struct memtx_story *story, - uint32_t idx) -{ - assert(idx < story->index_count); - struct memtx_story_link *link = &story->link[idx]; - struct memtx_story *newer_story = link->newer_story; - struct memtx_story *older_story = link->older_story; - memtx_tx_story_unlink(newer_story, story, idx); - memtx_tx_story_unlink(story, older_story, idx); - memtx_tx_story_link(newer_story, older_story, idx); -} - -/** - * Unlink a @a story from history chain in @a index in both directions. - * If the story was in the top of history chain - unlink from top. Otherwise, - * see description of `memtx_tx_story_unlink_both_common`. - * Since the space is being deleted, we need to simply unlink the story. - */ -static void -memtx_tx_story_unlink_both_on_space_delete(struct memtx_story *story, - uint32_t idx) -{ - assert(idx < story->index_count); - struct memtx_story_link *link = &story->link[idx]; - if (link->newer_story == NULL) { - memtx_tx_story_unlink_top_on_space_delete(story, idx); - } else { - memtx_tx_story_unlink_both_common(story, idx); - } -} - /** * Change the order of stories in history chain. */ @@ -2076,28 +2029,6 @@ memtx_tx_history_add_stmt(struct txn_stmt *stmt, struct tuple *old_tuple, result); } -/* - * Relink those who delete this story and make them delete older story. - */ -static void -memtx_tx_history_remove_story_del_stmts(struct memtx_story *story) -{ - struct memtx_story *old_story = story->link[0].older_story; - while (story->del_stmt) { - struct txn_stmt *del_stmt = story->del_stmt; - - /* Unlink from old list in any case. */ - story->del_stmt = del_stmt->next_in_del_list; - del_stmt->next_in_del_list = NULL; - del_stmt->del_story = NULL; - - /* Link to old story's list. */ - if (old_story != NULL) - memtx_tx_story_link_deleted_by(old_story, - del_stmt); - } -} - /* * Abort with conflict all transactions that have read @a story. */ @@ -2332,44 +2263,6 @@ memtx_tx_history_rollback_stmt(struct txn_stmt *stmt) assert(stmt->add_story == NULL && stmt->del_story == NULL); } -/* - * Completely remove statement that adds a story. - */ -static void -memtx_tx_history_remove_added_story(struct txn_stmt *stmt) -{ - assert(stmt->add_story != NULL); - assert(stmt->add_story->tuple == stmt->rollback_info.new_tuple); - struct memtx_story *story = stmt->add_story; - memtx_tx_history_remove_story_del_stmts(story); - for (uint32_t i = 0; i < story->index_count; i++) - memtx_tx_story_unlink_both_on_space_delete(story, i); - memtx_tx_story_unlink_added_by(story, stmt); -} - -/* - * Completely remove statement that deletes a story. - */ -static void -memtx_tx_history_remove_deleted_story(struct txn_stmt *stmt) -{ - memtx_tx_story_unlink_deleted_by(stmt->del_story, stmt); -} - -/* - * Completely (as opposed to rollback) remove statement from history. - */ -static void -memtx_tx_history_remove_stmt(struct txn_stmt *stmt) -{ - if (stmt->add_story != NULL) - memtx_tx_history_remove_added_story(stmt); - if (stmt->del_story != NULL) - memtx_tx_history_remove_deleted_story(stmt); - if (stmt->txn->status == TXN_ABORTED) - stmt->engine_savepoint = NULL; -} - /** * Abort or send to read view readers of @a story, except the transaction * @a writer that is actually deletes the story. @@ -2944,9 +2837,10 @@ memtx_tx_invalidate_space(struct space *space, struct txn *active_txn) struct memtx_story, in_space_stories); if (story->add_stmt != NULL) - memtx_tx_history_remove_stmt(story->add_stmt); + memtx_tx_story_unlink_added_by(story, story->add_stmt); while (story->del_stmt != NULL) - memtx_tx_history_remove_stmt(story->del_stmt); + memtx_tx_story_unlink_deleted_by(story, + story->del_stmt); memtx_tx_story_full_unlink_on_space_delete(story); for (uint32_t i = 0; i < story->index_count; i++) { struct rlist *read_gaps = &story->link[i].read_gaps; @@ -2972,6 +2866,23 @@ memtx_tx_invalidate_space(struct space *space, struct txn *active_txn) } memtx_tx_story_delete(story); } + + /* + * Phase three: remove savepoints from all affected statements so that + * they won't be rolled back because we already did it. Moreover, they + * could access the old space that is going to be deleted leading to + * use-after-free. + */ + struct txn *txn; + rlist_foreach_entry(txn, &txm.all_txs, in_all_txs) { + if (txn->status != TXN_ABORTED || txn->psn != 0) + continue; + struct txn_stmt *stmt; + stailq_foreach_entry(stmt, &txn->stmts, next) { + if (stmt->space == space) + stmt->engine_savepoint = NULL; + } + } } /** diff --git a/test/box-luatest/mvcc_ddl_test.lua b/test/box-luatest/mvcc_ddl_test.lua index 0279172c55..7f0381adfa 100644 --- a/test/box-luatest/mvcc_ddl_test.lua +++ b/test/box-luatest/mvcc_ddl_test.lua @@ -472,3 +472,25 @@ g.test_ddl_handles_prepared = function(cg) end end) end + +-- The test covers the problem when already deleted space could be accessed +-- by rollback of a deletion when another concurrent deletion was already +-- prepared. +-- Since the access is read-only and doesn't break anything, the test actually +-- covers the problem only when ASAN is enabled. +g.test_ddl_use_after_free_on_rollback_of_deletion = function(cg) + cg.server:exec(function() + local txn_proxy = require('test.box.lua.txn_proxy') + box.schema.space.create('test') + box.space.test:create_index('pk') + box.space.test:replace{1} + + local tx = txn_proxy.new() + tx:begin() + tx('box.space.test:delete(1)') + + box.space.test:delete(1) + box.space.test:create_index('sk') + tx:rollback() + end) +end -- GitLab