Skip to content

Commit

Permalink
Auto merge of #62081 - RalfJung:miri-pointer-checks, r=oli-obk
Browse files Browse the repository at this point in the history
Refactor miri pointer checks

Centralize bounds, alignment and NULL checking for memory accesses in one function: `memory.check_ptr_access`. That function also takes care of converting a `Scalar` to a `Pointer`, should that be needed.  Not all accesses need that though: if the access has size 0, `None` is returned. Everyone accessing memory based on a `Scalar` should use this method to get the `Pointer` they need.

All operations on the `Allocation` work on `Pointer` inputs and expect all the checks to have happened (and will ICE if the bounds are violated). The operations on `Memory` work on `Scalar` inputs and do the checks themselves.

The only other public method to check pointers is `memory.ptr_may_be_null`, which is needed in a few places. No need for `check_align` or similar methods. That makes the public API surface much easier to use and harder to mis-use.

This should be largely no-functional-change, except that ZST accesses to a "true" pointer that is dangling or out-of-bounds are now considered UB. This is to be conservative wrt. whatever LLVM might be doing.

While I am at it, this also removes the assumption that the vtable part of a `dyn Trait`-fat-pointer is a `Pointer` (as opposed to a pointer cast to an integer, stored as raw bits).

r? @oli-obk
  • Loading branch information
bors committed Jun 24, 2019
2 parents bbbb3e5 + 7e83028 commit 7e08576
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 253 deletions.
133 changes: 52 additions & 81 deletions src/librustc/mir/interpret/allocation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,13 @@ use super::{

use crate::ty::layout::{Size, Align};
use syntax::ast::Mutability;
use std::{iter, fmt::{self, Display}};
use std::iter;
use crate::mir;
use std::ops::{Deref, DerefMut};
use std::ops::{Range, Deref, DerefMut};
use rustc_data_structures::sorted_map::SortedMap;
use rustc_macros::HashStable;
use rustc_target::abi::HasDataLayout;
use std::borrow::Cow;

/// Used by `check_bounds` to indicate whether the pointer needs to be just inbounds
/// or also inbounds of a *live* allocation.
#[derive(Debug, Copy, Clone, RustcEncodable, RustcDecodable, HashStable)]
pub enum InboundsCheck {
Live,
MaybeDead,
}

/// Used by `check_in_alloc` to indicate context of check
#[derive(Debug, Copy, Clone, RustcEncodable, RustcDecodable, HashStable)]
pub enum CheckInAllocMsg {
MemoryAccessTest,
NullPointerTest,
PointerArithmeticTest,
InboundsTest,
}

impl Display for CheckInAllocMsg {
/// When this is printed as an error the context looks like this
/// "{test name} failed: pointer must be in-bounds at offset..."
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match *self {
CheckInAllocMsg::MemoryAccessTest => "Memory access",
CheckInAllocMsg::NullPointerTest => "Null pointer test",
CheckInAllocMsg::PointerArithmeticTest => "Pointer arithmetic",
CheckInAllocMsg::InboundsTest => "Inbounds test",
})
}
}

#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, RustcEncodable, RustcDecodable)]
pub struct Allocation<Tag=(),Extra=()> {
/// The actual bytes of the allocation.
Expand Down Expand Up @@ -146,54 +115,48 @@ impl<Tag> Allocation<Tag> {

impl<'tcx> ::serialize::UseSpecializedDecodable for &'tcx Allocation {}

/// Alignment and bounds checks
impl<'tcx, Tag, Extra> Allocation<Tag, Extra> {
/// Checks if the pointer is "in-bounds". Notice that a pointer pointing at the end
/// of an allocation (i.e., at the first *inaccessible* location) *is* considered
/// in-bounds! This follows C's/LLVM's rules.
/// If you want to check bounds before doing a memory access, better use `check_bounds`.
fn check_bounds_ptr(
&self,
ptr: Pointer<Tag>,
msg: CheckInAllocMsg,
) -> InterpResult<'tcx> {
let allocation_size = self.bytes.len() as u64;
ptr.check_in_alloc(Size::from_bytes(allocation_size), msg)
}

/// Checks if the memory range beginning at `ptr` and of size `Size` is "in-bounds".
#[inline(always)]
pub fn check_bounds(
/// Byte accessors
impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
/// Just a small local helper function to avoid a bit of code repetition.
/// Returns the range of this allocation that was meant.
#[inline]
fn check_bounds(
&self,
cx: &impl HasDataLayout,
ptr: Pointer<Tag>,
size: Size,
msg: CheckInAllocMsg,
) -> InterpResult<'tcx> {
// if ptr.offset is in bounds, then so is ptr (because offset checks for overflow)
self.check_bounds_ptr(ptr.offset(size, cx)?, msg)
offset: Size,
size: Size
) -> Range<usize> {
let end = offset + size; // this does overflow checking
assert_eq!(
end.bytes() as usize as u64, end.bytes(),
"cannot handle this access on this host architecture"
);
let end = end.bytes() as usize;
assert!(
end <= self.bytes.len(),
"Out-of-bounds access at offset {}, size {} in allocation of size {}",
offset.bytes(), size.bytes(), self.bytes.len()
);
(offset.bytes() as usize)..end
}
}

/// Byte accessors
impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
/// The last argument controls whether we error out when there are undefined
/// or pointer bytes. You should never call this, call `get_bytes` or
/// `get_bytes_with_undef_and_ptr` instead,
///
/// This function also guarantees that the resulting pointer will remain stable
/// even when new allocations are pushed to the `HashMap`. `copy_repeatedly` relies
/// on that.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
fn get_bytes_internal(
&self,
cx: &impl HasDataLayout,
ptr: Pointer<Tag>,
size: Size,
check_defined_and_ptr: bool,
msg: CheckInAllocMsg,
) -> InterpResult<'tcx, &[u8]>
{
self.check_bounds(cx, ptr, size, msg)?;
let range = self.check_bounds(ptr.offset, size);

if check_defined_and_ptr {
self.check_defined(ptr, size)?;
Expand All @@ -205,12 +168,13 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {

AllocationExtra::memory_read(self, ptr, size)?;

assert_eq!(ptr.offset.bytes() as usize as u64, ptr.offset.bytes());
assert_eq!(size.bytes() as usize as u64, size.bytes());
let offset = ptr.offset.bytes() as usize;
Ok(&self.bytes[offset..offset + size.bytes() as usize])
Ok(&self.bytes[range])
}

/// Check that these bytes are initialized and not pointer bytes, and then return them
/// as a slice.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
#[inline]
pub fn get_bytes(
&self,
Expand All @@ -219,11 +183,13 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
size: Size,
) -> InterpResult<'tcx, &[u8]>
{
self.get_bytes_internal(cx, ptr, size, true, CheckInAllocMsg::MemoryAccessTest)
self.get_bytes_internal(cx, ptr, size, true)
}

/// It is the caller's responsibility to handle undefined and pointer bytes.
/// However, this still checks that there are no relocations on the *edges*.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
#[inline]
pub fn get_bytes_with_undef_and_ptr(
&self,
Expand All @@ -232,30 +198,28 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
size: Size,
) -> InterpResult<'tcx, &[u8]>
{
self.get_bytes_internal(cx, ptr, size, false, CheckInAllocMsg::MemoryAccessTest)
self.get_bytes_internal(cx, ptr, size, false)
}

/// Just calling this already marks everything as defined and removes relocations,
/// so be sure to actually put data there!
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn get_bytes_mut(
&mut self,
cx: &impl HasDataLayout,
ptr: Pointer<Tag>,
size: Size,
) -> InterpResult<'tcx, &mut [u8]>
{
assert_ne!(size.bytes(), 0, "0-sized accesses should never even get a `Pointer`");
self.check_bounds(cx, ptr, size, CheckInAllocMsg::MemoryAccessTest)?;
let range = self.check_bounds(ptr.offset, size);

self.mark_definedness(ptr, size, true);
self.clear_relocations(cx, ptr, size)?;

AllocationExtra::memory_written(self, ptr, size)?;

assert_eq!(ptr.offset.bytes() as usize as u64, ptr.offset.bytes());
assert_eq!(size.bytes() as usize as u64, size.bytes());
let offset = ptr.offset.bytes() as usize;
Ok(&mut self.bytes[offset..offset + size.bytes() as usize])
Ok(&mut self.bytes[range])
}
}

Expand All @@ -276,9 +240,10 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
let size_with_null = Size::from_bytes((size + 1) as u64);
// Go through `get_bytes` for checks and AllocationExtra hooks.
// We read the null, so we include it in the request, but we want it removed
// from the result!
// from the result, so we do subslicing.
Ok(&self.get_bytes(cx, ptr, size_with_null)?[..size])
}
// This includes the case where `offset` is out-of-bounds to begin with.
None => err!(UnterminatedCString(ptr.erase_tag())),
}
}
Expand Down Expand Up @@ -306,7 +271,7 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {

/// Writes `src` to the memory starting at `ptr.offset`.
///
/// Will do bounds checks on the allocation.
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn write_bytes(
&mut self,
cx: &impl HasDataLayout,
Expand All @@ -320,6 +285,8 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
}

/// Sets `count` bytes starting at `ptr.offset` with `val`. Basically `memset`.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn write_repeat(
&mut self,
cx: &impl HasDataLayout,
Expand All @@ -342,7 +309,7 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
/// * in oder to obtain a `Pointer` we need to check for ZSTness anyway due to integer pointers
/// being valid for ZSTs
///
/// Note: This function does not do *any* alignment checks, you need to do these before calling
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn read_scalar(
&self,
cx: &impl HasDataLayout,
Expand Down Expand Up @@ -378,7 +345,9 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
Ok(ScalarMaybeUndef::Scalar(Scalar::from_uint(bits, size)))
}

/// Note: This function does not do *any* alignment checks, you need to do these before calling
/// Read a pointer-sized scalar.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn read_ptr_sized(
&self,
cx: &impl HasDataLayout,
Expand All @@ -395,7 +364,7 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
/// * in oder to obtain a `Pointer` we need to check for ZSTness anyway due to integer pointers
/// being valid for ZSTs
///
/// Note: This function does not do *any* alignment checks, you need to do these before calling
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn write_scalar(
&mut self,
cx: &impl HasDataLayout,
Expand Down Expand Up @@ -435,7 +404,9 @@ impl<'tcx, Tag: Copy, Extra: AllocationExtra<Tag>> Allocation<Tag, Extra> {
Ok(())
}

/// Note: This function does not do *any* alignment checks, you need to do these before calling
/// Write a pointer-sized scalar.
///
/// It is the caller's responsibility to check bounds and alignment beforehand.
pub fn write_ptr_sized(
&mut self,
cx: &impl HasDataLayout,
Expand Down
7 changes: 2 additions & 5 deletions src/librustc/mir/interpret/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,9 @@ pub use self::error::{

pub use self::value::{Scalar, ScalarMaybeUndef, RawConst, ConstValue};

pub use self::allocation::{
InboundsCheck, Allocation, AllocationExtra,
Relocations, UndefMask, CheckInAllocMsg,
};
pub use self::allocation::{Allocation, AllocationExtra, Relocations, UndefMask};

pub use self::pointer::{Pointer, PointerArithmetic};
pub use self::pointer::{Pointer, PointerArithmetic, CheckInAllocMsg};

use std::fmt;
use crate::mir;
Expand Down
26 changes: 24 additions & 2 deletions src/librustc/mir/interpret/pointer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
use std::fmt;
use std::fmt::{self, Display};

use crate::mir;
use crate::ty::layout::{self, HasDataLayout, Size};
use rustc_macros::HashStable;

use super::{
AllocId, InterpResult, CheckInAllocMsg
AllocId, InterpResult,
};

/// Used by `check_in_alloc` to indicate context of check
#[derive(Debug, Copy, Clone, RustcEncodable, RustcDecodable, HashStable)]
pub enum CheckInAllocMsg {
MemoryAccessTest,
NullPointerTest,
PointerArithmeticTest,
InboundsTest,
}

impl Display for CheckInAllocMsg {
/// When this is printed as an error the context looks like this
/// "{test name} failed: pointer must be in-bounds at offset..."
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", match *self {
CheckInAllocMsg::MemoryAccessTest => "Memory access",
CheckInAllocMsg::NullPointerTest => "Null pointer test",
CheckInAllocMsg::PointerArithmeticTest => "Pointer arithmetic",
CheckInAllocMsg::InboundsTest => "Inbounds test",
})
}
}

////////////////////////////////////////////////////////////////////////////////
// Pointer arithmetic
////////////////////////////////////////////////////////////////////////////////
Expand Down
2 changes: 1 addition & 1 deletion src/librustc_mir/interpret/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ impl<'mir, 'tcx, M: Machine<'mir, 'tcx>> InterpretCx<'mir, 'tcx, M> {
Ok(Some((size.align_to(align), align)))
}
ty::Dynamic(..) => {
let vtable = metadata.expect("dyn trait fat ptr must have vtable").to_ptr()?;
let vtable = metadata.expect("dyn trait fat ptr must have vtable");
// the second entry in the vtable is the dynamic size of the object.
Ok(Some(self.read_size_and_align_from_vtable(vtable)?))
}
Expand Down
Loading

0 comments on commit 7e08576

Please sign in to comment.