miri/shims/alloc.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
use std::iter;
use rustc_abi::{Align, Size};
use rustc_ast::expand::allocator::AllocatorKind;
use crate::*;
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
/// Returns the alignment that `malloc` would guarantee for requests of the given size.
fn malloc_align(&self, size: u64) -> Align {
let this = self.eval_context_ref();
// The C standard says: "The pointer returned if the allocation succeeds is suitably aligned
// so that it may be assigned to a pointer to any type of object with a fundamental
// alignment requirement and size less than or equal to the size requested."
// So first we need to figure out what the limits are for "fundamental alignment".
// This is given by `alignof(max_align_t)`. The following list is taken from
// `library/std/src/sys/pal/common/alloc.rs` (where this is called `MIN_ALIGN`) and should
// be kept in sync.
let max_fundamental_align = match this.tcx.sess.target.arch.as_ref() {
"x86" | "arm" | "mips" | "mips32r6" | "powerpc" | "powerpc64" | "wasm32" => 8,
"x86_64" | "aarch64" | "mips64" | "mips64r6" | "s390x" | "sparc64" | "loongarch64" =>
16,
arch => bug!("unsupported target architecture for malloc: `{}`", arch),
};
// The C standard only requires sufficient alignment for any *type* with size less than or
// equal to the size requested. Types one can define in standard C seem to never have an alignment
// bigger than their size. So if the size is 2, then only alignment 2 is guaranteed, even if
// `max_fundamental_align` is bigger.
// This matches what some real-world implementations do, see e.g.
// - https://github.com/jemalloc/jemalloc/issues/1533
// - https://github.com/llvm/llvm-project/issues/53540
// - https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2293.htm
if size >= max_fundamental_align {
return Align::from_bytes(max_fundamental_align).unwrap();
}
// C doesn't have zero-sized types, so presumably nothing is guaranteed here.
if size == 0 {
return Align::ONE;
}
// We have `size < min_align`. Round `size` *down* to the next power of two and use that.
fn prev_power_of_two(x: u64) -> u64 {
let next_pow2 = x.next_power_of_two();
if next_pow2 == x {
// x *is* a power of two, just use that.
x
} else {
// x is between two powers, so next = 2*prev.
next_pow2 / 2
}
}
Align::from_bytes(prev_power_of_two(size)).unwrap()
}
/// Emulates calling the internal __rust_* allocator functions
fn emulate_allocator(
&mut self,
default: impl FnOnce(&mut MiriInterpCx<'tcx>) -> InterpResult<'tcx>,
) -> InterpResult<'tcx, EmulateItemResult> {
let this = self.eval_context_mut();
let Some(allocator_kind) = this.tcx.allocator_kind(()) else {
// in real code, this symbol does not exist without an allocator
return interp_ok(EmulateItemResult::NotSupported);
};
match allocator_kind {
AllocatorKind::Global => {
// When `#[global_allocator]` is used, `__rust_*` is defined by the macro expansion
// of this attribute. As such we have to call an exported Rust function,
// and not execute any Miri shim. Somewhat unintuitively doing so is done
// by returning `NotSupported`, which triggers the `lookup_exported_symbol`
// fallback case in `emulate_foreign_item`.
interp_ok(EmulateItemResult::NotSupported)
}
AllocatorKind::Default => {
default(this)?;
interp_ok(EmulateItemResult::NeedsReturn)
}
}
}
fn malloc(&mut self, size: u64, zero_init: bool) -> InterpResult<'tcx, Pointer> {
let this = self.eval_context_mut();
let align = this.malloc_align(size);
let ptr = this.allocate_ptr(Size::from_bytes(size), align, MiriMemoryKind::C.into())?;
if zero_init {
// We just allocated this, the access is definitely in-bounds and fits into our address space.
this.write_bytes_ptr(
ptr.into(),
iter::repeat(0u8).take(usize::try_from(size).unwrap()),
)
.unwrap();
}
interp_ok(ptr.into())
}
fn posix_memalign(
&mut self,
memptr: &OpTy<'tcx>,
align: &OpTy<'tcx>,
size: &OpTy<'tcx>,
) -> InterpResult<'tcx, Scalar> {
let this = self.eval_context_mut();
let memptr = this.deref_pointer(memptr)?;
let align = this.read_target_usize(align)?;
let size = this.read_target_usize(size)?;
// Align must be power of 2, and also at least ptr-sized (POSIX rules).
// But failure to adhere to this is not UB, it's an error condition.
if !align.is_power_of_two() || align < this.pointer_size().bytes() {
interp_ok(this.eval_libc("EINVAL"))
} else {
let ptr = this.allocate_ptr(
Size::from_bytes(size),
Align::from_bytes(align).unwrap(),
MiriMemoryKind::C.into(),
)?;
this.write_pointer(ptr, &memptr)?;
interp_ok(Scalar::from_i32(0))
}
}
fn free(&mut self, ptr: Pointer) -> InterpResult<'tcx> {
let this = self.eval_context_mut();
if !this.ptr_is_null(ptr)? {
this.deallocate_ptr(ptr, None, MiriMemoryKind::C.into())?;
}
interp_ok(())
}
fn realloc(&mut self, old_ptr: Pointer, new_size: u64) -> InterpResult<'tcx, Pointer> {
let this = self.eval_context_mut();
let new_align = this.malloc_align(new_size);
if this.ptr_is_null(old_ptr)? {
// Here we must behave like `malloc`.
self.malloc(new_size, /*zero_init*/ false)
} else {
if new_size == 0 {
// C, in their infinite wisdom, made this UB.
// <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2464.pdf>
throw_ub_format!("`realloc` with a size of zero");
} else {
let new_ptr = this.reallocate_ptr(
old_ptr,
None,
Size::from_bytes(new_size),
new_align,
MiriMemoryKind::C.into(),
)?;
interp_ok(new_ptr.into())
}
}
}
fn aligned_alloc(
&mut self,
align: &OpTy<'tcx>,
size: &OpTy<'tcx>,
) -> InterpResult<'tcx, Pointer> {
let this = self.eval_context_mut();
let align = this.read_target_usize(align)?;
let size = this.read_target_usize(size)?;
// Alignment must be a power of 2, and "supported by the implementation".
// We decide that "supported by the implementation" means that the
// size must be a multiple of the alignment. (This restriction seems common
// enough that it is stated on <https://en.cppreference.com/w/c/memory/aligned_alloc>
// as a general rule, but the actual standard has no such rule.)
// If any of these are violated, we have to return NULL.
// All fundamental alignments must be supported.
//
// macOS and Illumos are buggy in that they require the alignment
// to be at least the size of a pointer, so they do not support all fundamental
// alignments. We do not emulate those platform bugs.
//
// Linux also sets errno to EINVAL, but that's non-standard behavior that we do not
// emulate.
// FreeBSD says some of these cases are UB but that's violating the C standard.
// http://en.cppreference.com/w/cpp/memory/c/aligned_alloc
// Linux: https://linux.die.net/man/3/aligned_alloc
// FreeBSD: https://man.freebsd.org/cgi/man.cgi?query=aligned_alloc&apropos=0&sektion=3&manpath=FreeBSD+9-current&format=html
match size.checked_rem(align) {
Some(0) if align.is_power_of_two() => {
let align = align.max(this.malloc_align(size).bytes());
let ptr = this.allocate_ptr(
Size::from_bytes(size),
Align::from_bytes(align).unwrap(),
MiriMemoryKind::C.into(),
)?;
interp_ok(ptr.into())
}
_ => interp_ok(Pointer::null()),
}
}
}