Repository navigation
This release fixes a number of soundness holes, mostly identified by Claude Fable 5.1 and GPT-6 Astra, plus Google's unsafe_rust_review_experimental agent skill. All identified soundness holes require significantly contrived code, e.g. a Hash impl that stashes the passed-in reference into a thread-local or internal Cell.
Overall, iddqd now has significantly less unsafe code than before, though due to Rust compiler limitations it asks slightly more of trait implementers (such as IdOrdItem::Key now requiring Hash for change detection). We hope to relax these requirements in the future as the Rust compiler improves.
Thanks to the authors of the Google agent skill.
Added
-
debug_with_keysmethods onIdOrdMap,IdHashMap,BiHashMap, andTriHashMap. These return a value whoseDebugoutput is the previous{key: item, ...}form, and require the key types to beDebugfor the lifetime of the borrow. -
IdOrdMap'sIntoIternow implementsExactSizeIteratorandFusedIterator, matching the other maps' owning iterators.
Changed
-
Breaking: The mutable-borrow lookup methods now take the key by value, as
T::Key<'_>, rather than anyQ: Equivalent<T::Key<'_>>(orComparable). This affects:IdHashMapandIdOrdMap:get_mutandremove.BiHashMap:get1_mut,get2_mut,remove1,remove2,get_mut_unique, andremove_unique.TriHashMap:get1_mut,get2_mut,get3_mut,remove1,remove2,remove3,get_mut_unique, andremove_unique.
The shared-borrow lookups (
get,contains_key, and their numbered variants) still accept anyQ.See the Mutable lookups take owned keys section in the crate docs for more information, and the "Fixed" entry below for the soundness hole this closes.
-
The
Debugimpl forIdHashMapno longer requiresS: Clone + BuildHasher, matchingBiHashMapandTriHashMap. -
The
Debugimpls forIdOrdMap,IdHashMap,BiHashMap, andTriHashMapnow format items only, as a set ({item, ...}), and require justT: Debug. Previously they formatted{key: item, ...}and also required the key types to beDebug. Usedebug_with_keysfor the previous form. TheDebugimpls for thedaftDiffandMapLeaftypes likewise no longer require the key types to beDebug. -
Breaking:
IdOrdItem::Keynow requiresHashin addition toOrd. AnyHashimpl that is generic over the key lifetime, including derived ones, satisfies the new bound.IdOrdMap'sRefMuthas always neededHashto detect key changes, but the bound used to be on each method that hands out aRefMut. Moving it onto the trait removes those per-method bounds.As a result,
RefMut::reborrowno longer requires the item type to be'static. (The 0.3.10 changelog claimed this already worked, but it did not.)
Fixed
-
Fixed a soundness hole in the mutable-borrow lookup methods listed under "Changed". Their signatures, such as
fn get_mut<'a, Q: Equivalent<T::Key<'a>>>(&'a mut self, key: &Q), let caller code copy a reference out of that key into aCell<Option<&'a str>>and read it after the map had mutated or dropped the item.These APIs have been changed to take
T::Key<'_>directly, which closes this soundness hole. -
The
Iter,IterMut, andIntoItertypes now report an exactsize_hint. Previously, they returned(0, None). This violated theExactSizeIteratorcontract, resulting in calls like.take(...).len()panicking on a non-empty map. -
Fixed a soundness hole in
IdOrdMap'sRefMut. WithinEntry::and_modifyandIdOrdMap::retain, the item can be removed while theRefMut's borrow lifetime'ais still live. In a contrived scenario where:- A map is held for
'static, e.g. withBox::leak; and, - A
Hashimpl was written only forKey<'static>,
The
Hashimpl could observe a key that wasn't valid for'static. The newHashbound onIdOrdItem::Keyrejects a'static-onlyHashimpl at compile time. - A map is held for
-
Fixed a soundness hole in the
Debugimpls forIdOrdMap,IdHashMap,BiHashMap, andTriHashMap. A contrived scenario where aDebugimpl was written only forKey<'static>could observe a'statickey that actually borrowed from the map. The impls no longer format keys, and the internal lifetime-extendingtransmuteis gone. (This is why theDebugoutput changed; see above.) -
Fixed a soundness hole in the
serializefunctions ofIdOrdMapAsMap,IdHashMapAsMap,BiHashMapAsMap, andTriHashMapAsMap. The lifetime'ainT::Key<'a>: Serializewas not tied to the borrow of the map, so a contrived scenario where aSerializeimpl was written only forKey<'static>could observe a key that actually borrowed from the map.This is a breaking change only for
Serializeimpls that exist solely for a'statickey type. In most cases, impls are generic over the key lifetime — those are unaffected. -
Fixed a soundness hole in
IdHashMap,BiHashMap, andTriHashMapwith custom allocators (via theallocator-api2feature). The maps now correctly call the allocator'sgrow,grow_zeroed,shrink, andallocate_zeroedmethods.Previously, only
allocateanddeallocatewere called, and the others fell through to the trait's default implementations -- those implementations allocate a new block, copy the memory, and then calldeallocateon the old one. In the unlikely case that the lastdeallocatefreed the block and then panicked, a map resize could leave the map holding a freed pointer, and dropping the map would free it again.Allocators that don't implement
growandshrinkstill get the trait's defaultgrowandshrink. For those allocators,deallocateshould not unwind after freeing. (This is a pre-existing limitation in theallocator-api2crate.)IdOrdMapdoes not support custom allocators and is not affected. Unsoundness on allocator panics is a widespread problem in the Rust ecosystem, which is why the soon-to-be-stabilized standard library allocator API bans panicking within the allocator.