|
| 1 | +//! Regression test for the panic-safety fix in rust-lang/rust#155707. |
| 2 | +//! |
| 3 | +//! rust-lang/rust#70201 gave `<CStr as ToOwned>::clone_into` a path that moved the |
| 4 | +//! target `CString`'s buffer out before growing a `Vec`; if that growth's allocation |
| 5 | +//! failed and unwound, the target was left without its nul terminator. This only |
| 6 | +//! reproduces under Miri: in a normal build the `#[global_allocator]` below can't |
| 7 | +//! intercept the reallocation inside `CString::clone_into` (it lives in libstd, which |
| 8 | +//! library tests link with `-C prefer-dynamic`), so as a regular test it just checks |
| 9 | +//! the happy path. |
| 10 | +#![feature(alloc_error_hook)] |
| 11 | + |
| 12 | +use std::alloc::{GlobalAlloc, Layout, System, set_alloc_error_hook}; |
| 13 | +use std::ffi::CString; |
| 14 | +use std::panic::{AssertUnwindSafe, catch_unwind}; |
| 15 | +use std::sync::atomic::{AtomicBool, Ordering}; |
| 16 | + |
| 17 | +// Once armed, the first allocation of 8 bytes or more fails and disarms, so the |
| 18 | +// reallocation inside `clone_into`'s grow path fails while the runtime's own |
| 19 | +// smaller allocations keep succeeding. |
| 20 | +struct OneShotFailingAlloc; |
| 21 | + |
| 22 | +static ARMED: AtomicBool = AtomicBool::new(false); |
| 23 | + |
| 24 | +unsafe impl GlobalAlloc for OneShotFailingAlloc { |
| 25 | + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { |
| 26 | + if layout.size() >= 8 && ARMED.swap(false, Ordering::SeqCst) { |
| 27 | + return core::ptr::null_mut(); |
| 28 | + } |
| 29 | + unsafe { System.alloc(layout) } |
| 30 | + } |
| 31 | + |
| 32 | + unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { |
| 33 | + unsafe { System.dealloc(ptr, layout) } |
| 34 | + } |
| 35 | + |
| 36 | + unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { |
| 37 | + if new_size >= 8 && ARMED.swap(false, Ordering::SeqCst) { |
| 38 | + return core::ptr::null_mut(); |
| 39 | + } |
| 40 | + unsafe { System.realloc(ptr, layout, new_size) } |
| 41 | + } |
| 42 | +} |
| 43 | + |
| 44 | +#[global_allocator] |
| 45 | +static ALLOC: OneShotFailingAlloc = OneShotFailingAlloc; |
| 46 | + |
| 47 | +#[test] |
| 48 | +#[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")] |
| 49 | +fn clone_into_alloc_failure_leaves_target_valid() { |
| 50 | + set_alloc_error_hook(|_| panic!("alloc error")); |
| 51 | + |
| 52 | + let src = CString::new("a fairly long value").unwrap(); |
| 53 | + let mut target = CString::new("x").unwrap(); |
| 54 | + |
| 55 | + ARMED.store(true, Ordering::SeqCst); |
| 56 | + // Under Miri the failing allocator is honored, so this reallocation unwinds; in a |
| 57 | + // normal build the allocator can't intercept it and `clone_into` just succeeds. |
| 58 | + let res = catch_unwind(AssertUnwindSafe(|| src.as_c_str().clone_into(&mut target))); |
| 59 | + ARMED.store(false, Ordering::SeqCst); |
| 60 | + |
| 61 | + if cfg!(miri) { |
| 62 | + assert!(res.is_err(), "clone_into should have unwound on the alloc failure"); |
| 63 | + } |
| 64 | + // Either way `target` must still end in its nul terminator. Before the fix the Miri |
| 65 | + // unwind left it empty (also caught as a bad write in `CString`'s destructor). |
| 66 | + assert_eq!(target.as_bytes_with_nul().last(), Some(&0)); |
| 67 | +} |
0 commit comments