[MDEV-33383] fts query crashes in fts_query_calculate_ranking() Created: 2024-02-05 Updated: 2024-02-08 |
|
| Status: | In Review |
| Project: | MariaDB Server |
| Component/s: | Full-text Search, Storage Engine - InnoDB |
| Affects Version/s: | 10.5.23 |
| Fix Version/s: | 10.5, 10.6, 10.11, 11.0, 11.1, 11.2, 11.3, 11.4 |
| Type: | Bug | Priority: | Critical |
| Reporter: | Shaohua Wang | Assignee: | Thirunarayanan Balathandayuthapani |
| Resolution: | Unresolved | Votes: | 1 |
| Labels: | None | ||
| Environment: |
linux |
||
| Issue Links: |
|
||||||||
| Description |
|
Description: #0 0x00007f9d2b85b387 in raise () from /lib64/libc.so.6 Suggested fix: /** Compare two fts_ranking_t doc_ids. return ((int)(rk1->doc_id - rk2->doc_id)); --wrong!!! It's totally wrong to compare two integers as below, no matter signed or |
| Comments |
| Comment by Marko Mäkelä [ 2024-02-05 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I see that fts_ranking_doc_id_cmp() is only used in one compilation unit (always passed as a function pointer to rbt_create(), yet defined in another. It does not seem to make sense to specify a function pointer as inline. The pointer will always be assigned to ib_rbt_tree::compare. The other big problem that I see there is that fts_ranking_t::doc_id is doc_id_t, which is a 64-bit unsigned integer. Comparison by subtraction is completely fine, but truncating a 64-bit result to a 32-bit int definitely is not. If the difference is 1U<<31 or more, the return value would be incorrect. The function comment does not promise to normalize the result to -1, 0, 1, and the callers in ut0rbt.cc are checking the result for negative, positive, or 0. An answer in https://stackoverflow.com/questions/66844378/fast-saturating-integer-conversion suggests an interesting trick for converting uint64_t to uint32_t without any conditional branches. Any x > ~0U would be converted to ~0U:
This is almost what we need here. We would want to convert signed integers in a similar way. In C+26 there would be saturate_cast, but we will be stuck to C+11 for quite a while. I am sure that the above can be tweaked for 2’s complement 64-bit arithmetics. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-02-05 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
The line numbers in the stack trace do not match mariadb-10.5.23, but I assume that the code is the following:
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-02-06 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I think that I figured out how to implement a signed saturating conversion using 2’s complement arithmetics. My trick is to temporarily convert negative values to non-negative ones, and then to apply a saturating unsigned conversion to 1 bit less, so that we can restore the sign bit. I first implemented it for 16-to-8-bit conversion so that I could test the full range:
Then I ported this to 64-bit and 32-bit integers and tested a few values:
A translation of the 64-to-32-bit signed saturating convresion to the AMD64 ISA is not that long, and it could be at least 2 instructions less when inlined:
I see that a saturating conversion is missing in fts_trx_row_doc_id_cmp(), fts_ranking_doc_id_cmp(), and fts_doc_id_cmp(). The latter is only being passed to ib_vector_sort(), which is an inline wrapper for qsort(3), which indeed expects an int comparison result. Can you post a pull request for fixing all these? I think that the call ib_vector_sort(doc_ids, fts_doc_id_cmp) deserves to be a non-inline function by itself, because it is being called from 3 different compilation units. Or it should be replaced with simple std::sort(), which would allow an inline comparison to be used. As an alternative fix, we could replace the homebrew red-black tree rbt_ with std::map and the homebrew ib_vector with std::vector. It could make debugging easier too. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-02-06 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Note: In the above I assumed that the difference of the doc_id values would always be less than 1ULL << 63. If we used std::map and std::sort and let them compare the unsigned 64-bit values directly, these problems would not exist. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-02-06 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
I played a bit with https://godbolt.org to find out that the following type of code would compile to conditional ALU instructions on ARMv7, ARMv8, IA-32 and AMD64, already starting with GCC 4.8.5, which is the oldest compiler that we currently target:
Only for POWER, even the most recent GCC would generate conditional branches. I guess that it is an ISA limitation. As an alternative, I will try to see if we can simply replace qsort with std::sort and the homebrew red-black tree with std::map. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Comment by Marko Mäkelä [ 2024-02-06 ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Replacing the ib_rbt_t with C++11 std::multimap turns out to be a quite intrusive change. I reviewed and each comparison function that is passed to rbt_create(). One more strange one is fts_query_compare_rank(), which never returns a negative value when the ranks are equal and the doc_id are inequal. According to my interpretation of the function comment, it should return negative when r1->doc_id < r2->doc_id. This should affect the result of ha_innobase::ft_read(). |