MDEV-27562 MSAN use-of-uninitialized-value in cmp_buffer_with_ref - #5648
Open
arcivanov wants to merge 1 commit into
Open
MDEV-27562 MSAN use-of-uninitialized-value in cmp_buffer_with_ref#5648arcivanov wants to merge 1 commit into
arcivanov wants to merge 1 commit into
Conversation
`cmp_buffer_with_ref()` decides whether an `eq_ref` lookup can reuse the row the previous lookup fetched, and it decides it by comparing the whole ref key buffer with `memcmp()`. A key part over a `VARCHAR` is a fixed width image, so the bytes that follow the value are compared as well, although they hold no information. `store_key` fills that buffer through `field_conv()`, which for two identical `VARCHAR` fields takes the `field_conv_memcpy()` shortcut and copies `pack_length()` bytes. The bytes after the value are therefore whatever the source row carried there, and a storage engine is entitled to leave that part of a record undefined. InnoDB marks it so in `row_sel_field_store_in_mysql_format_func()` and Aria in `_ma_read_block_record2()`, and a MemorySanitizer build then aborts the server inside the comparison. The sibling path is already clean. `store_key_field` copies through `Copy_field`, which for the same pair of fields picks `do_varstring2_no_truncation()` and writes only the length prefix and the value, leaving the rest as the preceding `bzero()` left it. The comparison is a shortcut rather than a correctness decision: a difference in those bytes only causes an already fetched row to be fetched again. Mark them defined, which is what `Count_distinct_field::add()` already does for the same reason. The field the marking is applied to describes the key part rather than the column, because `copy_keys_from_share()` gives a key part over a prefix its own field narrowed to the key part's length, so the range marked is the slot inside the key buffer and not the width of the column. `Field::mark_unused_memory_as_defined()` is an empty inline unless the server is built with memory instrumentation, so a production build is unchanged. The test reaches the comparison through a multi-table update whose condition is cached, through a correlated subquery, which lands at a different offset in the same buffer, and once more with the outer table on Aria, so that both engines named above are covered. Values of differing length are used so that the bytes after a short value are the ones a longer value left behind. The column has to be wider than 255 bytes for the full width copy to be chosen, since a key field always carries a two byte length prefix and `memcpy_field_possible()` requires the source to agree. No build without memory instrumentation can fail on the read, so each block also asserts that the subquery cache counters moved, which is what shows the comparison was reached at all. Only whether they moved is asserted, because a prepared statement executes the subquery twice and doubles them. MDEV-32436 reports the correlated shape. The Valgrind errors it also reports come from InnoDB's insert path and are a separate matter.
arcivanov
force-pushed
the
MDEV-27562
branch
from
September 9, 2026 01:27
e882fdd to
b0b8513
Compare
This was referenced Sep 9, 2026
gkodinov
approved these changes
Sep 9, 2026
gkodinov
left a comment
Member
There was a problem hiding this comment.
LGTM. Thank you!
Please stay tuned for the final review.
Member
|
FYI: According to our development cycle we work on bugs In the following periods 15 Mar-30 Apr, 15 Jun-30 Jul, 15 Sep-30 Oct and 15 Dec-31 Jan. So, please, expect to get a review somewhere between these two dates and the goal is to have your PR merged before the second date |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
cmp_buffer_with_ref()decides whether aneq_reflookup can reuse the row the previous lookup fetched, and it decides it by comparing the whole ref key buffer withmemcmp(). A key part over aVARCHARis a fixed width image, so the bytes that follow the value are compared as well, although they hold no information.store_keyfills that buffer throughfield_conv(), which for two identicalVARCHARfields takes thefield_conv_memcpy()shortcut and copiespack_length()bytes. The bytes after the value are therefore whatever the source row carried there, and a storage engine is entitled to leave that part of a record undefined. InnoDB marks it so inrow_sel_field_store_in_mysql_format_func()and Aria in_ma_read_block_record2(), and a MemorySanitizer build then aborts the server inside the comparison.The sibling path is already clean:
store_key_fieldcopies throughCopy_field, which for the same pair of fields picksdo_varstring2_no_truncation()and writes only the length prefix and the value, leaving the rest as the precedingbzero()left it.cmp_buffer_with_ref()is the only consumer that treats a key buffer as an opaque byte range rather than parsing it by key part, which is why one call site is enough.The fix
The comparison is a shortcut rather than a correctness decision: a difference in those bytes only causes an already fetched row to be fetched again. The change marks them defined, which is what
Count_distinct_field::add()already does for the same reason.Field::mark_unused_memory_as_defined()is an empty inline unless the server is built with memory instrumentation, so a production build is unchanged.The marked range is the key part's slot, not the column's width.
copy_keys_from_share()gives a key part built over a prefix its own field narrowed to the key part's length, sostore_key's field already describes the slot; aKEY k (a(10))over aVARCHAR(256)reportsfield_length10, not 256.Measured
Measured on an MSAN build of this branch, using the
amd64-msan-clang-20builder's cmake line insidequay.io/mariadb-foundation/bb-worker:debian12-msan-clang-20, with only this change differing. MSAN halts on the first report, so each block below is a separate test file.use-of-uninitialized-value in bcmp, offset 6 inside a 259 byte buffer, poisoned byrow_sel_field_store_in_mysql_format_func()_ma_read_block_record2()main,heap,mariaunder MSANThe two offsets match the offsets in the two reports.
No build without memory instrumentation can fail on the read, so each block also checks that the subquery cache counters moved; without that the test passes with
subquery_cache=offand covers nothing. Only whether they moved is checked, because a prepared statement executes the subquery twice and doubles them. The column has to be wider than 255 bytes: a key field always carries a two byte length prefix andField_varstring::memcpy_field_possible()requires the source to agree, so a narrower column takes a conversion path that never copies the bytes in question.Scope
This is the MSAN half of MDEV-32436 as well. The Valgrind errors that ticket also reports come from InnoDB's insert path (
rec_convert_dtuple_to_rec_*<-row_ins_*<-ha_innobase::write_row) and are a separate matter.Two neighbouring sites build key buffers the same way and are unaffected. In
opt_subselect.ccthe field created for the weedout key is assigned and never read, so nothing is copied through it. The GROUP BYgroup_buffreally is filled by the same full width copy, but it is handed toha_index_read_map(), and both engines parse the two byte length prefix and read no further - HEAP inhp_hashnr(), Aria in_ma_pack_key().Targeted at 10.11 because the code is identical there and reachability was measured on it, not taken from the ticket's Affects Versions.