From f05761934b436afd3dff8631876c494b62b35ff3 Mon Sep 17 00:00:00 2001 From: forkfun Date: Tue, 14 Jul 2026 13:48:18 +0200 Subject: [PATCH] MDEV-29510 ASAN errors upon updating a BLOB column twice in one UPDATE UPDATE t SET a='x', b=a, a='foobarqux' makes b alias a's value buffer (field_conv_memcpy() copies the blob pointer while copy_blobs is off). Storing a again frees that buffer, and b is left dangling. The same happens in INSERT ... ON DUPLICATE KEY UPDATE. fill_record(update=true) now stores a blob by value, unless the source is a plain same-type blob column still in engine memory: nothing frees that, so aliasing stays safe and cheap. Field_blob::store_field() copies such blobs verbatim, skipping the charset check. INSERT ... ON DUPLICATE KEY UPDATE keeps the inserted row's blob pointers in insert_values, pointing into Field_blob::value. Keep those buffers aside while the UPDATE clause runs, so that storing into a column does not overwrite what VALUES() reads later. --- mysql-test/main/insert_update.result | 153 +++++++++++++++++++ mysql-test/main/insert_update.test | 41 +++++ mysql-test/main/update.result | 216 +++++++++++++++++++++++++++ mysql-test/main/update.test | 61 ++++++++ sql/field.cc | 10 ++ sql/field.h | 32 +++- sql/sql_base.cc | 46 ++++++ sql/sql_insert.cc | 51 +++++++ 8 files changed, 608 insertions(+), 2 deletions(-) diff --git a/mysql-test/main/insert_update.result b/mysql-test/main/insert_update.result index bfb89449368..589740fa14e 100644 --- a/mysql-test/main/insert_update.result +++ b/mysql-test/main/insert_update.result @@ -472,4 +472,157 @@ insert into t1 set j = (value (j)) on duplicate key update i = default; insert into t1 set j = (value (j)) on duplicate key update i = default; drop table t1; drop sequence s; +# +# MDEV-29510: ASAN errors upon updating a BLOB column twice in one UPDATE +# +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=Aria; +INSERT INTO t VALUES (1,'foobarqux','x'),(2,'baz','qux'); +INSERT INTO t VALUES (1,'z','z'),(2,'z','z') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'foobarqux'; +SELECT * FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +INSERT INTO t VALUES (1,'v1','v2') ON DUPLICATE KEY UPDATE b = a, a = VALUES(a); +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 foobarqux x +INSERT INTO t SELECT 2,'z','z' ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'y'; +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 y x +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t WHERE pk=1; +pk a b +1 xxxxxxxxxxxx v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 x +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=Aria; +CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'v1','v2'); +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t; +pk a b +1 xxxxxxxxxxxx v1 +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=MyISAM; +INSERT INTO t VALUES (1,'foobarqux','x'),(2,'baz','qux'); +INSERT INTO t VALUES (1,'z','z'),(2,'z','z') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'foobarqux'; +SELECT * FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +INSERT INTO t VALUES (1,'v1','v2') ON DUPLICATE KEY UPDATE b = a, a = VALUES(a); +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 foobarqux x +INSERT INTO t SELECT 2,'z','z' ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'y'; +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 y x +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t WHERE pk=1; +pk a b +1 xxxxxxxxxxxx v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 x +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=MyISAM; +CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'v1','v2'); +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t; +pk a b +1 xxxxxxxxxxxx v1 +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=InnoDB; +INSERT INTO t VALUES (1,'foobarqux','x'),(2,'baz','qux'); +INSERT INTO t VALUES (1,'z','z'),(2,'z','z') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'foobarqux'; +SELECT * FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +INSERT INTO t VALUES (1,'v1','v2') ON DUPLICATE KEY UPDATE b = a, a = VALUES(a); +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 foobarqux x +INSERT INTO t SELECT 2,'z','z' ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'y'; +SELECT * FROM t ORDER BY pk; +pk a b +1 v1 foobarqux +2 y x +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t WHERE pk=1; +pk a b +1 xxxxxxxxxxxx v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'x', b = a, a = VALUES(b); +SELECT * FROM t WHERE pk=1; +pk a b +1 v2 x +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=InnoDB; +CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'v1','v2'); +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); +SELECT * FROM t; +pk a b +1 v2 v1 +INSERT INTO t VALUES (1,'v1','v2') +ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); +SELECT * FROM t; +pk a b +1 xxxxxxxxxxxx v1 +DROP TABLE t; # End of 10.11 tests diff --git a/mysql-test/main/insert_update.test b/mysql-test/main/insert_update.test index eb8faad4f6c..9bff792ebc5 100644 --- a/mysql-test/main/insert_update.test +++ b/mysql-test/main/insert_update.test @@ -360,4 +360,45 @@ insert into t1 set j = (value (j)) on duplicate key update i = default; drop table t1; drop sequence s; +--echo # +--echo # MDEV-29510: ASAN errors upon updating a BLOB column twice in one UPDATE +--echo # +let $engine= Aria; +while ($engine) +{ + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=$engine; + INSERT INTO t VALUES (1,'foobarqux','x'),(2,'baz','qux'); + INSERT INTO t VALUES (1,'z','z'),(2,'z','z') + ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'foobarqux'; + SELECT * FROM t ORDER BY pk; + INSERT INTO t VALUES (1,'v1','v2') ON DUPLICATE KEY UPDATE b = a, a = VALUES(a); + SELECT * FROM t ORDER BY pk; + INSERT INTO t SELECT 2,'z','z' ON DUPLICATE KEY UPDATE a = 'x', b = a, a = 'y'; + SELECT * FROM t ORDER BY pk; + INSERT INTO t VALUES (1,'v1','v2') + ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); + SELECT * FROM t WHERE pk=1; + INSERT INTO t VALUES (1,'v1','v2') + ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); + SELECT * FROM t WHERE pk=1; + INSERT INTO t VALUES (1,'v1','v2') + ON DUPLICATE KEY UPDATE a = 'x', b = a, a = VALUES(b); + SELECT * FROM t WHERE pk=1; + DROP TABLE t; + --echo # NOT NULL blobs and a trigger: the row is stored through other fields + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) + ENGINE=$engine; + CREATE TRIGGER tr BEFORE INSERT ON t FOR EACH ROW SET @x=1; + INSERT INTO t VALUES (1,'v1','v2'); + INSERT INTO t VALUES (1,'v1','v2') + ON DUPLICATE KEY UPDATE b = VALUES(a), a = VALUES(b); + SELECT * FROM t; + INSERT INTO t VALUES (1,'v1','v2') + ON DUPLICATE KEY UPDATE a = 'xxxxxxxxxxxx', b = VALUES(a); + SELECT * FROM t; + DROP TABLE t; + let $engine= `SELECT CASE '$engine' WHEN 'Aria' THEN 'MyISAM' + WHEN 'MyISAM' THEN 'InnoDB' ELSE '' END`; +} + --echo # End of 10.11 tests diff --git a/mysql-test/main/update.result b/mysql-test/main/update.result index 5f91415f32e..3f9accdadd0 100644 --- a/mysql-test/main/update.result +++ b/mysql-test/main/update.result @@ -844,4 +844,220 @@ id v 44 444 55 555 drop table t1, t2, t3; +# +# MDEV-29510: ASAN errors upon updating a BLOB column twice in one UPDATE +# +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=Aria; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a = 'x', b = a, a = 'foobarqux'; +SELECT pk, a, b FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB, c BLOB, g TEXT, +h TEXT, vb BLOB AS (CONCAT(a,'!')) VIRTUAL, +tb TINYBLOB) ENGINE=Aria; +INSERT INTO t (pk,a,b,c,g,h) VALUES (1,'foo','bar','baz','tg','th'); +# sources in the engine row buffer +UPDATE t SET b=a, a=b; +UPDATE t SET h=g, g=h; +SELECT pk, a, b, g, h FROM t; +pk a b g h +1 foo foo tg tg +# sources in a field buffer +UPDATE t SET a=CONCAT(a,'x'), b=a, a='y'; +SELECT pk, a, b FROM t; +pk a b +1 y foox +UPDATE t SET b=a, c=b, a=REPEAT('z',300); +SELECT pk, LEFT(a,3), b, c FROM t; +pk LEFT(a,3) b c +1 zzz y y +UPDATE t SET a='m', b=a, c=b, b='n', a='o'; +SELECT pk, a, b, c FROM t; +pk a b c +1 o n m +# virtual column source +UPDATE t SET b=vb, a='longer-than-before'; +SELECT pk, a, b, vb FROM t; +pk a b vb +1 longer-than-before o! longer-than-before! +# target shorter than the source +UPDATE t SET a=REPEAT('a',1000); +UPDATE t SET tb=a, a='y'; +ERROR 22001: Data too long for column 'tb' at row 1 +SELECT pk, LENGTH(a), tb FROM t; +pk LENGTH(a) tb +1 1000 NULL +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=Aria; +CREATE TRIGGER tr BEFORE UPDATE ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a='x', b=a, a=REPEAT('q',300); +SELECT pk, LEFT(a,3), b FROM t; +pk LEFT(a,3) b +1 qqq x +2 qqq x +UPDATE t SET b=a, a=b; +SELECT pk, LEFT(a,3), LEFT(b,3) FROM t; +pk LEFT(a,3) LEFT(b,3) +1 qqq qqq +2 qqq qqq +DROP TABLE t; +# SIMULTANEOUS_ASSIGNMENT +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=Aria; +INSERT INTO t VALUES (1,'foo','bar'); +SET @save_sql_mode= @@sql_mode; +SET sql_mode= CONCAT(@@sql_mode, ',SIMULTANEOUS_ASSIGNMENT'); +UPDATE t SET b=a, a=b; +SELECT * FROM t; +pk a b +1 bar foo +SET sql_mode= @save_sql_mode; +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=MyISAM; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a = 'x', b = a, a = 'foobarqux'; +SELECT pk, a, b FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB, c BLOB, g TEXT, +h TEXT, vb BLOB AS (CONCAT(a,'!')) VIRTUAL, +tb TINYBLOB) ENGINE=MyISAM; +INSERT INTO t (pk,a,b,c,g,h) VALUES (1,'foo','bar','baz','tg','th'); +# sources in the engine row buffer +UPDATE t SET b=a, a=b; +UPDATE t SET h=g, g=h; +SELECT pk, a, b, g, h FROM t; +pk a b g h +1 foo foo tg tg +# sources in a field buffer +UPDATE t SET a=CONCAT(a,'x'), b=a, a='y'; +SELECT pk, a, b FROM t; +pk a b +1 y foox +UPDATE t SET b=a, c=b, a=REPEAT('z',300); +SELECT pk, LEFT(a,3), b, c FROM t; +pk LEFT(a,3) b c +1 zzz y y +UPDATE t SET a='m', b=a, c=b, b='n', a='o'; +SELECT pk, a, b, c FROM t; +pk a b c +1 o n m +# virtual column source +UPDATE t SET b=vb, a='longer-than-before'; +SELECT pk, a, b, vb FROM t; +pk a b vb +1 longer-than-before o! longer-than-before! +# target shorter than the source +UPDATE t SET a=REPEAT('a',1000); +UPDATE t SET tb=a, a='y'; +ERROR 22001: Data too long for column 'tb' at row 1 +SELECT pk, LENGTH(a), tb FROM t; +pk LENGTH(a) tb +1 1000 NULL +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=MyISAM; +CREATE TRIGGER tr BEFORE UPDATE ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a='x', b=a, a=REPEAT('q',300); +SELECT pk, LEFT(a,3), b FROM t; +pk LEFT(a,3) b +1 qqq x +2 qqq x +UPDATE t SET b=a, a=b; +SELECT pk, LEFT(a,3), LEFT(b,3) FROM t; +pk LEFT(a,3) LEFT(b,3) +1 qqq qqq +2 qqq qqq +DROP TABLE t; +# SIMULTANEOUS_ASSIGNMENT +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=MyISAM; +INSERT INTO t VALUES (1,'foo','bar'); +SET @save_sql_mode= @@sql_mode; +SET sql_mode= CONCAT(@@sql_mode, ',SIMULTANEOUS_ASSIGNMENT'); +UPDATE t SET b=a, a=b; +SELECT * FROM t; +pk a b +1 bar foo +SET sql_mode= @save_sql_mode; +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=InnoDB; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a = 'x', b = a, a = 'foobarqux'; +SELECT pk, a, b FROM t ORDER BY pk; +pk a b +1 foobarqux x +2 foobarqux x +DROP TABLE t; +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB, c BLOB, g TEXT, +h TEXT, vb BLOB AS (CONCAT(a,'!')) VIRTUAL, +tb TINYBLOB) ENGINE=InnoDB; +INSERT INTO t (pk,a,b,c,g,h) VALUES (1,'foo','bar','baz','tg','th'); +# sources in the engine row buffer +UPDATE t SET b=a, a=b; +UPDATE t SET h=g, g=h; +SELECT pk, a, b, g, h FROM t; +pk a b g h +1 foo foo tg tg +# sources in a field buffer +UPDATE t SET a=CONCAT(a,'x'), b=a, a='y'; +SELECT pk, a, b FROM t; +pk a b +1 y foox +UPDATE t SET b=a, c=b, a=REPEAT('z',300); +SELECT pk, LEFT(a,3), b, c FROM t; +pk LEFT(a,3) b c +1 zzz y y +UPDATE t SET a='m', b=a, c=b, b='n', a='o'; +SELECT pk, a, b, c FROM t; +pk a b c +1 o n m +# virtual column source +UPDATE t SET b=vb, a='longer-than-before'; +SELECT pk, a, b, vb FROM t; +pk a b vb +1 longer-than-before o! longer-than-before! +# target shorter than the source +UPDATE t SET a=REPEAT('a',1000); +UPDATE t SET tb=a, a='y'; +ERROR 22001: Data too long for column 'tb' at row 1 +SELECT pk, LENGTH(a), tb FROM t; +pk LENGTH(a) tb +1 1000 NULL +DROP TABLE t; +# NOT NULL blobs and a trigger: the row is stored through other fields +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) +ENGINE=InnoDB; +CREATE TRIGGER tr BEFORE UPDATE ON t FOR EACH ROW SET @x=1; +INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); +UPDATE t SET a='x', b=a, a=REPEAT('q',300); +SELECT pk, LEFT(a,3), b FROM t; +pk LEFT(a,3) b +1 qqq x +2 qqq x +UPDATE t SET b=a, a=b; +SELECT pk, LEFT(a,3), LEFT(b,3) FROM t; +pk LEFT(a,3) LEFT(b,3) +1 qqq qqq +2 qqq qqq +DROP TABLE t; +# SIMULTANEOUS_ASSIGNMENT +CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=InnoDB; +INSERT INTO t VALUES (1,'foo','bar'); +SET @save_sql_mode= @@sql_mode; +SET sql_mode= CONCAT(@@sql_mode, ',SIMULTANEOUS_ASSIGNMENT'); +UPDATE t SET b=a, a=b; +SELECT * FROM t; +pk a b +1 bar foo +SET sql_mode= @save_sql_mode; +DROP TABLE t; # End of MariaDB 10.11 tests diff --git a/mysql-test/main/update.test b/mysql-test/main/update.test index fe938dd35f0..3f2d5680ab2 100644 --- a/mysql-test/main/update.test +++ b/mysql-test/main/update.test @@ -746,4 +746,65 @@ select * from t3; drop table t1, t2, t3; +--echo # +--echo # MDEV-29510: ASAN errors upon updating a BLOB column twice in one UPDATE +--echo # +let $engine= Aria; +while ($engine) +{ + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=$engine; + INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); + UPDATE t SET a = 'x', b = a, a = 'foobarqux'; + SELECT pk, a, b FROM t ORDER BY pk; + DROP TABLE t; + + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB, c BLOB, g TEXT, + h TEXT, vb BLOB AS (CONCAT(a,'!')) VIRTUAL, + tb TINYBLOB) ENGINE=$engine; + INSERT INTO t (pk,a,b,c,g,h) VALUES (1,'foo','bar','baz','tg','th'); + --echo # sources in the engine row buffer + UPDATE t SET b=a, a=b; + UPDATE t SET h=g, g=h; + SELECT pk, a, b, g, h FROM t; + --echo # sources in a field buffer + UPDATE t SET a=CONCAT(a,'x'), b=a, a='y'; + SELECT pk, a, b FROM t; + UPDATE t SET b=a, c=b, a=REPEAT('z',300); + SELECT pk, LEFT(a,3), b, c FROM t; + UPDATE t SET a='m', b=a, c=b, b='n', a='o'; + SELECT pk, a, b, c FROM t; + --echo # virtual column source + UPDATE t SET b=vb, a='longer-than-before'; + SELECT pk, a, b, vb FROM t; + --echo # target shorter than the source + UPDATE t SET a=REPEAT('a',1000); + --error ER_DATA_TOO_LONG + UPDATE t SET tb=a, a='y'; + SELECT pk, LENGTH(a), tb FROM t; + DROP TABLE t; + + --echo # NOT NULL blobs and a trigger: the row is stored through other fields + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB NOT NULL, b BLOB NOT NULL) + ENGINE=$engine; + CREATE TRIGGER tr BEFORE UPDATE ON t FOR EACH ROW SET @x=1; + INSERT INTO t VALUES (1,'foo','bar'),(2,'baz','qux'); + UPDATE t SET a='x', b=a, a=REPEAT('q',300); + SELECT pk, LEFT(a,3), b FROM t; + UPDATE t SET b=a, a=b; + SELECT pk, LEFT(a,3), LEFT(b,3) FROM t; + DROP TABLE t; + + --echo # SIMULTANEOUS_ASSIGNMENT + eval CREATE TABLE t (pk INT PRIMARY KEY, a BLOB, b BLOB) ENGINE=$engine; + INSERT INTO t VALUES (1,'foo','bar'); + SET @save_sql_mode= @@sql_mode; + SET sql_mode= CONCAT(@@sql_mode, ',SIMULTANEOUS_ASSIGNMENT'); + UPDATE t SET b=a, a=b; + SELECT * FROM t; + SET sql_mode= @save_sql_mode; + DROP TABLE t; + let $engine= `SELECT CASE '$engine' WHEN 'Aria' THEN 'MyISAM' + WHEN 'MyISAM' THEN 'InnoDB' ELSE '' END`; +} + --echo # End of MariaDB 10.11 tests diff --git a/sql/field.cc b/sql/field.cc index 24a1d37af22..b65a08ffe0a 100644 --- a/sql/field.cc +++ b/sql/field.cc @@ -8807,6 +8807,16 @@ int Field_blob::copy_value(Field_blob *from) } +bool Field_blob::blob_copy_is_verbatim(const Field *from) const +{ + return type() == MYSQL_TYPE_BLOB && from->type() == MYSQL_TYPE_BLOB && + from->pack_length() == Field_blob::pack_length() && + from->charset() == field_charset() && + !compression_method() && !from->compression_method() && + !f_is_hex_escape(flags) && !table->blob_storage; +} + + int Field_blob::store(const char *from,size_t length,CHARSET_INFO *cs) { DBUG_ASSERT(marked_for_write_or_computed()); diff --git a/sql/field.h b/sql/field.h index ff9ff071e1c..b453299621e 100644 --- a/sql/field.h +++ b/sql/field.h @@ -4517,6 +4517,15 @@ public: return do_copy_blob; return get_identical_copy_func(); } + /* Is 'data' in the buffer of 'value' (overwritten or freed by a store) */ + bool in_value(const char *data) const + { + return value.is_alloced() && data >= value.ptr() && + data < value.ptr() + value.alloced_length(); + } + bool data_in_value() const { return in_value((const char*) get_ptr()); } + /* Is 'from' a blob that is stored as is, without conversion or truncation */ + bool blob_copy_is_verbatim(const Field *from) const; int store_field(Field *from) override { // Be sure the value is stored if (field_charset() == &my_charset_bin && @@ -4527,9 +4536,28 @@ public: value.copy(tmp.ptr(), tmp.length(), &my_charset_bin); return store(value.ptr(), value.length(), &my_charset_bin); } + if (table->copy_blobs) + { + /* 'value' is the copy target: read the source into a local String */ + String tmp; + from->val_str(&tmp); + if (tmp.length() && blob_copy_is_verbatim(from)) + { + /* The data is already well-formed and fits: skip the charset check */ + DBUG_ASSERT(marked_for_write_or_computed()); + DBUG_ASSERT(!in_value(tmp.ptr())); + if (value.copy(tmp.ptr(), tmp.length(), &my_charset_bin)) + { + bzero(ptr, Field_blob::pack_length()); + return -1; + } + set_ptr(value.length(), (uchar*) value.ptr()); + return 0; + } + return store(tmp.ptr(), tmp.length(), from->charset()); + } from->val_str(&value); - if (table->copy_blobs || - (!value.is_alloced() && from->is_varchar_and_in_write_set())) + if (!value.is_alloced() && from->is_varchar_and_in_write_set()) value.copy(); return store(value.ptr(), value.length(), from->charset()); } diff --git a/sql/sql_base.cc b/sql/sql_base.cc index eabad77012a..76171c7338a 100644 --- a/sql/sql_base.cc +++ b/sql/sql_base.cc @@ -9115,6 +9115,46 @@ static void unwind_stored_field_offsets(const List &fields, Item_field *en ******************************************************************************/ +/* + Blob assignments in UPDATE alias the source buffer. That breaks for + SET a=.., b=a, a=.. : b points into a's buffer, which storing a again + overwrites or frees. + The guard makes the assignments of the SET list that need it store by value. +*/ +class Copy_blobs_guard +{ + TABLE *table; + bool saved, active; +public: + Copy_blobs_guard(TABLE *table_arg, bool active_arg) + : table(table_arg), saved(table_arg->copy_blobs), active(active_arg) {} + void set(bool copy) { if (active) table->copy_blobs= saved || copy; } + void restore() { table->copy_blobs= saved; } + ~Copy_blobs_guard() { restore(); } +}; + + +/* + A blob taken as is from a column of a row read from the engine points into + engine memory, which stays valid until the row is written. Anything else + may point into a buffer owned by a field, or needs conversion, and must be + copied. +*/ +static bool blob_source_needs_copy(Field *to, Item *value) +{ + if (!(to->flags & BLOB_FLAG)) + return false; + Item *src= value->real_item(); + if (src->type() != Item::FIELD_ITEM) + return true; + Field *from= static_cast(src)->result_field; + Field_blob *to_blob= static_cast(to); + /* blob_copy_is_verbatim() makes 'from' a Field_blob for the cast below */ + return !to_blob->blob_copy_is_verbatim(from) || from->vcol_info || + static_cast(from)->data_in_value(); +} + + /** Fill the fields of a table with the values of an Item list @@ -9150,6 +9190,8 @@ fill_record(THD *thd, TABLE *table_arg, List &fields, List &values, bool save_abort_on_warning= thd->abort_on_warning; bool save_no_errors= thd->no_errors; DBUG_ENTER("fill_record"); + Copy_blobs_guard copy_blobs_guard(table_arg, + update && table_arg->s->blob_fields); thd->no_errors= ignore_errors; /* @@ -9190,6 +9232,7 @@ fill_record(THD *thd, TABLE *table_arg, List &fields, List &values, { if (!skip_sys_field) { + copy_blobs_guard.set(blob_source_needs_copy(rfield, value)); if (value->save_in_field(rfield, 0) < 0 && !ignore_errors) { my_message(ER_UNKNOWN_ERROR, ER_THD(thd, ER_UNKNOWN_ERROR), MYF(0)); @@ -9208,6 +9251,9 @@ fill_record(THD *thd, TABLE *table_arg, List &fields, List &values, } } + /* Virtual and default fields are filled after the SET list: no copies. */ + copy_blobs_guard.restore(); + if (update && thd->variables.sql_mode & MODE_SIMULTANEOUS_ASSIGNMENT) unwind_stored_field_offsets(fields, NULL); diff --git a/sql/sql_insert.cc b/sql/sql_insert.cc index c8ad6d4a267..ffa5c4e0818 100644 --- a/sql/sql_insert.cc +++ b/sql/sql_insert.cc @@ -2169,6 +2169,54 @@ int Write_record::replace_row(ha_rows *inserted, ha_rows *deleted) } +/* + insert_values keeps the blob pointers of the row being inserted. They point + into the buffers of Field_blob::value, which storing into the fields in the + UPDATE clause would overwrite or free. Keep the buffers aside while the + update runs. With BEFORE triggers, NOT NULL columns are stored through the + fields of TABLE::field_to_fill(), so their buffers are kept too. +*/ +class Insert_values_blobs +{ + TABLE *table; + String *held; + bool oom; +public: + Insert_values_blobs(TABLE *table_arg) + : table(table_arg), held(NULL), oom(false) + { + uint n= table->s->blob_fields; + if (!n) + return; + if (!(held= (String*) my_malloc(PSI_INSTRUMENT_ME, 2 * n * sizeof(String), + MYF(MY_WME)))) + { + oom= true; + return; + } + Field **fill= table->field_to_fill(); + for (uint i= 0; i < 2 * n; i++) + ::new (&held[i]) String; + for (uint i= 0; i < n; i++) + { + uint fn= table->s->blob_field[i]; + static_cast(table->field[fn])->swap(held[i], false); + if (fill[fn] != table->field[fn]) + static_cast(fill[fn])->swap(held[n + i], false); + } + } + ~Insert_values_blobs() + { + for (uint i= 0; held && i < 2 * table->s->blob_fields; i++) + held[i].~String(); + my_free(held); + } + Insert_values_blobs(const Insert_values_blobs&)= delete; + Insert_values_blobs &operator=(const Insert_values_blobs&)= delete; + bool failed() const { return oom; } +}; + + int Write_record::insert_on_duplicate_update(ha_rows *inserted, ha_rows *updated) { @@ -2188,6 +2236,9 @@ int Write_record::insert_on_duplicate_update(ha_rows *inserted, */ DBUG_ASSERT(table->insert_values != NULL); store_record(table,insert_values); + Insert_values_blobs insert_values_blobs(table); + if (unlikely(insert_values_blobs.failed())) + return restore_on_error(); restore_record(table,record[1]); table->reset_default_fields();