I tried this code: [playground]
use std::io::{Cursor, BufWriter};
fn main() {
let mut v = vec![0; 1024];
let c = Cursor::new(&mut v);
let w = BufWriter::new(Box::new(c));
let _ = w.into_parts();
}
I expected to see this happen: it runs cleanly under Miri.
Instead, this happened:
error: Undefined Behavior: trying to retag from <2686> for Unique permission at alloc1164[0x0], but that tag does not exist in the borrow stack for this location
--> /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/buffered/bufwriter.rs:175:10
|
175 | (inner, buf)
| ^^^^^
| |
| trying to retag from <2686> for Unique permission at alloc1164[0x0], but that tag does not exist in the borrow stack for this location
| this error occurs as part of retag at alloc1164[0x0..0x10]
|
= help: this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental
= help: see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information
help: <2686> was created by a Unique retag at offsets [0x0..0x10]
--> src/main.rs:7:13
|
7 | let _ = w.into_parts();
| ^^^^^^^^^^^^^^
help: <2686> was later invalidated at offsets [0x0..0x10] by a Unique retag (of a reference/box inside this compound value)
--> src/main.rs:7:13
|
7 | let _ = w.into_parts();
| ^^^^^^^^^^^^^^
= note: BACKTRACE (of the first span):
= note: inside `std::io::BufWriter::<std::boxed::Box<std::io::Cursor<&mut std::vec::Vec<u8>>>>::into_parts` at /playground/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/buffered/bufwriter.rs:175:10: 175:15
note: inside `main`
--> src/main.rs:7:13
|
7 | let _ = w.into_parts();
| ^^^^^^^^^^^^^^
The issue is in this construct:
https://github.com/rust-lang/rust/blob/master/library%2Fstd%2Fsrc%2Fio%2Fbuffered%2Fbufwriter.rs#L171-L173
By the SB model, moving a (struct directly containing) Box retags that Box as the live unique owner of the referenced allocation. Thus passing self to forget invalidates the duplicated Box (W) we just retrieved. The correct way to write this is with ManuallyDrop:
// inner is the only remaining field with nontrivial drop glue
let this = ManuallyDrop::new(self);
// SAFETY: this takes logical ownership of inner
let inner = unsafe { ptr::read(&this.inner) };
Meta
Tested against 1.81.0-nightly (2024-07-09 6be96e3).
This also catches BufWriter::into_inner, which is implemented using into_parts.
IIRC the LLVM backend is currently not informed about this instance of this UB, as the relevant attributes are only added when the argument passing ABI is Scalar or ScalarPair, which it is not in this case. Additionally, Miri's SB implementation does not complain when the box is Box<dyn Write>, (conjecture: since dyn Write is not Unpin).
cc @RalfJung — another case of read; forget instead of ManuallyDrop::new; read in std.
This was randomly spotted. It might be worth it to do a grep for forget to try to spot further such mistakes. forget's docs call this out as incorrect, but it's still quite easy to overlook.
I tried this code: [playground]
I expected to see this happen: it runs cleanly under Miri.
Instead, this happened:
The issue is in this construct:
https://github.com/rust-lang/rust/blob/master/library%2Fstd%2Fsrc%2Fio%2Fbuffered%2Fbufwriter.rs#L171-L173
By the SB model, moving a (struct directly containing)
Boxretags thatBoxas the live unique owner of the referenced allocation. Thus passingselftoforgetinvalidates the duplicatedBox(W) we just retrieved. The correct way to write this is withManuallyDrop:Meta
Tested against 1.81.0-nightly (2024-07-09 6be96e3).
This also catches
BufWriter::into_inner, which is implemented usinginto_parts.IIRC the LLVM backend is currently not informed about this instance of this UB, as the relevant attributes are only added when the argument passing ABI is
ScalarorScalarPair, which it is not in this case. Additionally, Miri's SB implementation does not complain when the box isBox<dyn Write>, (conjecture: sincedyn Writeis notUnpin).cc @RalfJung — another case of
read; forgetinstead ofManuallyDrop::new; readin std.This was randomly spotted. It might be worth it to do a grep for
forgetto try to spot further such mistakes.forget's docs call this out as incorrect, but it's still quite easy to overlook.