Skip to content

Commit f490d27

Browse files
committed
fix(allocator): remove unsound Send impl and tighten Sync requirement for HashMap (#13203)
Same as #13041. It's unsound for `oxc_allocator::HashMap` to be `Send` because if you can move a `HashMap` to another thread, you can call `insert` on it and it may allocate in the arena. This is unsound because another thread may be simultaneously be doing the same with another `HashMap`, and `Bump` is not thread-safe. Remove the `Send` impl on `HashMap`. However, we do want `HashMap` to be `Sync`. There's no reason why you shouldn't have 2 `&HashMap` references on different threads, as `&HashMap` doesn't allow mutating the `HashMap` in any way. Tighten up the trait bounds so `HashMap<K, V>` is `Sync` only if `K` and `V` both are. This is not yet completely sound. There are a couple of holes I'm aware of: 1. `allocator` method which allows obtaining a `&Bump` from a `&HashMap`. 2. `clone` which allocates into arena, taking only an immutable `&self` reference. This PR closes those holes as much as possible without a major refactor. It's not *completely* sound, but it's at least now quite difficult to commit UB - it'd be unlikely to happen by accident. So, not perfect, but at least there's now only a crack in the window, rather than the front door swinging wide open. --- The reason these 2 impls were added originally was as a quick fix to make `oxc_semantic::Scoping` `Send` and `Sync`. That is addressed in the next PR in this stack.
1 parent 0815a89 commit f490d27

File tree

1 file changed

+52
-4
lines changed

1 file changed

+52
-4
lines changed

crates/oxc_allocator/src/hash_map.rs

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,41 @@ type FxHashMap<'alloc, K, V> = hashbrown::HashMap<K, V, FxBuildHasher, &'alloc B
5050
/// [`FxHasher`]: rustc_hash::FxHasher
5151
pub struct HashMap<'alloc, K, V>(ManuallyDrop<FxHashMap<'alloc, K, V>>);
5252

53-
/// SAFETY: Not actually safe, but for enabling `Send` for downstream crates.
54-
unsafe impl<K, V> Send for HashMap<'_, K, V> {}
55-
/// SAFETY: Not actually safe, but for enabling `Sync` for downstream crates.
56-
unsafe impl<K, V> Sync for HashMap<'_, K, V> {}
53+
/// SAFETY: Even though `Bump` is not `Sync`, we can make `HashMap<K, V>` `Sync` if both `K` and `V`
54+
/// are `Sync` because:
55+
///
56+
/// 1. No public methods allow access to the `&Bump` that `HashMap` contains (in `hashbrown::HashMap`),
57+
/// so user cannot illegally obtain 2 `&Bump`s on different threads via `HashMap`.
58+
///
59+
/// 2. All internal methods which access the `&Bump` take a `&mut self`.
60+
/// `&mut HashMap` cannot be transferred across threads, and nor can an owned `HashMap`
61+
/// (`HashMap` is not `Send`).
62+
/// Therefore these methods taking `&mut self` can be sure they're not operating on a `HashMap`
63+
/// which has been moved across threads.
64+
///
65+
/// Note: `HashMap` CANNOT be `Send`, even if `K` and `V` are `Send`, because that would allow 2 `HashMap`s
66+
/// on different threads to both allocate into same arena simultaneously. `Bump` is not thread-safe,
67+
/// and this would be undefined behavior.
68+
///
69+
/// ### Soundness holes
70+
///
71+
/// This is not actually fully sound. There are 2 holes I (@overlookmotel) am aware of:
72+
///
73+
/// 1. `allocator` method, which does allow access to the `&Bump` that `HashMap` contains.
74+
/// 2. `Clone` impl on `hashbrown::HashMap`, which may perform allocations in the arena, given only a
75+
/// `&self` reference.
76+
///
77+
/// [`HashMap::allocator`] prevents accidental access to the underlying method of `hashbrown::HashMap`,
78+
/// and `clone` called on a `&HashMap` clones the `&HashMap` reference, not the `HashMap` itself (harmless).
79+
/// But both can be accessed via explicit `Deref` (`hash_map.deref().allocator()` or `hash_map.deref().clone()`),
80+
/// so we don't have complete soundness.
81+
///
82+
/// To close these holes we need to remove `Deref` and `DerefMut` impls on `HashMap`, and instead add
83+
/// methods to `HashMap` itself which pass on calls to the inner `hashbrown::HashMap`.
84+
///
85+
/// TODO: Fix these holes.
86+
/// TODO: Remove any other methods that currently allow performing allocations with only a `&self` reference.
87+
unsafe impl<K: Sync, V: Sync> Sync for HashMap<'_, K, V> {}
5788

5889
// TODO: `IntoIter`, `Drain`, and other consuming iterators provided by `hashbrown` are `Drop`.
5990
// Wrap them in `ManuallyDrop` to prevent that.
@@ -114,6 +145,23 @@ impl<'alloc, K, V> HashMap<'alloc, K, V> {
114145
let inner = ManuallyDrop::into_inner(self.0);
115146
inner.into_values()
116147
}
148+
149+
/// Calling this method produces a compile-time panic.
150+
///
151+
/// This method would be unsound, because [`HashMap`] is `Sync`, and the underlying allocator
152+
/// (`bumpalo::Bump`) is not `Sync`.
153+
///
154+
/// This method exists only to block access as much as possible to the underlying
155+
/// `hashbrown::HashMap::allocator` method. That method can still be accessed via explicit `Deref`
156+
/// (`hash_map.deref().allocator()`), but that's unsound.
157+
///
158+
/// We'll prevent access to it completely and remove this method as soon as we can.
159+
// TODO: Do that!
160+
#[expect(clippy::unused_self)]
161+
pub fn allocator(&self) -> &'alloc Bump {
162+
const { panic!("This method cannot be called") };
163+
unreachable!();
164+
}
117165
}
118166

119167
// Provide access to all `hashbrown::HashMap`'s methods via deref

0 commit comments

Comments
 (0)