commit d943d85fd2eebc212bca42e7e04143863a92d139
Author: Thirunarayanan Balathandayuthapani <thiru@mariadb.com>
Date:   Tue Jun 23 13:53:03 2026 +0530

    MDEV-39923 HANDLER read crashes restore_position() for InnoDB partitioned table
    
    Problem:
    ========
      HANDLER READ idx=(const) followed by READ idx PREV/NEXT on a
    partitioned table aborts in btr_pcur_t::restore_position()
    prebuilt->pcur lives from handler open to close and is reused across
    statements. An ordinary query re-positions the cursor before every
    NEXT/PREV, but HANDLER continues a scan from a previous statement
    and trusts the cursor to still hold a valid position for the
    current index.
    
    On a partitioned table that assumption breaks because:
    ha_partition::init_table_handle_for_HANDLER() is a no-op, so the
    child InnoDB handlers never set used_in_HANDLER and
    row_search_mvcc() skipped storing the cursor position.
    
    - Each partition has its own prebuilt/cursor; READ idx=(const)
    is pruned to one partition, but others keep a stale position.
    change_active_index() switched prebuilt->index to PRIMARY without
    resetting the cursor, and the exhaustive ordered PREV
    then restored that stale/empty position.
    
    Solution:
    =========
    ha_partition::init_table_handle_for_HANDLER():
    forwards to all locked partitions so the children enter HANDLER mode.
    
    ha_innobase::change_active_index(): Discards a position stored
    on a different index.
    
    row_search_mvcc(): returns DB_RECORD_NOT_FOUND for a HANDLER
    continuation whose cursor holds no stored position.

diff --git a/mysql-test/suite/parts/r/partition_special_innodb.result b/mysql-test/suite/parts/r/partition_special_innodb.result
index 504989e25a1..f404ea119ae 100644
--- a/mysql-test/suite/parts/r/partition_special_innodb.result
+++ b/mysql-test/suite/parts/r/partition_special_innodb.result
@@ -354,3 +354,17 @@ connection con2;
 DROP TABLE t1;
 disconnect con2;
 connection default;
+#
+#  MDEV-39923 HANDLER read crashes restore_position() for
+# 		InnoDB partitioned table
+#
+CREATE TABLE t1 (c1 BIT KEY,c2 INT, INDEX idx(c2)) ENGINE=InnoDB
+PARTITION BY LINEAR HASH(c1) PARTITIONS 4;
+INSERT INTO t1 PARTITION (p1) VALUES (1,1);
+UPDATE t1 SET c1=0;
+HANDLER t1 OPEN;
+HANDLER t1 READ `PRIMARY`=(1);
+c1	c2
+HANDLER t1 READ `PRIMARY` PREV;
+c1	c2
+DROP TABLE t1;
diff --git a/mysql-test/suite/parts/t/partition_special_innodb.test b/mysql-test/suite/parts/t/partition_special_innodb.test
index 836b7048ccf..057fe0ec6cc 100644
--- a/mysql-test/suite/parts/t/partition_special_innodb.test
+++ b/mysql-test/suite/parts/t/partition_special_innodb.test
@@ -232,3 +232,16 @@ DROP TABLE t1;
 
 --disconnect con2
 --connection default
+
+--echo #
+--echo #  MDEV-39923 HANDLER read crashes restore_position() for
+--echo # 		InnoDB partitioned table
+--echo #
+CREATE TABLE t1 (c1 BIT KEY,c2 INT, INDEX idx(c2)) ENGINE=InnoDB
+		PARTITION BY LINEAR HASH(c1) PARTITIONS 4;
+INSERT INTO t1 PARTITION (p1) VALUES (1,1);
+UPDATE t1 SET c1=0;
+HANDLER t1 OPEN;
+HANDLER t1 READ `PRIMARY`=(1);
+HANDLER t1 READ `PRIMARY` PREV;
+DROP TABLE t1;
diff --git a/sql/ha_partition.cc b/sql/ha_partition.cc
index ffb6c01aea9..48c8c15af86 100644
--- a/sql/ha_partition.cc
+++ b/sql/ha_partition.cc
@@ -11199,7 +11199,10 @@ void ha_partition::release_auto_increment()
 
 void ha_partition::init_table_handle_for_HANDLER()
 {
-  return;
+  for (uint i= bitmap_get_first_set(&m_part_info->lock_partitions);
+       i < m_tot_parts;
+       i= bitmap_get_next_set(&m_part_info->lock_partitions, i))
+    m_file[i]->init_table_handle_for_HANDLER();
 }
 
 
diff --git a/storage/innobase/handler/ha_innodb.cc b/storage/innobase/handler/ha_innodb.cc
index 26564a5fb50..e7fd1c2be5e 100644
--- a/storage/innobase/handler/ha_innodb.cc
+++ b/storage/innobase/handler/ha_innodb.cc
@@ -9164,6 +9164,15 @@ ha_innobase::change_active_index(
 				m_prebuilt->table->fts->doc_col, false);
 	}
 
+	if (m_prebuilt->pcur &&
+	    m_prebuilt->pcur->index() != m_prebuilt->index) {
+		/* Discard the position stored on a different index */
+		m_prebuilt->pcur->old_rec = nullptr;
+		m_prebuilt->pcur->old_n_fields = 0;
+		m_prebuilt->pcur->old_n_core_fields = 0;
+		m_prebuilt->pcur->pos_state = BTR_PCUR_NOT_POSITIONED;
+	}
+
 	/* MySQL changes the active index for a handle also during some
 	queries, for example SELECT MAX(a), SUM(a) first retrieves the MAX()
 	and then calculates the sum. Previously we played safe and used
diff --git a/storage/innobase/row/row0sel.cc b/storage/innobase/row/row0sel.cc
index fcbc9e95559..0f07850f708 100644
--- a/storage/innobase/row/row0sel.cc
+++ b/storage/innobase/row/row0sel.cc
@@ -4738,6 +4738,18 @@ row_search_mvcc(
 			goto next_rec;
 		}
 
+		if (UNIV_UNLIKELY(prebuilt->used_in_HANDLER
+				  && pcur->old_rec == nullptr)) {
+			/* HANDLER..READ IDX NEXT/PREV is continuing
+			from a persistent cursor that holds no stored
+			position: either the preceding HANDLER READ
+			matched no record, or change_active_index()
+			discarded a position that had been stored
+			on a different index. Nothing to restore */
+			err = DB_RECORD_NOT_FOUND;
+			goto normal_return;
+		}
+
 		bool	need_to_process = sel_restore_position_for_mysql(
 			&same_user_rec, BTR_SEARCH_LEAF,
 			pcur, moves_up, &mtr);
