-
Notifications
You must be signed in to change notification settings - Fork 33
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
FrozenMap
unsound if Hash
panics in such a way as to prevent HashMap
rehashing to succeed.
#48
Comments
Ugh, that's unfortunate. I guess FrozenMap would have to use an internal hashmap implementation that stores the hashes or something. (Basically, have Would it be possible to get hashbrown to assign a garbage hash to such entries instead? This would still be correct according to the garbage-in-garbage-out pattern of HashMap behavior when the user writes silly implementations of Hash and Eq. It would be nice if we could rely on the invariant that insert-only maps never drop. |
HashMap::insert
*can* drop values, hence FrozenMap
is unsoundFrozenMap
unsound if Hash
panics in such a way as to prevent HashMap
reallocation from recomputing hashes.
Another way to fix this would be to |
I agree that this is somewhat surprising behavior from Also – though I haven’t quite thought through / experimented with what happens if the same thing is forced (through a silly |
If only one could somehow safely convert |
That may be sound for |
Yeah, one of the problems is that we expose methods that give you access to the underlying hashmap. Unfortunate. Not strongly opposed to changing |
AFAIK that isn’t the case: For example, as far as I’m aware… pub struct HashMap<K, V, S = DefaultHashBuilder, A: Allocator + Clone = Global> {
pub(crate) hash_builder: S,
pub(crate) table: RawTable<(K, V), A>,
} being |
Actually, one way to fix this would be a destructor guard type in Unless there is a chance that other user code can run between the panicky hash and re: transparent: Yeah, so .... given that |
Usually panics don’t get converted in memory leaks, right? But at least I agree this does sound like it would improve soundness. Edit: Actually… no. The values in the map get dropped while unwinding, before |
Oh, wait, yeah, that wouldn't do anything. Never mind. |
By the way, your new title was inaccurate, as the problem only possibly arises on the non-reallocating rehashing ;-) This also makes this issue harder to run into. E.g. the use-case of starting up with an empty |
FrozenMap
unsound if Hash
panics in such a way as to prevent HashMap
reallocation from recomputing hashes.FrozenMap
unsound if Hash
panics in such a way as to prevent HashMap
rehashing to succeed.
It should probably be possible to create self-referencing uses of That means that at the time of catching or drop-bombing the panic leaving the Edit: Here’s a demo: /*
[dependencies]
elsa = "1.8.1"
thread_local = "1.1.7"
*/
use std::{
collections::HashMap,
sync::{Mutex, OnceLock},
};
use elsa::FrozenMap;
use thread_local::ThreadLocal;
#[derive(PartialEq, Eq, Debug)]
struct H(u32);
impl std::hash::Hash for H {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
if PANIC_ON.lock().unwrap().as_ref() == Some(&self.0) {
panic!();
}
0_u32.hash(state);
}
}
struct V(Mutex<Option<&'static str>>);
static PANIC_ON: Mutex<Option<u32>> = Mutex::new(None);
fn main() {
let mut map = HashMap::new();
for i in 1..=28 {
map.insert(H(i), Box::new((String::new(), None)));
}
for i in 1..=27 {
map.remove(&H(i));
}
*map.get_mut(&H(28)).unwrap() = Box::new((
String::from("Hello World!"),
Some(V(Mutex::new(None::<&str>))),
));
let mut map = FrozenMap::from(map);
static MAP: OnceLock<ThreadLocal<FrozenMap<H, Box<(String, Option<V>)>>>> = OnceLock::new();
let map: &'static FrozenMap<_, _> = MAP
.get_or_init(ThreadLocal::new)
.get_or(|| std::mem::take(&mut map));
let last = map.get(&H(28)).unwrap();
let hello_world = &last.0[..];
*last.1.as_ref().unwrap().0.lock().unwrap() = Some(hello_world);
impl Drop for V {
fn drop(&mut self) {
let hello_world = self.0.lock().unwrap().unwrap();
println!("gone: {hello_world}");
}
}
println!("exists: {hello_world}");
*PANIC_ON.lock().unwrap() = Some(28);
map.insert(H(1), Box::new((String::new(), None)));
} |
Ah, so it's basically only when you construct a FrozenMap out of a HashMap that has experienced deletion. Hm. |
Right, or if you modify it using the |
heh, so my preferred fix (cache the hash) has the side effect of removing all of the APIs that cause this problem in the first place, nice. (But i'd really prefer to avoid removing those APIs) |
If we hide away the internal implementation, we can harden it with more UnsafeCells if we want so that #50 goes from being "probably not a problem" to "definitely not a problem" |
Which data structures are affected by this? |
Anything with a hashmap |
It’s a bit tricky to reproduce, but here’s a way that seems to reliably do it, as of now:
(run this code online on rustexplorer.com)
See here and here in the
hashbrown
source-code for more information.The text was updated successfully, but these errors were encountered: