Skip to content

Fix mutable aliasing UB: Replace Box<Entry> with NonNull<Entry> - #302

Merged
SimonSapin merged 1 commit into
servo:mainfrom
matteoldani:fix-mutable-aliasing
Aug 19, 2026
Merged

Fix mutable aliasing UB: Replace Box<Entry> with NonNull<Entry>#302
SimonSapin merged 1 commit into
servo:mainfrom
matteoldani:fix-mutable-aliasing

Conversation

@matteoldani

@matteoldani matteoldani commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This PR resolves Undefined Behavior caused by mutable aliasing in the dynamic_set.rs implementation.

Previously, the Set traversal logic temporarily dropped and re-boxed the Option<Box<Entry>> links, which violated Stacked Borrows and invalidated raw pointers pointing to the entry during insert and remove operations.

This has been fixed by replacing Option<Box<Entry>> with Option<NonNull<Entry>>, adding proper manual Drop semantics to prevent memory leaks, and implementing explicit Send and Sync bounds.

Comment thread src/dynamic_set.rs Outdated
Comment on lines 128 to 140
impl Drop for Set {
fn drop(&mut self) {
for bucket in self.buckets.iter_mut() {
let mut current = bucket.get_mut().take();
while let Some(ptr) = current {
// SAFETY: The Set is being dropped, meaning no other Atom references can exist.
// We own the pointers and must reconstruct the Box to safely free the memory.
let mut entry = unsafe { Box::from_raw(ptr.as_ptr()) };
current = entry.next_in_bucket.take();
}
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is dead code, since the only Set ever created in a process lives in static DYNAMIC_SET: OnceLock<Set> and is never dropped. I’ve pushed a commit to remove it

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Set is being dropped, meaning no other Atom references can exist

There is no such guarantee (which is why the Set can never be dropped)

@SimonSapin
SimonSapin force-pushed the fix-mutable-aliasing branch from 59f0600 to fa86b3e Compare August 19, 2026 19:58
@SimonSapin

Copy link
Copy Markdown
Member

Fixes #264

Removing this because, while this PR does improve things, I’m still getting another error from miri (a data race between Atom::clone and Set::remove)

@SimonSapin
SimonSapin added this pull request to the merge queue Aug 19, 2026
In Rust's Stacked Borrows and Tree Borrows memory models, moving a `Box<T>`
asserts unique access and invalidates all existing raw pointers to its
contents. In `string-cache`, dynamic string entries are allocated via `Box<Entry>`
and immediately handed out as raw pointers (`*mut Entry`) to be held by `Atom`.
However, because these `Box`es are moved into the global linked list
(`*linked_list = Some(entry)`), and moved again whenever a hash collision
occurs (`next_in_bucket: linked_list.take()`), the raw pointers held by
active `Atom`s were being continuously invalidated. This led to pervasive
UB when `Atom` subsequently dereferenced them in `clone` and `drop`.

This commit re-architects the linked list to use `Option<NonNull<Entry>>`.
By manually allocating (`Box::into_raw`) and destroying (`Box::from_raw`)
the entries, we sever the compiler's strict aliasing assumptions over the
pointers, preserving their provenance. We also provide a custom `Drop`
implementation for `Set` to prevent memory leaks during tests, manually
re-implement `Send` and `Sync`, and include the missing integer overflow
guard inside `Set::insert`.
@SimonSapin
SimonSapin force-pushed the fix-mutable-aliasing branch from fa86b3e to 60da8bf Compare August 19, 2026 20:09
Merged via the queue into servo:main with commit 187e6b5 Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants