Auto merge of #95826 - carbotaniuman:miri-permissive-provenance, r=RalfJung

Initial work on Miri permissive-exposed-provenance

Rustc portion of the changes for portions of a permissive ptr-to-int model for Miri. The main changes here are changing `ptr_get_alloc` and `get_alloc_id` to return an Option, and also making ptr-to-int casts have an expose side effect.
This commit is contained in:
bors 2022-05-14 10:36:47 +00:00
commit 8019fa0dc0
12 changed files with 142 additions and 45 deletions

View File

@ -18,7 +18,7 @@ use rustc_target::spec::abi::Abi;
use crate::interpret::{ use crate::interpret::{
self, compile_time_machine, AllocId, ConstAllocation, Frame, ImmTy, InterpCx, InterpResult, self, compile_time_machine, AllocId, ConstAllocation, Frame, ImmTy, InterpCx, InterpResult,
OpTy, PlaceTy, Scalar, StackPopUnwind, OpTy, PlaceTy, Pointer, Scalar, StackPopUnwind,
}; };
use super::error::*; use super::error::*;
@ -443,6 +443,14 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for CompileTimeInterpreter<'mir,
Ok(()) Ok(())
} }
#[inline(always)]
fn expose_ptr(
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
_ptr: Pointer<AllocId>,
) -> InterpResult<'tcx> {
Err(ConstEvalErrKind::NeedsRfc("exposing pointers".to_string()).into())
}
#[inline(always)] #[inline(always)]
fn init_frame_extra( fn init_frame_extra(
ecx: &mut InterpCx<'mir, 'tcx, Self>, ecx: &mut InterpCx<'mir, 'tcx, Self>,

View File

@ -98,7 +98,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
} }
pub fn misc_cast( pub fn misc_cast(
&self, &mut self,
src: &ImmTy<'tcx, M::PointerTag>, src: &ImmTy<'tcx, M::PointerTag>,
cast_ty: Ty<'tcx>, cast_ty: Ty<'tcx>,
) -> InterpResult<'tcx, Immediate<M::PointerTag>> { ) -> InterpResult<'tcx, Immediate<M::PointerTag>> {
@ -139,7 +139,9 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
if let Some(discr) = src.layout.ty.discriminant_for_variant(*self.tcx, index) { if let Some(discr) = src.layout.ty.discriminant_for_variant(*self.tcx, index) {
assert!(src.layout.is_zst()); assert!(src.layout.is_zst());
let discr_layout = self.layout_of(discr.ty)?; let discr_layout = self.layout_of(discr.ty)?;
return Ok(self.cast_from_int_like(discr.val, discr_layout, cast_ty).into());
let scalar = Scalar::from_uint(discr.val, discr_layout.layout.size());
return Ok(self.cast_from_int_like(scalar, discr_layout, cast_ty)?.into());
} }
} }
Variants::Multiple { .. } => {} Variants::Multiple { .. } => {}
@ -170,38 +172,65 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
} }
// # The remaining source values are scalar and "int-like". // # The remaining source values are scalar and "int-like".
let scalar = src.to_scalar()?;
// For all remaining casts, we either // If we are casting from a pointer to something
// (a) cast a raw ptr to usize, or // that is not a pointer, mark the pointer as exposed
// (b) cast from an integer-like (including bool, char, enums). if src.layout.ty.is_any_ptr() && !cast_ty.is_any_ptr() {
// In both cases we want the bits. let ptr = self.scalar_to_ptr(scalar)?;
let bits = src.to_scalar()?.to_bits(src.layout.size)?;
Ok(self.cast_from_int_like(bits, src.layout, cast_ty).into()) match ptr.into_pointer_or_addr() {
Ok(ptr) => {
M::expose_ptr(self, ptr)?;
}
Err(_) => {
// do nothing, exposing an invalid pointer
// has no meaning
}
};
}
Ok(self.cast_from_int_like(scalar, src.layout, cast_ty)?.into())
} }
fn cast_from_int_like( pub fn cast_from_int_like(
&self, &self,
v: u128, // raw bits (there is no ScalarTy so we separate data+layout) scalar: Scalar<M::PointerTag>, // input value (there is no ScalarTy so we separate data+layout)
src_layout: TyAndLayout<'tcx>, src_layout: TyAndLayout<'tcx>,
cast_ty: Ty<'tcx>, cast_ty: Ty<'tcx>,
) -> Scalar<M::PointerTag> { ) -> InterpResult<'tcx, Scalar<M::PointerTag>> {
// Let's make sure v is sign-extended *if* it has a signed type. // Let's make sure v is sign-extended *if* it has a signed type.
let signed = src_layout.abi.is_signed(); // Also asserts that abi is `Scalar`. let signed = src_layout.abi.is_signed(); // Also asserts that abi is `Scalar`.
let v = scalar.to_bits(src_layout.size)?;
let v = if signed { self.sign_extend(v, src_layout) } else { v }; let v = if signed { self.sign_extend(v, src_layout) } else { v };
trace!("cast_from_scalar: {}, {} -> {}", v, src_layout.ty, cast_ty); trace!("cast_from_scalar: {}, {} -> {}", v, src_layout.ty, cast_ty);
use rustc_middle::ty::TyKind::*; use rustc_middle::ty::TyKind::*;
match *cast_ty.kind() {
Int(_) | Uint(_) | RawPtr(_) => { Ok(match *cast_ty.kind() {
Int(_) | Uint(_) => {
let size = match *cast_ty.kind() { let size = match *cast_ty.kind() {
Int(t) => Integer::from_int_ty(self, t).size(), Int(t) => Integer::from_int_ty(self, t).size(),
Uint(t) => Integer::from_uint_ty(self, t).size(), Uint(t) => Integer::from_uint_ty(self, t).size(),
RawPtr(_) => self.pointer_size(),
_ => bug!(), _ => bug!(),
}; };
let v = size.truncate(v); let v = size.truncate(v);
Scalar::from_uint(v, size) Scalar::from_uint(v, size)
} }
RawPtr(_) => {
assert!(src_layout.ty.is_integral());
let size = self.pointer_size();
let addr = u64::try_from(size.truncate(v)).unwrap();
let ptr = M::ptr_from_addr_cast(&self, addr);
if addr == 0 {
assert!(ptr.provenance.is_none(), "null pointer can never have an AllocId");
}
Scalar::from_maybe_pointer(ptr, self)
}
Float(FloatTy::F32) if signed => Scalar::from_f32(Single::from_i128(v as i128).value), Float(FloatTy::F32) if signed => Scalar::from_f32(Single::from_i128(v as i128).value),
Float(FloatTy::F64) if signed => Scalar::from_f64(Double::from_i128(v as i128).value), Float(FloatTy::F64) if signed => Scalar::from_f64(Double::from_i128(v as i128).value),
Float(FloatTy::F32) => Scalar::from_f32(Single::from_u128(v).value), Float(FloatTy::F32) => Scalar::from_f32(Single::from_u128(v).value),
@ -214,7 +243,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
// Casts to bool are not permitted by rustc, no need to handle them here. // Casts to bool are not permitted by rustc, no need to handle them here.
_ => span_bug!(self.cur_span(), "invalid int to {:?} cast", cast_ty), _ => span_bug!(self.cur_span(), "invalid int to {:?} cast", cast_ty),
} })
} }
fn cast_from_float<F>(&self, f: F, dest_ty: Ty<'tcx>) -> Scalar<M::PointerTag> fn cast_from_float<F>(&self, f: F, dest_ty: Ty<'tcx>) -> Scalar<M::PointerTag>

View File

@ -905,7 +905,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
trace!( trace!(
"deallocating local {:?}: {:?}", "deallocating local {:?}: {:?}",
local, local,
self.dump_alloc(ptr.provenance.unwrap().get_alloc_id()) // Locals always have a `alloc_id` (they are never the result of a int2ptr).
self.dump_alloc(ptr.provenance.unwrap().get_alloc_id().unwrap())
); );
self.deallocate_ptr(ptr, None, MemoryKind::Stack)?; self.deallocate_ptr(ptr, None, MemoryKind::Stack)?;
}; };
@ -1013,9 +1014,13 @@ impl<'a, 'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> std::fmt::Debug
} }
} }
write!(fmt, ": {:?}", self.ecx.dump_allocs(allocs)) write!(
fmt,
": {:?}",
self.ecx.dump_allocs(allocs.into_iter().filter_map(|x| x).collect())
)
} }
Place::Ptr(mplace) => match mplace.ptr.provenance.map(Provenance::get_alloc_id) { Place::Ptr(mplace) => match mplace.ptr.provenance.and_then(Provenance::get_alloc_id) {
Some(alloc_id) => write!( Some(alloc_id) => write!(
fmt, fmt,
"by align({}) ref {:?}: {:?}", "by align({}) ref {:?}: {:?}",

View File

@ -286,19 +286,36 @@ pub trait Machine<'mir, 'tcx>: Sized {
) -> Pointer<Self::PointerTag>; ) -> Pointer<Self::PointerTag>;
/// "Int-to-pointer cast" /// "Int-to-pointer cast"
fn ptr_from_addr( fn ptr_from_addr_cast(
ecx: &InterpCx<'mir, 'tcx, Self>, ecx: &InterpCx<'mir, 'tcx, Self>,
addr: u64, addr: u64,
) -> Pointer<Option<Self::PointerTag>>; ) -> Pointer<Option<Self::PointerTag>>;
// FIXME: Transmuting an integer to a pointer should just always return a `None`
// provenance, but that causes problems with function pointers in Miri.
/// Hook for returning a pointer from a transmute-like operation on an addr.
fn ptr_from_addr_transmute(
ecx: &InterpCx<'mir, 'tcx, Self>,
addr: u64,
) -> Pointer<Option<Self::PointerTag>>;
/// Marks a pointer as exposed, allowing it's provenance
/// to be recovered. "Pointer-to-int cast"
fn expose_ptr(
ecx: &mut InterpCx<'mir, 'tcx, Self>,
ptr: Pointer<Self::PointerTag>,
) -> InterpResult<'tcx>;
/// Convert a pointer with provenance into an allocation-offset pair /// Convert a pointer with provenance into an allocation-offset pair
/// and extra provenance info. /// and extra provenance info.
/// ///
/// The returned `AllocId` must be the same as `ptr.provenance.get_alloc_id()`. /// The returned `AllocId` must be the same as `ptr.provenance.get_alloc_id()`.
///
/// When this fails, that means the pointer does not point to a live allocation.
fn ptr_get_alloc( fn ptr_get_alloc(
ecx: &InterpCx<'mir, 'tcx, Self>, ecx: &InterpCx<'mir, 'tcx, Self>,
ptr: Pointer<Self::PointerTag>, ptr: Pointer<Self::PointerTag>,
) -> (AllocId, Size, Self::TagExtra); ) -> Option<(AllocId, Size, Self::TagExtra)>;
/// Called to initialize the "extra" state of an allocation and make the pointers /// Called to initialize the "extra" state of an allocation and make the pointers
/// it contains (in relocations) tagged. The way we construct allocations is /// it contains (in relocations) tagged. The way we construct allocations is
@ -480,7 +497,18 @@ pub macro compile_time_machine(<$mir: lifetime, $tcx: lifetime>) {
} }
#[inline(always)] #[inline(always)]
fn ptr_from_addr(_ecx: &InterpCx<$mir, $tcx, Self>, addr: u64) -> Pointer<Option<AllocId>> { fn ptr_from_addr_transmute(
_ecx: &InterpCx<$mir, $tcx, Self>,
addr: u64,
) -> Pointer<Option<AllocId>> {
Pointer::new(None, Size::from_bytes(addr))
}
#[inline(always)]
fn ptr_from_addr_cast(
_ecx: &InterpCx<$mir, $tcx, Self>,
addr: u64,
) -> Pointer<Option<AllocId>> {
Pointer::new(None, Size::from_bytes(addr)) Pointer::new(None, Size::from_bytes(addr))
} }
@ -488,9 +516,9 @@ pub macro compile_time_machine(<$mir: lifetime, $tcx: lifetime>) {
fn ptr_get_alloc( fn ptr_get_alloc(
_ecx: &InterpCx<$mir, $tcx, Self>, _ecx: &InterpCx<$mir, $tcx, Self>,
ptr: Pointer<AllocId>, ptr: Pointer<AllocId>,
) -> (AllocId, Size, Self::TagExtra) { ) -> Option<(AllocId, Size, Self::TagExtra)> {
// We know `offset` is relative to the allocation, so we can use `into_parts`. // We know `offset` is relative to the allocation, so we can use `into_parts`.
let (alloc_id, offset) = ptr.into_parts(); let (alloc_id, offset) = ptr.into_parts();
(alloc_id, offset, ()) Some((alloc_id, offset, ()))
} }
} }

View File

@ -770,7 +770,9 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
if reachable.insert(id) { if reachable.insert(id) {
// This is a new allocation, add its relocations to `todo`. // This is a new allocation, add its relocations to `todo`.
if let Some((_, alloc)) = self.memory.alloc_map.get(id) { if let Some((_, alloc)) = self.memory.alloc_map.get(id) {
todo.extend(alloc.relocations().values().map(|tag| tag.get_alloc_id())); todo.extend(
alloc.relocations().values().filter_map(|tag| tag.get_alloc_id()),
);
} }
} }
} }
@ -805,7 +807,7 @@ impl<'a, 'mir, 'tcx, M: Machine<'mir, 'tcx>> std::fmt::Debug for DumpAllocs<'a,
allocs_to_print: &mut VecDeque<AllocId>, allocs_to_print: &mut VecDeque<AllocId>,
alloc: &Allocation<Tag, Extra>, alloc: &Allocation<Tag, Extra>,
) -> std::fmt::Result { ) -> std::fmt::Result {
for alloc_id in alloc.relocations().values().map(|tag| tag.get_alloc_id()) { for alloc_id in alloc.relocations().values().filter_map(|tag| tag.get_alloc_id()) {
allocs_to_print.push_back(alloc_id); allocs_to_print.push_back(alloc_id);
} }
write!(fmt, "{}", display_allocation(tcx, alloc)) write!(fmt, "{}", display_allocation(tcx, alloc))
@ -1142,7 +1144,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
Err(ptr) => ptr.into(), Err(ptr) => ptr.into(),
Ok(bits) => { Ok(bits) => {
let addr = u64::try_from(bits).unwrap(); let addr = u64::try_from(bits).unwrap();
let ptr = M::ptr_from_addr(&self, addr); let ptr = M::ptr_from_addr_transmute(&self, addr);
if addr == 0 { if addr == 0 {
assert!(ptr.provenance.is_none(), "null pointer can never have an AllocId"); assert!(ptr.provenance.is_none(), "null pointer can never have an AllocId");
} }
@ -1182,10 +1184,14 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
ptr: Pointer<Option<M::PointerTag>>, ptr: Pointer<Option<M::PointerTag>>,
) -> Result<(AllocId, Size, M::TagExtra), u64> { ) -> Result<(AllocId, Size, M::TagExtra), u64> {
match ptr.into_pointer_or_addr() { match ptr.into_pointer_or_addr() {
Ok(ptr) => { Ok(ptr) => match M::ptr_get_alloc(self, ptr) {
let (alloc_id, offset, extra) = M::ptr_get_alloc(self, ptr); Some((alloc_id, offset, extra)) => Ok((alloc_id, offset, extra)),
Ok((alloc_id, offset, extra)) None => {
} assert!(M::PointerTag::OFFSET_IS_ADDR);
let (_, addr) = ptr.into_parts();
Err(addr.bytes())
}
},
Err(addr) => Err(addr.bytes()), Err(addr) => Err(addr.bytes()),
} }
} }

View File

@ -741,17 +741,18 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
// Figure out which discriminant and variant this corresponds to. // Figure out which discriminant and variant this corresponds to.
Ok(match *tag_encoding { Ok(match *tag_encoding {
TagEncoding::Direct => { TagEncoding::Direct => {
let scalar = tag_val.to_scalar()?;
// Generate a specific error if `tag_val` is not an integer. // Generate a specific error if `tag_val` is not an integer.
// (`tag_bits` itself is only used for error messages below.) // (`tag_bits` itself is only used for error messages below.)
let tag_bits = tag_val let tag_bits = scalar
.to_scalar()?
.try_to_int() .try_to_int()
.map_err(|dbg_val| err_ub!(InvalidTag(dbg_val)))? .map_err(|dbg_val| err_ub!(InvalidTag(dbg_val)))?
.assert_bits(tag_layout.size); .assert_bits(tag_layout.size);
// Cast bits from tag layout to discriminant layout. // Cast bits from tag layout to discriminant layout.
// After the checks we did above, this cannot fail. // After the checks we did above, this cannot fail, as
// discriminants are int-like.
let discr_val = let discr_val =
self.misc_cast(&tag_val, discr_layout.ty).unwrap().to_scalar().unwrap(); self.cast_from_int_like(scalar, tag_val.layout, discr_layout.ty).unwrap();
let discr_bits = discr_val.assert_bits(discr_layout.size); let discr_bits = discr_val.assert_bits(discr_layout.size);
// Convert discriminant to variant index, and catch invalid discriminants. // Convert discriminant to variant index, and catch invalid discriminants.
let index = match *op.layout.ty.kind() { let index = match *op.layout.ty.kind() {

View File

@ -120,9 +120,11 @@ pub trait Provenance: Copy + fmt::Debug {
where where
Self: Sized; Self: Sized;
/// Provenance must always be able to identify the allocation this ptr points to. /// If `OFFSET_IS_ADDR == false`, provenance must always be able to
/// identify the allocation this ptr points to (i.e., this must return `Some`).
/// Otherwise this function is best-effort (but must agree with `Machine::ptr_get_alloc`).
/// (Identifying the offset in that allocation, however, is harder -- use `Memory::ptr_get_alloc` for that.) /// (Identifying the offset in that allocation, however, is harder -- use `Memory::ptr_get_alloc` for that.)
fn get_alloc_id(self) -> AllocId; fn get_alloc_id(self) -> Option<AllocId>;
} }
impl Provenance for AllocId { impl Provenance for AllocId {
@ -147,8 +149,8 @@ impl Provenance for AllocId {
Ok(()) Ok(())
} }
fn get_alloc_id(self) -> AllocId { fn get_alloc_id(self) -> Option<AllocId> {
self Some(self)
} }
} }

View File

@ -344,7 +344,8 @@ impl<'tcx, Tag: Provenance> Scalar<Tag> {
} else { } else {
// We know `offset` is relative, since `OFFSET_IS_ADDR == false`. // We know `offset` is relative, since `OFFSET_IS_ADDR == false`.
let (tag, offset) = ptr.into_parts(); let (tag, offset) = ptr.into_parts();
Err(Scalar::Ptr(Pointer::new(tag.get_alloc_id(), offset), sz)) // Because `OFFSET_IS_ADDR == false`, this unwrap can never fail.
Err(Scalar::Ptr(Pointer::new(tag.get_alloc_id().unwrap(), offset), sz))
} }
} }
} }

View File

@ -28,7 +28,8 @@ use crate::MirPass;
use rustc_const_eval::interpret::{ use rustc_const_eval::interpret::{
self, compile_time_machine, AllocId, ConstAllocation, ConstValue, CtfeValidationMode, Frame, self, compile_time_machine, AllocId, ConstAllocation, ConstValue, CtfeValidationMode, Frame,
ImmTy, Immediate, InterpCx, InterpResult, LocalState, LocalValue, MemPlace, MemoryKind, OpTy, ImmTy, Immediate, InterpCx, InterpResult, LocalState, LocalValue, MemPlace, MemoryKind, OpTy,
Operand as InterpOperand, PlaceTy, Scalar, ScalarMaybeUninit, StackPopCleanup, StackPopUnwind, Operand as InterpOperand, PlaceTy, Pointer, Scalar, ScalarMaybeUninit, StackPopCleanup,
StackPopUnwind,
}; };
/// The maximum number of bytes that we'll allocate space for a local or the return value. /// The maximum number of bytes that we'll allocate space for a local or the return value.
@ -285,6 +286,14 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for ConstPropMachine<'mir, 'tcx>
Ok(()) Ok(())
} }
#[inline(always)]
fn expose_ptr(
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
_ptr: Pointer<AllocId>,
) -> InterpResult<'tcx> {
throw_machine_stop_str!("exposing pointers isn't supported in ConstProp")
}
#[inline(always)] #[inline(always)]
fn init_frame_extra( fn init_frame_extra(
_ecx: &mut InterpCx<'mir, 'tcx, Self>, _ecx: &mut InterpCx<'mir, 'tcx, Self>,

View File

@ -30,8 +30,8 @@ use crate::MirLint;
use rustc_const_eval::const_eval::ConstEvalErr; use rustc_const_eval::const_eval::ConstEvalErr;
use rustc_const_eval::interpret::{ use rustc_const_eval::interpret::{
self, compile_time_machine, AllocId, ConstAllocation, Frame, ImmTy, InterpCx, InterpResult, self, compile_time_machine, AllocId, ConstAllocation, Frame, ImmTy, InterpCx, InterpResult,
LocalState, LocalValue, MemPlace, MemoryKind, OpTy, Operand as InterpOperand, PlaceTy, Scalar, LocalState, LocalValue, MemPlace, MemoryKind, OpTy, Operand as InterpOperand, PlaceTy, Pointer,
ScalarMaybeUninit, StackPopCleanup, StackPopUnwind, Scalar, ScalarMaybeUninit, StackPopCleanup, StackPopUnwind,
}; };
/// The maximum number of bytes that we'll allocate space for a local or the return value. /// The maximum number of bytes that we'll allocate space for a local or the return value.
@ -280,6 +280,14 @@ impl<'mir, 'tcx> interpret::Machine<'mir, 'tcx> for ConstPropMachine<'mir, 'tcx>
Ok(()) Ok(())
} }
#[inline(always)]
fn expose_ptr(
_ecx: &mut InterpCx<'mir, 'tcx, Self>,
_ptr: Pointer<AllocId>,
) -> InterpResult<'tcx> {
throw_machine_stop_str!("exposing pointers isn't supported in ConstProp")
}
#[inline(always)] #[inline(always)]
fn init_frame_extra( fn init_frame_extra(
_ecx: &mut InterpCx<'mir, 'tcx, Self>, _ecx: &mut InterpCx<'mir, 'tcx, Self>,

View File

@ -14,7 +14,7 @@ static CMP: () = {
static PTR_INT_CAST: () = { static PTR_INT_CAST: () = {
let x = &0 as *const _ as usize; let x = &0 as *const _ as usize;
//~^ ERROR could not evaluate static initializer //~^ ERROR could not evaluate static initializer
//~| unable to turn pointer into raw bytes //~| "exposing pointers" needs an rfc before being allowed inside constants
let _v = x == x; let _v = x == x;
}; };

View File

@ -8,7 +8,7 @@ error[E0080]: could not evaluate static initializer
--> $DIR/ptr_arith.rs:15:13 --> $DIR/ptr_arith.rs:15:13
| |
LL | let x = &0 as *const _ as usize; LL | let x = &0 as *const _ as usize;
| ^^^^^^^^^^^^^^^^^^^^^^^ unable to turn pointer into raw bytes | ^^^^^^^^^^^^^^^^^^^^^^^ "exposing pointers" needs an rfc before being allowed inside constants
error[E0080]: could not evaluate static initializer error[E0080]: could not evaluate static initializer
--> $DIR/ptr_arith.rs:23:14 --> $DIR/ptr_arith.rs:23:14