Skip to content

Fix Stacked Borrows UB when splicing an inline SmallVec - #449

Closed
jaideeppyne wants to merge 1 commit into
servo:v2from
jaideeppyne:fix/splice-inline-stacked-borrows
Closed

Fix Stacked Borrows UB when splicing an inline SmallVec#449
jaideeppyne wants to merge 1 commit into
servo:v2from
jaideeppyne:fix/splice-inline-stacked-borrows

Conversation

@jaideeppyne

Copy link
Copy Markdown

SmallVec::splice triggers undefined behavior (a Stacked Borrows aliasing violation, caught by cargo miri test) when the vector is stored inline:

let mut v: SmallVec<i32, 16> = smallvec![1, 2, 3, 4, 5];   // stays inline
let removed: Vec<i32> = v.splice(1..3, [10, 11, 12]).collect();

Drain::fill builds a &mut [T] into the inline buffer and then calls vec.set_len(..) inside the fill loop. Since the inline buffer lives inside the SmallVec, a &mut self method reborrows the whole struct and invalidates that slice (and the place reference derived from it), so the next loop iteration retags from a dead tag:

error: Undefined Behavior: trying to retag from <128112> for Unique permission at
alloc41362[0x10], but that tag does not exist in the borrow stack for this location
help: <128112> was created by a SharedReadWrite retag at offsets [0xc..0x14]
help: <128112> was later invalidated at offsets [0x0..0x28] by a Unique function-entry retag

Drain::move_tail has the same issue: it takes a *const via vec.as_ptr() and then reborrows the whole SmallVec again through vec.as_mut_ptr(), invalidating the first pointer before ptr::copy uses it. Both only manifest while inline, which is why the existing splice test (SmallVec<u8, 1>, always spilled) never caught it.

Fixed the way std's Drain::fill avoids it: update the length by writing the len field directly instead of calling a method, and derive the copy's source and destination from a single base pointer. Adds tests::splice_inline, which fails under cargo miri test before this change and passes after. The full suite stays green under both Stacked Borrows and Tree Borrows.

One note for your judgement: this is a Stacked Borrows violation — it passes under Tree Borrows, which tracks that set_len only writes the len field. Both models are still experimental, but this is UB under the model Miri runs by default and that scripts/run_miri.sh uses in CI, it's reachable from entirely safe code, and holding a reference into a struct-embedded buffer across a &mut self reborrow is the pattern std deliberately avoids. The fix costs nothing either way.

`SmallVec::splice` could trigger undefined behavior (an aliasing violation
detected by Miri under Stacked Borrows) when the vector is stored inline.

`Drain::fill` built a `&mut [T]` (`range_slice`) pointing into the inline
buffer and then called `vec.set_len(..)` inside the fill loop. Because the
inline buffer lives *inside* the `SmallVec`, calling a `&mut self` method
reborrows the whole struct and so invalidates `range_slice` (and the `place`
reference derived from it), making the next loop iteration read through a
dangling tag. `Drain::move_tail` had the same problem: it took a `*const`
via `vec.as_ptr()` and then reborrowed the whole `SmallVec` again through
`vec.as_mut_ptr()`, invalidating the first pointer before `ptr::copy` used it.

Both are fixed the way the standard library's `Drain::fill` avoids the issue:
update the length by writing the `len` field directly instead of calling a
method, and derive the copy's source and destination from a single base
pointer. The bug only manifested while inline, which is why the existing
`splice` test (using `SmallVec<u8, 1>`, always spilled) never caught it.

Adds `tests::splice_inline`, which fails under `cargo miri test` before this
change and passes after.
@alejandro-vaz

Copy link
Copy Markdown
Collaborator

AI contributions are not allowed in any @servo repository as indicated on the contributing guidelines

https://book.servo.org/contributing/getting-started.html#ai-contributions

this PR will subsequently be closed

@jdm

jdm commented Aug 26, 2026

Copy link
Copy Markdown
Member

@alejandro-vaz What signal did you use to decide this was an AI contribution?

@alejandro-vaz

alejandro-vaz commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

@jdm em-dashes, perfect syntax, the generic summary-implementation notes, and a comment in #444 saying it was literally a Codex agent

and if you look at the code, agents tend to add tests for everything, helper functions where there should be none, and increase LOC way too much for what was necessary

look at each individual one

all these contributions started appearing when I labeled issues as "good first issue" and "help wanted" which is amazing for attracting contributors and people with agents

correct me if I'm wrong but this looks really AI-made

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.

3 participants