From 6be737413389a9213c6019eb68b37f44f44b3ecb Mon Sep 17 00:00:00 2001 From: Brandon Nesterenko Date: Mon, 6 Apr 2026 12:53:25 -0600 Subject: [PATCH] MDEV-38830: SIGSEGV and UBSAN null-pointer-use in TABLE::evaluate_update_default_function on UPDATE MDEV-38716 fixes were incomplete. It still allowed a table's default_fields to be left un-restored after an ALTER table finished its copy_data_between_fields(). This patch actually matches the original conception of MDEV-38716. It was later changed after finding a test failure in maria.alter, where I thought the patch broke that test. But actually, maria.alter itself relies on somewhat broken/inconsistent server behavior. It is the MDEV-19055 extension of the test which broke. To summarize the broken part of the test, first, it creates a temporary table t2, adds a new column of type DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, and adds a constraint check on that new type: CREATE TEMPORARY TABLE t2 (a TIME) ENGINE=Aria; ALTER TABLE t2 ADD b DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; ALTER TABLE t2 ADD CHECK (b = 4); The next part results in inconsistent behavior: INSERT IGNORE INTO t2 () VALUES (),(),(),(); What is not defined is "what should this do"? Prior to this patch / MDEV-38716, this would create 4 new rows, each with a zeroed out timestamp for `b`. This is due to a the default_field not resetting after the ALTER TABLE, thus the DEFAULT CURRENT_TIMESTAMP clause is lost after the ALTER TABLE ADD CHECK. With this patch, because the default_field is restored after the ALTER, there is no affect of the INSERT IGNORE INTO t2. I.e., t2 is empty after this insert. This change in results further changes how an ALTER TABLE behaves later in the test: SET SQL_MODE= 'STRICT_ALL_TABLES'; SELECT count(a),sum(a) FROM t2; --error ER_TRUNCATED_WRONG_VALUE ALTER TABLE t2 CHANGE IF EXISTS d c INT; Without this patch, there are rows in t2, and so this ALTER TABLE results in an error. With this patch, t2 is empty, and therefore there is no data that was truncated to warn about. Also note, the result of INSERT IGNORE INTO t2 () VALUES (),(),(),(); is very inconsistent overall. For example, if t2 is a regular table (rather than a temporary table), the pre-patch result is also NULL. However, if values are explicitly provided (in any case), the result is truncated and rows are added to the table. E.g. INSERT IGNORE INTO t2 (b) VALUES (1),(2),(3),(5); will result in 4 rows with truncated timestamps in any case. See also MDEV-30115 for more problems with INSERT IGNORE and CHECK. Reviewed-by: Monty Signed-off-by: Brandon Nesterenko --- mysql-test/main/temp_table.result | 11 +++++++++ mysql-test/main/temp_table.test | 12 +++++++++ mysql-test/suite/maria/alter.result | 3 +-- mysql-test/suite/maria/alter.test | 1 - sql/sql_table.cc | 38 ++++++++++++----------------- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/mysql-test/main/temp_table.result b/mysql-test/main/temp_table.result index 75f9cd1eed6a6..a4f01fbdad830 100644 --- a/mysql-test/main/temp_table.result +++ b/mysql-test/main/temp_table.result @@ -746,3 +746,14 @@ SELECT * FROM tmp_38716_2 where b=20; a b c NULL 20 21 DROP TABLE tmp_38716_2; +# +# MDEV-38830 +# +CREATE TEMPORARY TABLE tmp_38830 (c INT KEY); +INSERT INTO tmp_38830 VALUES (1); +ALTER TABLE tmp_38830 ADD COLUMN (d TIMESTAMP ON UPDATE CURRENT_TIMESTAMP); +UPDATE tmp_38830 SET c=c+1; +select * from tmp_38830; +c d +2 2001-01-01 10:20:30 +DROP TEMPORARY TABLE tmp_38830; diff --git a/mysql-test/main/temp_table.test b/mysql-test/main/temp_table.test index ea6201c45ab7e..fd4a93fbbfad0 100644 --- a/mysql-test/main/temp_table.test +++ b/mysql-test/main/temp_table.test @@ -810,3 +810,15 @@ INSERT INTO tmp_38716_2 (b) VALUES (20); SELECT * FROM tmp_38716_2 where b=20; DROP TABLE tmp_38716_2; + + +--echo # +--echo # MDEV-38830 +--echo # +CREATE TEMPORARY TABLE tmp_38830 (c INT KEY); +INSERT INTO tmp_38830 VALUES (1); +ALTER TABLE tmp_38830 ADD COLUMN (d TIMESTAMP ON UPDATE CURRENT_TIMESTAMP); +UPDATE tmp_38830 SET c=c+1; +select * from tmp_38830; + +DROP TEMPORARY TABLE tmp_38830; diff --git a/mysql-test/suite/maria/alter.result b/mysql-test/suite/maria/alter.result index 36d7ae9c302b5..06282f6690deb 100644 --- a/mysql-test/suite/maria/alter.result +++ b/mysql-test/suite/maria/alter.result @@ -63,7 +63,7 @@ DELETE FROM t2 ORDER BY c LIMIT 1; INSERT IGNORE INTO t2 SELECT * FROM t2; OPTIMIZE TABLE t2; Table Op Msg_type Msg_text -test.t2 optimize status OK +test.t2 optimize status Table is already up to date SELECT count(a),sum(a) FROM t2; count(a) sum(a) 0 NULL @@ -73,7 +73,6 @@ SELECT count(a),sum(a) FROM t2; count(a) sum(a) 0 NULL ALTER TABLE t2 CHANGE IF EXISTS d c INT; -ERROR 22007: Truncated incorrect datetime value: '4' SELECT count(a),sum(a) FROM t2; count(a) sum(a) 0 NULL diff --git a/mysql-test/suite/maria/alter.test b/mysql-test/suite/maria/alter.test index a68b5f2e0d721..2c18d6508c061 100644 --- a/mysql-test/suite/maria/alter.test +++ b/mysql-test/suite/maria/alter.test @@ -64,7 +64,6 @@ SELECT count(a),sum(a) FROM t2; INSERT IGNORE INTO t2 SELECT * FROM t2; SET SQL_MODE= 'STRICT_ALL_TABLES'; SELECT count(a),sum(a) FROM t2; ---error ER_TRUNCATED_WRONG_VALUE ALTER TABLE t2 CHANGE IF EXISTS d c INT; SELECT count(a),sum(a) FROM t2; ALTER IGNORE TABLE t2 ADD IF NOT EXISTS e BIT; diff --git a/sql/sql_table.cc b/sql/sql_table.cc index c0795cb2ab562..15a9d36b8d573 100644 --- a/sql/sql_table.cc +++ b/sql/sql_table.cc @@ -13073,16 +13073,14 @@ copy_data_between_tables(THD *thd, TABLE *from, TABLE *to, for new fields added to the table (i.e those which should get new default values). Now that the data copy is done, the to->default_field list needs to be restored to include all fields that have default values to fill. - - First, if to->default_field is NULL, it means it was nullified to prevent - trying to update any default values during the copy process because there - were no new fields added with default values to fill in. The list needs to - be restored to the original location so future records in this session can - get default values. - - Then, because to->default_field was reset and now only considers new - fields, it is missing fields that have ON UPDATE clauses (i.e. TIMESTAMP - and DATETIME). So add those back in. + Either: + 1) to->default_field is NULL, in which case either dfield_ptr is also + NULL or the pre-alter table has fields with default values and the + alter doesn't add any new fields with default values. In either case, + simply restore the original default_field list. + 2) Otherwise, to->default_field has been modified in-place and needs to + be manually restored. I.e., it is missing fields that have ON UPDATE + clauses (i.e. TIMESTAMP and DATETIME). So add those back in. Notes: * The code is similar to the dfield_ptr modification earlier in the @@ -13091,19 +13089,15 @@ copy_data_between_tables(THD *thd, TABLE *from, TABLE *to, data, which requires the table's default_fields to be set up */ if (!to->default_field) + { to->default_field= dfield_ptr; -#ifndef DBUG_OFF + } else { /* - If to->default_field exists, dfield_ptr should point to the NULL list - terminator in the default_field list. + to->default_list was modified in-place, so we need to check to manually + add back missing fields. */ - DBUG_ASSERT(dfield_ptr && !*dfield_ptr); - } -#endif - if (dfield_ptr) - { it.rewind(); for (Field **ptr= to->field; *ptr; ptr++) { @@ -13111,10 +13105,10 @@ copy_data_between_tables(THD *thd, TABLE *from, TABLE *to, if (def->field && def->field->has_update_default_function()) *(dfield_ptr++)= *ptr; } - if (dfield_ptr == to->default_field) - to->default_field= NULL; // No default fields - else - *dfield_ptr= NULL; // Mark end of default field pointers + /* + Mark end of list + */ + *dfield_ptr= NULL; } #ifdef HAVE_REPLICATION