blob: 669a783645baa54d343109ad6e96c23860d28401 [file] [edit]
//! 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));
}