| //! Regression test for the panic-safety fix in rust-lang/rust#155707. |
| //! |
| //! rust-lang/rust#70201 gave `<CStr as ToOwned>::clone_into` a path that moved the |
| //! target `CString`'s buffer out before growing a `Vec`; if that growth's allocation |
| //! failed and unwound, the target was left without its nul terminator. This only |
| //! reproduces under Miri: in a normal build the `#[global_allocator]` below can't |
| //! intercept the reallocation inside `CString::clone_into` (it lives in libstd, which |
| //! library tests link with `-C prefer-dynamic`), so as a regular test it just checks |
| //! the happy path. |
| |
| // Disabled under Miri on Windows: a `#[global_allocator]` wrapping `System` trips |
| // Stacked Borrows there, and it affects libtest's own allocations, not just this test |
| // (so `#[ignore]` would not be enough). See <https://github.com/rust-lang/miri/issues/2104>. |
| #![cfg(not(all(miri, windows)))] |
| #![feature(alloc_error_hook)] |
| |
| use std::alloc::{GlobalAlloc, Layout, System, set_alloc_error_hook}; |
| use std::ffi::CString; |
| use std::panic::{AssertUnwindSafe, catch_unwind}; |
| use std::sync::atomic::{AtomicBool, Ordering}; |
| |
| // Once armed, the first allocation of 8 bytes or more fails and disarms, so the |
| // reallocation inside `clone_into`'s grow path fails while the runtime's own |
| // smaller allocations keep succeeding. |
| struct OneShotFailingAlloc; |
| |
| static ARMED: AtomicBool = AtomicBool::new(false); |
| |
| unsafe impl GlobalAlloc for OneShotFailingAlloc { |
| unsafe fn alloc(&self, layout: Layout) -> *mut u8 { |
| if layout.size() >= 8 && ARMED.swap(false, Ordering::SeqCst) { |
| return core::ptr::null_mut(); |
| } |
| unsafe { System.alloc(layout) } |
| } |
| |
| unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { |
| unsafe { System.dealloc(ptr, layout) } |
| } |
| |
| unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { |
| if new_size >= 8 && ARMED.swap(false, Ordering::SeqCst) { |
| return core::ptr::null_mut(); |
| } |
| unsafe { System.realloc(ptr, layout, new_size) } |
| } |
| } |
| |
| #[global_allocator] |
| static ALLOC: OneShotFailingAlloc = OneShotFailingAlloc; |
| |
| #[test] |
| #[cfg_attr(not(panic = "unwind"), ignore = "test requires unwinding support")] |
| fn clone_into_alloc_failure_leaves_target_valid() { |
| set_alloc_error_hook(|_| panic!("alloc error")); |
| |
| let src = CString::new("a fairly long value").unwrap(); |
| let mut target = CString::new("x").unwrap(); |
| |
| ARMED.store(true, Ordering::SeqCst); |
| // Under Miri the failing allocator is honored, so this reallocation unwinds; in a |
| // normal build the allocator can't intercept it and `clone_into` just succeeds. |
| let res = catch_unwind(AssertUnwindSafe(|| src.as_c_str().clone_into(&mut target))); |
| ARMED.store(false, Ordering::SeqCst); |
| |
| if cfg!(miri) { |
| assert!(res.is_err(), "clone_into should have unwound on the alloc failure"); |
| } |
| // Either way `target` must still end in its nul terminator. Before the fix the Miri |
| // unwind left it empty (also caught as a bad write in `CString`'s destructor). |
| assert_eq!(target.as_bytes_with_nul().last(), Some(&0)); |
| } |