-
-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[BUG] Segmentation fault after move assign of map with different allocator #79
Comments
Hi @tokiura, thanks a lot for finding this, especially the reproducer! I'll merge a fix soon. |
m_buckets can only be reused when the allocators are equal. Otherwise we would deallocate with m_buckets with the incorrect allocator. Also bumped the version for the fix.
Fixed in release v4.0.2. |
First of all, thanks for the quick fix and release! But. I am sorry to say that I have found an omission in my test case. I have added the following pattern based on your added tests. {
int_str_map map_default_resource_construct(
return_hello_world(&pool), ANKERL_UNORDERED_DENSE_PMR::new_delete_resource());
std::cout << map_default_resource.contains(0) << std::endl;
}
https://gcc.godbolt.org/z/f8a54rYYP Also, and this may be my misunderstanding, there may be a possible memory leak in fix a9893b3. After the buckets are copied, other.m_num_buckets is set 0. m_num_buckets = std::exchange(other.m_num_buckets, 0); So it seems that the second argument of the following deallocate is always set to zero during the "other" destruction. ~table() {
if (nullptr != m_buckets) {
auto ba = bucket_alloc(m_values.get_allocator());
bucket_alloc_traits::deallocate(ba, m_buckets, bucket_count()); // bucket_count() is zero after move assignment
}
} However, Valgrind did not find any leaks. I could not determine if there was actually no problem or if it was just a coincidence that it did not leak. |
thanks, on a second look I saw that as well. Also it doesn't work yet for |
I've got another update, @tokiura could you try the version from the PR #81: https://raw.githubusercontent.com/martinus/unordered_dense/2023-07-fix-more-move-allocator-issues/include/ankerl/unordered_dense.h |
unordered_dense/include/ankerl/unordered_dense.h Lines 1232 to 1238 in a9893b3
I think the exchange should only be when moving. When copying buckets, there should be copying of parameters, as here: unordered_dense/include/ankerl/unordered_dense.h Lines 1205 to 1214 in a9893b3
|
Thank you, @martinus. So I checked again with a simple case. for (int i=0; i<10000000; i++)
map_default_resource = return_hello_world(&pool); (In this case, the memory pool increases monotonically, but is eventually released by the destructor of the pool allocator, so Valgrind says "no leaks are possible".) I think @phprus ' suggestion is one possible solution. I have confirmed that simply copying "other" eliminates memory leaks. auto operator=(table&& other) noexcept(
noexcept(std::is_nothrow_move_assignable_v<value_container_type>&& std::is_nothrow_move_assignable_v<Hash>&&
std::is_nothrow_move_assignable_v<KeyEqual>)) -> table& {
if (&other != this) {
deallocate_buckets(); // deallocate before m_values is set (might have another allocator)
m_values = std::move(other.m_values);
// we can only reuse m_buckets over when both maps have the same allocator!
if (get_allocator() == other.get_allocator()) {
m_buckets = std::exchange(other.m_buckets, nullptr);
m_num_buckets = std::exchange(other.m_num_buckets, 0);
m_max_bucket_capacity = std::exchange(other.m_max_bucket_capacity, 0);
} else {
copy_buckets(other);
// Deallocate bucket to avoid memory leak.
// m_num_buckets and m_max_bucket_capacity of other are set simultaneously.
other.deallocate_buckets();
}
m_max_load_factor = std::exchange(other.m_max_load_factor, default_max_load_factor);
m_hash = std::exchange(other.m_hash, {});
m_equal = std::exchange(other.m_equal, {});
m_shifts = std::exchange(other.m_shifts, initial_shifts);
other.m_values.clear();
}
return *this;
} Both work fine. |
Ah, it took me a long time now to see how that really is a memory leak. Thanks again for the reproducer! The issue is jus this line m_num_buckets = std::exchange(other.m_num_buckets, 0); The problem was that the destructor uses I've updated the code, I believe all these problems errors are now fixed. Please try again: |
I too have confirmed that all issues have been resolved. |
I'll release version 4.0.4 with the fix. |
A move assignment of map_A, which uses allocator A, to map_B, which uses allocator B, terminates the program by SIGSEGV.
Here is simple code to reproduce it on Compiler Explorer. (Exits with double free or corruption)
https://gcc.godbolt.org/z/hGh8v1xfM
This is probably because the move assignment (or move constructor) replaces the m_bucket with m_bucket of the "other".
unordered_dense/include/ankerl/unordered_dense.h
Lines 1223 to 1224 in e88dd1c
The default behavior of std::vector move assignment is not to propagate the allocator. So, if the allocator of m_values is different from the one allocating other.m_backet, it appears that subsequent allocate() and deallocate() for m_backet will not work correctly.
The text was updated successfully, but these errors were encountered: