Description:
row_log_table_apply_update() can leak a persistent cursor's saved record buffer while replaying concurrent UPDATE operations during an online table rebuild.
The trigger is an UPDATE that changes a secondary-index key and grows an inline BLOB into an externally stored value in the rebuilt clustered index:
1. The function opens pcur on the clustered index.
2. btr_store_big_rec_extern_fields() writes the external value. The LOB writer calls pcur.store_position(), allocating m_old_rec_buf.
3. The function later passes the same pcur to row_search_index_entry() to remove the old secondary-index entry.
4. row_search_index_entry() calls pcur.open(), which calls init(). init() assigns nullptr to m_old_rec_buf without freeing the existing allocation.
5. The function-level scope guard calls pcur.close() on exit, but the original allocation is no longer reachable.
The scope guard added by commit df130daa135a5c85d2a625ace01b3e2da28e6f4e (Bug#37403052) therefore does not cover this cursor-reuse path. The guard protects function exit, but does not close the previous cursor lifetime before another open().
A synchronized MTR reproduction updates eight rows while ALTER TABLE ... FORCE, ALGORITHM=INPLACE, LOCK=NONE is paused before row-log replay. SQL and data checks pass. On server shutdown, LeakSanitizer reports:
Direct leak of 336 byte(s) in 8 object(s) allocated from:
rec_copy_prefix_to_buf
dict_index_copy_rec_order_prefix
btr_pcur_t::store_position
lob::BtrContext::store_position
lob::BtrContext::check_redolog_normal
lob::btr_store_big_rec_extern_fields
row_log_table_apply_update
SUMMARY: AddressSanitizer: 336 byte(s) leaked in 8 allocation(s).
How to repeat:
Add the following test as mysql-test/suite/innodb/t/row_log_update_pcur_reuse.test. It requires a Debug build with DEBUG_SYNC and a 16 KiB InnoDB page size.
--source include/have_innodb_16k.inc
--source include/have_debug_sync.inc
CREATE TABLE t1 (
id INT PRIMARY KEY,
k INT NOT NULL,
payload LONGBLOB,
KEY k (k)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
INSERT INTO t1 VALUES
(1,1,REPEAT('a',256)), (2,2,REPEAT('a',256)),
(3,3,REPEAT('a',256)), (4,4,REPEAT('a',256)),
(5,5,REPEAT('a',256)), (6,6,REPEAT('a',256)),
(7,7,REPEAT('a',256)), (8,8,REPEAT('a',256));
--connect (ddl,localhost,root,,test)
SET DEBUG_SYNC='row_log_table_apply1_before SIGNAL rebuilt WAIT_FOR updated';
--send ALTER TABLE t1 FORCE, ALGORITHM=INPLACE, LOCK=NONE
--connection default
SET DEBUG_SYNC='now WAIT_FOR rebuilt';
UPDATE t1 SET payload=REPEAT('b',20000), k=k+100;
SET DEBUG_SYNC='now SIGNAL updated';
--connection ddl
--reap
--connection default
--disconnect ddl
SET DEBUG_SYNC='RESET';
SELECT COUNT(*) AS rows_found,
MIN(OCTET_LENGTH(payload)) AS min_len,
MAX(OCTET_LENGTH(payload)) AS max_len,
SUM(payload=REPEAT('b',20000)) AS valid_payloads
FROM t1 FORCE INDEX (k) WHERE k BETWEEN 101 AND 108;
SELECT COUNT(*) AS old_keys FROM t1 FORCE INDEX (k) WHERE k BETWEEN 1 AND 8;
CHECK TABLE t1;
DROP TABLE t1;
Expected SQL results: rows_found=8, min_len=max_len=20000, valid_payloads=8, old_keys=0, CHECK TABLE reports OK.
Run the MTR case with leak detection enabled. The SQL results are correct, but the unfixed server leaks the saved cursor buffers at shutdown.
Suggested fix:
In storage/innobase/row/row0log.cc, replace the function-level scope guard in row_log_table_apply_update() with explicit close calls at the cursor lifetime boundaries:
- Close pcur after the clustered-index update and external-field write, before entering the secondary-index loop.
- Close pcur after a successful ordinary secondary-index deletion, immediately before committing its mtr.
- Close pcur at func_exit_committed, covering the common return path and errors that leave a cursor open.
Description: row_log_table_apply_update() can leak a persistent cursor's saved record buffer while replaying concurrent UPDATE operations during an online table rebuild. The trigger is an UPDATE that changes a secondary-index key and grows an inline BLOB into an externally stored value in the rebuilt clustered index: 1. The function opens pcur on the clustered index. 2. btr_store_big_rec_extern_fields() writes the external value. The LOB writer calls pcur.store_position(), allocating m_old_rec_buf. 3. The function later passes the same pcur to row_search_index_entry() to remove the old secondary-index entry. 4. row_search_index_entry() calls pcur.open(), which calls init(). init() assigns nullptr to m_old_rec_buf without freeing the existing allocation. 5. The function-level scope guard calls pcur.close() on exit, but the original allocation is no longer reachable. The scope guard added by commit df130daa135a5c85d2a625ace01b3e2da28e6f4e (Bug#37403052) therefore does not cover this cursor-reuse path. The guard protects function exit, but does not close the previous cursor lifetime before another open(). A synchronized MTR reproduction updates eight rows while ALTER TABLE ... FORCE, ALGORITHM=INPLACE, LOCK=NONE is paused before row-log replay. SQL and data checks pass. On server shutdown, LeakSanitizer reports: Direct leak of 336 byte(s) in 8 object(s) allocated from: rec_copy_prefix_to_buf dict_index_copy_rec_order_prefix btr_pcur_t::store_position lob::BtrContext::store_position lob::BtrContext::check_redolog_normal lob::btr_store_big_rec_extern_fields row_log_table_apply_update SUMMARY: AddressSanitizer: 336 byte(s) leaked in 8 allocation(s). How to repeat: Add the following test as mysql-test/suite/innodb/t/row_log_update_pcur_reuse.test. It requires a Debug build with DEBUG_SYNC and a 16 KiB InnoDB page size. --source include/have_innodb_16k.inc --source include/have_debug_sync.inc CREATE TABLE t1 ( id INT PRIMARY KEY, k INT NOT NULL, payload LONGBLOB, KEY k (k) ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC; INSERT INTO t1 VALUES (1,1,REPEAT('a',256)), (2,2,REPEAT('a',256)), (3,3,REPEAT('a',256)), (4,4,REPEAT('a',256)), (5,5,REPEAT('a',256)), (6,6,REPEAT('a',256)), (7,7,REPEAT('a',256)), (8,8,REPEAT('a',256)); --connect (ddl,localhost,root,,test) SET DEBUG_SYNC='row_log_table_apply1_before SIGNAL rebuilt WAIT_FOR updated'; --send ALTER TABLE t1 FORCE, ALGORITHM=INPLACE, LOCK=NONE --connection default SET DEBUG_SYNC='now WAIT_FOR rebuilt'; UPDATE t1 SET payload=REPEAT('b',20000), k=k+100; SET DEBUG_SYNC='now SIGNAL updated'; --connection ddl --reap --connection default --disconnect ddl SET DEBUG_SYNC='RESET'; SELECT COUNT(*) AS rows_found, MIN(OCTET_LENGTH(payload)) AS min_len, MAX(OCTET_LENGTH(payload)) AS max_len, SUM(payload=REPEAT('b',20000)) AS valid_payloads FROM t1 FORCE INDEX (k) WHERE k BETWEEN 101 AND 108; SELECT COUNT(*) AS old_keys FROM t1 FORCE INDEX (k) WHERE k BETWEEN 1 AND 8; CHECK TABLE t1; DROP TABLE t1; Expected SQL results: rows_found=8, min_len=max_len=20000, valid_payloads=8, old_keys=0, CHECK TABLE reports OK. Run the MTR case with leak detection enabled. The SQL results are correct, but the unfixed server leaks the saved cursor buffers at shutdown. Suggested fix: In storage/innobase/row/row0log.cc, replace the function-level scope guard in row_log_table_apply_update() with explicit close calls at the cursor lifetime boundaries: - Close pcur after the clustered-index update and external-field write, before entering the secondary-index loop. - Close pcur after a successful ordinary secondary-index deletion, immediately before committing its mtr. - Close pcur at func_exit_committed, covering the common return path and errors that leave a cursor open.