UpsertTableOp::Forward: missing return after ForceToFinish → use-after-free of pooled schema_op_ and finished txm #500
Description
In the schema-change state machine, the unlock_cluster_config_op_ error branch calls ForceToFinish(txm) (which finishes the txm and returns schema_op_ to the shared pool) but then falls through and keeps using schema_op_ / the txm — pushing and processing upsert_kv_table_op_ on an object that may already have been recycled and handed to another transaction.
Evidence
tx_service/src/tx_operation.cpp:2729-2756 (UpsertTableOp::Forward):
else if (op_ == &unlock_cluster_config_op_) {
...
if (unlock_cluster_config_op_.hd_result_.IsError()) {
if (txm->CheckLeaderTerm()) { assert(false); }
else { ForceToFinish(txm); } // finishes txm, recycles schema_op_ -- no return!
}
op_ = &upsert_kv_table_op_; // continues to use the (possibly recycled) op
upsert_kv_table_op_.table_schema_old_ = catalog_rec_.Schema();
...
txm->PushOperation(&upsert_kv_table_op_);
txm->Process(upsert_kv_table_op_); // operate on a finished txm
}Every other ForceToFinish site in this function returns immediately; this branch is missing the return. A sibling release-build fallthrough exists in UpsertTableIndexOp (tx_index_operation.cpp:459-503).
Scenario
A CreateTable/Update/TruncateTable whose unlock_cluster_config_op_ (a local read-lock release) reports an error precisely when this node is no longer leader (the only branch that hits ForceToFinish): the txm is finished and schema_op_ returned to the pool, then the code pushes upsert_kv_table_op_ and processes it → operating on a recycled op / finished txm. If the pool slot was reassigned, this corrupts another in-flight schema transaction.
Fix: return after ForceToFinish(txm) in this branch (and audit the UpsertTableIndexOp sibling).
Found during a code audit (PR #493). Verified against source at the cited lines.
🤖 Found with Claude Code
Activity
fix: prevent schema operation use-after-free and command ID truncation