Skip to content

Commit e87c001

Browse files
committed
fix(allocator): statically prevent memory leaks in allocator (#8570)
Prevent memory leaks by statically preventing `Drop` types from being allocated in the arena. Attempting to allocate any `Drop` type in the arena now produces a compilation failure. The stabilization of `const {}` blocks in Rust 1.79.0 gave the mechanism required to enforce this at compile time without a mess of generics and traits, and in a way which should not hurt compile times (and zero runtime cost). This PR is what discovered `CompactString`s being stored in arena in the mangler (fixed in #8557). Note: The compilation failure occurs in `cargo build` not `cargo check`. So unfortunately errors don't appear in Rust Analyser, only when you run `cargo build`. From what I've read, stable Rust does not offer any solution to this at present. But the errors are reasonably clear what the problem is, and point to the line where it occurs.
1 parent 95bc0d7 commit e87c001

File tree

4 files changed

+96
-27
lines changed

4 files changed

+96
-27
lines changed

crates/oxc_allocator/src/boxed.rs

+21-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{
77
fmt::{self, Debug, Formatter},
88
hash::{Hash, Hasher},
99
marker::PhantomData,
10+
mem::needs_drop,
1011
ops::{self, Deref},
1112
ptr::{self, NonNull},
1213
};
@@ -18,14 +19,16 @@ use crate::Allocator;
1819

1920
/// A `Box` without [`Drop`], which stores its data in the arena allocator.
2021
///
21-
/// Should only be used for storing AST types.
22+
/// ## No `Drop`s
2223
///
23-
/// Must NOT be used to store types which have a [`Drop`] implementation.
24-
/// `T::drop` will NOT be called on the `Box`'s contents when the `Box` is dropped.
25-
/// If `T` owns memory outside of the arena, this will be a memory leak.
24+
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop). Memory is released in bulk
25+
/// when the allocator is dropped, without dropping the individual objects in the arena.
2626
///
27-
/// Note: This is not a soundness issue, as Rust does not support relying on `drop`
28-
/// being called to guarantee soundness.
27+
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
28+
/// which own memory allocations outside the arena.
29+
///
30+
/// Static checks make this impossible to do. [`Box::new_in`] will refuse to compile if called
31+
/// with a [`Drop`] type.
2932
pub struct Box<'alloc, T: ?Sized>(NonNull<T>, PhantomData<(&'alloc (), T)>);
3033

3134
impl<T> Box<'_, T> {
@@ -68,6 +71,10 @@ impl<T> Box<'_, T> {
6871
/// let in_arena: Box<i32> = Box::new_in(5, &arena);
6972
/// ```
7073
pub fn new_in(value: T, allocator: &Allocator) -> Self {
74+
const {
75+
assert!(!needs_drop::<T>(), "Cannot create a Box<T> where T is a Drop type");
76+
}
77+
7178
Self(NonNull::from(allocator.alloc(value)), PhantomData)
7279
}
7380

@@ -78,6 +85,10 @@ impl<T> Box<'_, T> {
7885
/// Only purpose is for mocking types without allocating for const assertions.
7986
#[allow(unsafe_code, clippy::missing_safety_doc)]
8087
pub const unsafe fn dangling() -> Self {
88+
const {
89+
assert!(!needs_drop::<T>(), "Cannot create a Box<T> where T is a Drop type");
90+
}
91+
8192
Self(NonNull::dangling(), PhantomData)
8293
}
8394
}
@@ -97,6 +108,10 @@ impl<T: ?Sized> Box<'_, T> {
97108
/// `ptr` must have been created from a `*mut T` or `&mut T` (not a `*const T` / `&T`).
98109
#[inline]
99110
pub(crate) const unsafe fn from_non_null(ptr: NonNull<T>) -> Self {
111+
const {
112+
assert!(!needs_drop::<T>(), "Cannot create a Box<T> where T is a Drop type");
113+
}
114+
100115
Self(ptr, PhantomData)
101116
}
102117

crates/oxc_allocator/src/hash_map.rs

+20-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
use std::{
1111
hash::Hash,
12-
mem::ManuallyDrop,
12+
mem::{needs_drop, ManuallyDrop},
1313
ops::{Deref, DerefMut},
1414
};
1515

@@ -36,12 +36,16 @@ type FxHashMap<'alloc, K, V> = hashbrown::HashMap<K, V, FxBuildHasher, &'alloc B
3636
/// All APIs are the same, except create a [`HashMap`] with
3737
/// either [`new_in`](HashMap::new_in) or [`with_capacity_in`](HashMap::with_capacity_in).
3838
///
39-
/// Must NOT be used to store types which have a [`Drop`] implementation.
40-
/// `K::drop` and `V::drop` will NOT be called on the `HashMap`'s contents when the `HashMap` is dropped.
41-
/// If `K` or `V` own memory outside of the arena, this will be a memory leak.
39+
/// ## No `Drop`s
4240
///
43-
/// Note: This is not a soundness issue, as Rust does not support relying on `drop`
44-
/// being called to guarantee soundness.
41+
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop). Memory is released in bulk
42+
/// when the allocator is dropped, without dropping the individual objects in the arena.
43+
///
44+
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
45+
/// which own memory allocations outside the arena.
46+
///
47+
/// Static checks make this impossible to do. [`HashMap::new_in`] and all other methods which create
48+
/// a [`HashMap`] will refuse to compile if called with a [`Drop`] type.
4549
///
4650
/// [`FxHasher`]: rustc_hash::FxHasher
4751
pub struct HashMap<'alloc, K, V>(ManuallyDrop<FxHashMap<'alloc, K, V>>);
@@ -56,6 +60,11 @@ impl<'alloc, K, V> HashMap<'alloc, K, V> {
5660
/// until it is first inserted into.
5761
#[inline(always)]
5862
pub fn new_in(allocator: &'alloc Allocator) -> Self {
63+
const {
64+
assert!(!needs_drop::<K>(), "Cannot create a HashMap<K, V> where K is a Drop type");
65+
assert!(!needs_drop::<V>(), "Cannot create a HashMap<K, V> where V is a Drop type");
66+
}
67+
5968
let inner = FxHashMap::with_hasher_in(FxBuildHasher, allocator.bump());
6069
Self(ManuallyDrop::new(inner))
6170
}
@@ -66,6 +75,11 @@ impl<'alloc, K, V> HashMap<'alloc, K, V> {
6675
/// If capacity is 0, the hash map will not allocate.
6776
#[inline(always)]
6877
pub fn with_capacity_in(capacity: usize, allocator: &'alloc Allocator) -> Self {
78+
const {
79+
assert!(!needs_drop::<K>(), "Cannot create a HashMap<K, V> where K is a Drop type");
80+
assert!(!needs_drop::<V>(), "Cannot create a HashMap<K, V> where V is a Drop type");
81+
}
82+
6983
let inner =
7084
FxHashMap::with_capacity_and_hasher_in(capacity, FxBuildHasher, allocator.bump());
7185
Self(ManuallyDrop::new(inner))

crates/oxc_allocator/src/lib.rs

+29-9
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,41 @@
55
//! memory management data types from `std` adapted to use this arena.
66
//!
77
//! ## No `Drop`s
8-
//! Objects allocated into oxc memory arenas are never [`Dropped`](Drop), making
9-
//! it relatively easy to leak memory if you're not careful. Memory is released
10-
//! in bulk when the allocator is dropped.
8+
//!
9+
//! Objects allocated into Oxc memory arenas are never [`Dropped`](Drop).
10+
//! Memory is released in bulk when the allocator is dropped, without dropping the individual
11+
//! objects in the arena.
12+
//!
13+
//! Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
14+
//! which own memory allocations outside the arena.
15+
//!
16+
//! Static checks make this impossible to do. [`Allocator::alloc`], [`Box::new_in`], [`Vec::new_in`],
17+
//! and all other methods which store data in the arena will refuse to compile if called with
18+
//! a [`Drop`] type.
1119
//!
1220
//! ## Examples
13-
//! ```
21+
//!
22+
//! ```ignore
1423
//! use oxc_allocator::{Allocator, Box};
1524
//!
1625
//! struct Foo {
1726
//! pub a: i32
1827
//! }
28+
//!
1929
//! impl std::ops::Drop for Foo {
20-
//! fn drop(&mut self) {
21-
//! // Arena boxes are never dropped.
22-
//! unreachable!();
23-
//! }
30+
//! fn drop(&mut self) {}
31+
//! }
32+
//!
33+
//! struct Bar {
34+
//! v: std::vec::Vec<u8>,
2435
//! }
2536
//!
2637
//! let allocator = Allocator::default();
38+
//!
39+
//! // This will fail to compile because `Foo` implements `Drop`
2740
//! let foo = Box::new_in(Foo { a: 0 }, &allocator);
28-
//! drop(foo);
41+
//! // This will fail to compile because `Bar` contains a `std::vec::Vec`, and it implements `Drop`
42+
//! let bar = Box::new_in(Bar { v: vec![1, 2, 3] }, &allocator);
2943
//! ```
3044
//!
3145
//! Consumers of the [`oxc` umbrella crate](https://crates.io/crates/oxc) pass
@@ -41,6 +55,8 @@
4155
4256
#![warn(missing_docs)]
4357

58+
use std::mem::needs_drop;
59+
4460
use bumpalo::Bump;
4561

4662
mod address;
@@ -92,6 +108,10 @@ impl Allocator {
92108
#[expect(clippy::inline_always)]
93109
#[inline(always)]
94110
pub fn alloc<T>(&self, val: T) -> &mut T {
111+
const {
112+
assert!(!needs_drop::<T>(), "Cannot allocate Drop type in arena");
113+
}
114+
95115
self.bump.alloc(val)
96116
}
97117

crates/oxc_allocator/src/vec.rs

+26-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
self,
1010
fmt::{self, Debug},
1111
hash::{Hash, Hasher},
12-
mem::ManuallyDrop,
12+
mem::{needs_drop, ManuallyDrop},
1313
ops,
1414
ptr::NonNull,
1515
slice::SliceIndex,
@@ -25,12 +25,16 @@ use crate::{Allocator, Box, String};
2525

2626
/// A `Vec` without [`Drop`], which stores its data in the arena allocator.
2727
///
28-
/// Must NOT be used to store types which have a [`Drop`] implementation.
29-
/// `T::drop` will NOT be called on the `Vec`'s contents when the `Vec` is dropped.
30-
/// If `T` owns memory outside of the arena, this will be a memory leak.
28+
/// ## No `Drop`s
3129
///
32-
/// Note: This is not a soundness issue, as Rust does not support relying on `drop`
33-
/// being called to guarantee soundness.
30+
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop). Memory is released in bulk
31+
/// when the allocator is dropped, without dropping the individual objects in the arena.
32+
///
33+
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
34+
/// which own memory allocations outside the arena.
35+
///
36+
/// Static checks make this impossible to do. [`Vec::new_in`] and all other methods which create
37+
/// a [`Vec`] will refuse to compile if called with a [`Drop`] type.
3438
#[derive(PartialEq, Eq)]
3539
pub struct Vec<'alloc, T>(pub(crate) ManuallyDrop<InnerVec<T, &'alloc Bump>>);
3640

@@ -56,6 +60,10 @@ impl<'alloc, T> Vec<'alloc, T> {
5660
/// ```
5761
#[inline(always)]
5862
pub fn new_in(allocator: &'alloc Allocator) -> Self {
63+
const {
64+
assert!(!needs_drop::<T>(), "Cannot create a Vec<T> where T is a Drop type");
65+
}
66+
5967
Self(ManuallyDrop::new(InnerVec::new_in(allocator.bump())))
6068
}
6169

@@ -108,6 +116,10 @@ impl<'alloc, T> Vec<'alloc, T> {
108116
/// ```
109117
#[inline(always)]
110118
pub fn with_capacity_in(capacity: usize, allocator: &'alloc Allocator) -> Self {
119+
const {
120+
assert!(!needs_drop::<T>(), "Cannot create a Vec<T> where T is a Drop type");
121+
}
122+
111123
Self(ManuallyDrop::new(InnerVec::with_capacity_in(capacity, allocator.bump())))
112124
}
113125

@@ -117,6 +129,10 @@ impl<'alloc, T> Vec<'alloc, T> {
117129
/// This is behaviorially identical to [`FromIterator::from_iter`].
118130
#[inline]
119131
pub fn from_iter_in<I: IntoIterator<Item = T>>(iter: I, allocator: &'alloc Allocator) -> Self {
132+
const {
133+
assert!(!needs_drop::<T>(), "Cannot create a Vec<T> where T is a Drop type");
134+
}
135+
120136
let iter = iter.into_iter();
121137
let hint = iter.size_hint();
122138
let capacity = hint.1.unwrap_or(hint.0);
@@ -143,6 +159,10 @@ impl<'alloc, T> Vec<'alloc, T> {
143159
/// ```
144160
#[inline]
145161
pub fn from_array_in<const N: usize>(array: [T; N], allocator: &'alloc Allocator) -> Self {
162+
const {
163+
assert!(!needs_drop::<T>(), "Cannot create a Vec<T> where T is a Drop type");
164+
}
165+
146166
let boxed = Box::new_in(array, allocator);
147167
let ptr = Box::into_non_null(boxed).as_ptr().cast::<T>();
148168
// SAFETY: `ptr` has correct alignment - it was just allocated as `[T; N]`.

0 commit comments

Comments
 (0)