`Deque::make_contiguous` sets a wrong `back` for a wrapped, non-full deque: `len()` grows and elements are dropped twice
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 84/100
Línea de trabajo
Empieza en src/deque.rs, en Deque::make_contiguous, y sigue la rama no llena con vuelta descrita en el issue. Comprueba cómo se relacionan front, back y len() antes y después del movimiento; luego ejecuta o añade un caso de regresión con capacidad 8, front en 4 y 5 elementos. El trabajo está terminado cuando len() sigue siendo 5 después de make_contiguous() y cada elemento se destruye una sola vez.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
First of all, thank you so much for maintaining heapless; while applying our testing tool across multiple crates during its evaluation, we encountered an issue that I would like to bring to your attention.
Deque::make_contiguous on a wrapped deque that is not full can set the back cursor to the wrong slot. After that, len() reports more elements than the deque holds, and safe code reads and drops slots that hold no value.
I notice the following code may have an unsound problem: https://github.com/rust-embedded/heapless/blob/1d3dace9d0ec77f8546da87b0b94873a07b98ad9/src/deque.rs#L404-L427
This is a different case from #676 / #677 (full deque). The branch below is reached by a deque that is not full.
Describe the bug
} else if free >= back_len {
// from: FGH....ABCDE
// to: ...ABCDEFGH.
unsafe {
ptr::copy(buffer_ptr.add(self.front), buffer_ptr.add(self.back), front_len);
ptr::copy_nonoverlapping(buffer_ptr, buffer_ptr.add(self.back + front_len), back_len);
}
self.front = back;
self.back = 0;
}
After the copies the elements are in back..back + len, so back should be back + len. The code sets it to 0. Then len() returns capacity - front instead of len. This is right only when the elements happen to end at the last slot.
Example with capacity 8 and 5 elements in slots 4,5,6,7,0: the elements move to slots 1..6. front becomes 1 and back becomes 0, so len() returns 8 - 1 = 7. iter(), pop_*(), truncate(), clear() and Drop then treat 2 extra slots as elements. With Box elements this is a double free.
Tested on the latest release, heapless 0.9.3 (main 1d3dace has the same code).
To Reproduce
- Push and pop 4 times on a
Deque<_, 8>, so thatfrontis 4. - Push 5 elements. They wrap into slots 4,5,6,7,0, and the deque is not full.
- Call
make_contiguous().
use heapless::Deque;
fn main() {
let mut dq: Deque<Box<u64>, 8> = Deque::new();
// move `front` to slot 4
for _ in 0..4 {
dq.push_back(Box::new(0)).unwrap();
dq.pop_front();
}
// 5 elements in slots 4,5,6,7,0: wrapped, not full
for i in 0..5 {
dq.push_back(Box::new(i)).unwrap();
}
println!("len before = {}", dq.len());
dq.make_contiguous();
println!("len after = {}", dq.len());
} // drop: 7 slots are dropped, 2 of them twice
make_contiguous should not change len(): len after = 5, and each box should be freed once.
output (RUSTFLAGS="-Zsanitizer=address" cargo +nightly-2026-04-03 run --release --target x86_64-unknown-linux-gnu):
len before = 5
len after = 7
==ERROR: AddressSanitizer: attempting double-free on 0x1078225e00d0 in thread T0:
#0 free
#1 heapless_deque_make_contiguous_back_cursor::main
Suggested fix
Set back to the end of the moved elements instead of 0 (src/deque.rs):
self.front = back;
// the elements now span `front..front + len`, not up to the end of the buffer
self.back = self.to_physical_index(len);
to_physical_index wraps at the capacity, so when the elements end at the last slot it still gives 0, as before.
I applied this to a copy of 0.9.3. The PoC prints len after = 5 with no AddressSanitizer report.
Thank you for your time.
- Lenguaje dominante
- Rust
- Estrellas
- 2k
- Forks
- 253
- Merge medio
- 3 d 2 h
- PR fusionados (30 d)
- 1
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de rust-embedded/heapless
-
`FromIterator` for `Deque`Posiblemente ocupada @ollien la tomó hace 81 días. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
rust-embedded/heapless#683 ·
-
enhancement
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
rust-embedded/heapless#681 ·
-
enhancement
Dificultad 3/5 1-2 días Aptitud para principiantes 55/100
rust-embedded/heapless#653 · 1 comentario ·
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 28/100
rust-embedded/heapless#645 · 5 comentarios · 1 reacción ·
-
Vec const-nessAbierto
Dificultad 3/5 1-2 días Aptitud para principiantes 45/100
rust-embedded/heapless#643 · 3 comentarios ·
Todos los issues de rust-embedded/heapless
Issues similares
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
mozilla-services/syncstorage-rs#2715 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 70/100
-
documentation
Dificultad 1/5 Menos de una hora Aptitud para principiantes 82/100
physicsdolphin/nv-uefi-unlocker#53 · 4 reacciones ·
Los mantenedores suelen responder en 1 día
-
area/desktop-shell
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
yantrikos/yantrik-os#1012 ·
Los mantenedores suelen responder en 1 día