fix(allocator): fix unsound lifetime extension in Box::new_in (#23685)

Fix unsoundness in `Box::new_in`.

`Box::new_in` had this signature:

```rust
impl<T> Box<'_, T> {
    pub fn new_in(value: T, allocator: &Allocator) -> Self {
        Self(NonNull::from(allocator.alloc(value)), PhantomData)
    }
}
```

The returned `Box<'alloc, T>`'s lifetime `'alloc` and the `&Allocator` borrow were two independent, unconstrained lifetimes. Nothing tied them together, so the caller was free to choose any `'alloc` - including one that outlives the allocator. In effect you could mint a `Box<'static, T>` from a temporary `Allocator`:

```rust
let boxed = {
    let allocator = Allocator::default();
    Box::new_in(5, &allocator)   // returns Box<'_, i32> with a lifetime unrelated to `allocator`
};                               // `allocator` dropped here, freeing the backing memory
assert_eq!(*boxed, 5);           // use after free
```

Fix by tying the 2 lifetimes together.

```rust
impl<'alloc, T> Box<'alloc, T> {
    pub fn new_in(value: T, allocator: &'alloc Allocator) -> Self { ... }
}
```

This bug crept in in #2943 and has gone unnoticed since - largely because we rarely use `Box::new_in` directly, and `AstBuilder::alloc` was correctly providing a tighter lifetime bound.
This commit is contained in:
overlookmotel committed 2026-06-21 16:18:56 +00:00
1 parent b1b83d5b9f
commit 7231d5542f
1 file changed
+16 -4
+16 -4
View File
@@ -41,23 +41,35 @@ impl<T: ?Sized> Box<'_, T> {
assert!(!std::mem::needs_drop::<T>(), "Cannot create a Box<T> where T is a Drop type");
}
impl<T> Box<'_, T> {
/// Put a `value` into a memory arena and get back a [`Box`] with ownership
/// to the allocation.
impl<'alloc, T> Box<'alloc, T> {
/// Allocate `value` into the memory arena, and receive a [`Box`] which owns the value.
///
/// # Examples
///
/// ```
/// use oxc_allocator::{Allocator, Box};
///
/// let arena = Allocator::default();
/// let in_arena: Box<i32> = Box::new_in(5, &arena);
/// ```
///
/// The `Box` cannot outlive the `Allocator`. This fails to compile:
///
/// ```compile_fail
/// use oxc_allocator::{Allocator, Box};
///
/// let boxed = {
/// let allocator = Allocator::default();
/// Box::new_in(5, &allocator)
/// };
/// assert_eq!(*boxed, 5);
/// ```
//
// `#[inline(always)]` because this is a hot path and `Allocator::alloc` is a very small function.
// We always want it to be inlined.
#[expect(clippy::inline_always)]
#[inline(always)]
pub fn new_in(value: T, allocator: &Allocator) -> Self {
pub fn new_in(value: T, allocator: &'alloc Allocator) -> Self {
const { Self::ASSERT_T_IS_NOT_DROP };
Self(NonNull::from(allocator.alloc(value)), PhantomData)