Skip to content

Commit 3227e35

Browse files
committed
Auto merge of rust-lang#87248 - RalfJung:ctfe-partial-overwrite, r=oli-obk
CTFE: throw unsupported error when partially overwriting a pointer Currently, during CTFE, when a write to memory would overwrite parts of a pointer, we make the remaining parts of that pointer "uninitialized". This is probably not what users expect, so if this ever happens they will be quite confused about why some of the data just vanishes for seemingly no good reason. So I propose we change this to abort CTFE when that happens, to at last avoid silently doing the wrong thing. Cc rust-lang#87184 Our CTFE test suite still seems to pass. However, we should probably crater this, and I want to do some tests with Miri as well.
2 parents d08460e + 2a9b44d commit 3227e35

File tree

6 files changed

+104
-27
lines changed

6 files changed

+104
-27
lines changed

Diff for: compiler/rustc_middle/src/mir/interpret/allocation.rs

+43-21
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use rustc_span::DUMMY_SP;
1212
use rustc_target::abi::{Align, HasDataLayout, Size};
1313

1414
use super::{
15-
read_target_uint, write_target_uint, AllocId, InterpError, InterpResult, Pointer,
15+
read_target_uint, write_target_uint, AllocId, InterpError, InterpResult, Pointer, Provenance,
1616
ResourceExhaustionInfo, Scalar, ScalarMaybeUninit, UndefinedBehaviorInfo, UninitBytesAccess,
1717
UnsupportedOpInfo,
1818
};
@@ -53,18 +53,22 @@ pub struct Allocation<Tag = AllocId, Extra = ()> {
5353
pub enum AllocError {
5454
/// Encountered a pointer where we needed raw bytes.
5555
ReadPointerAsBytes,
56+
/// Partially overwriting a pointer.
57+
PartialPointerOverwrite(Size),
5658
/// Using uninitialized data where it is not allowed.
5759
InvalidUninitBytes(Option<UninitBytesAccess>),
5860
}
5961
pub type AllocResult<T = ()> = Result<T, AllocError>;
6062

6163
impl AllocError {
6264
pub fn to_interp_error<'tcx>(self, alloc_id: AllocId) -> InterpError<'tcx> {
65+
use AllocError::*;
6366
match self {
64-
AllocError::ReadPointerAsBytes => {
65-
InterpError::Unsupported(UnsupportedOpInfo::ReadPointerAsBytes)
66-
}
67-
AllocError::InvalidUninitBytes(info) => InterpError::UndefinedBehavior(
67+
ReadPointerAsBytes => InterpError::Unsupported(UnsupportedOpInfo::ReadPointerAsBytes),
68+
PartialPointerOverwrite(offset) => InterpError::Unsupported(
69+
UnsupportedOpInfo::PartialPointerOverwrite(Pointer::new(alloc_id, offset)),
70+
),
71+
InvalidUninitBytes(info) => InterpError::UndefinedBehavior(
6872
UndefinedBehaviorInfo::InvalidUninitBytes(info.map(|b| (alloc_id, b))),
6973
),
7074
}
@@ -218,7 +222,7 @@ impl<Tag, Extra> Allocation<Tag, Extra> {
218222
}
219223

220224
/// Byte accessors.
221-
impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
225+
impl<Tag: Provenance, Extra> Allocation<Tag, Extra> {
222226
/// The last argument controls whether we error out when there are uninitialized
223227
/// or pointer bytes. You should never call this, call `get_bytes` or
224228
/// `get_bytes_with_uninit_and_ptr` instead,
@@ -275,30 +279,35 @@ impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
275279
/// It is the caller's responsibility to check bounds and alignment beforehand.
276280
/// Most likely, you want to use the `PlaceTy` and `OperandTy`-based methods
277281
/// on `InterpCx` instead.
278-
pub fn get_bytes_mut(&mut self, cx: &impl HasDataLayout, range: AllocRange) -> &mut [u8] {
282+
pub fn get_bytes_mut(
283+
&mut self,
284+
cx: &impl HasDataLayout,
285+
range: AllocRange,
286+
) -> AllocResult<&mut [u8]> {
279287
self.mark_init(range, true);
280-
self.clear_relocations(cx, range);
288+
self.clear_relocations(cx, range)?;
281289

282-
&mut self.bytes[range.start.bytes_usize()..range.end().bytes_usize()]
290+
Ok(&mut self.bytes[range.start.bytes_usize()..range.end().bytes_usize()])
283291
}
284292

285293
/// A raw pointer variant of `get_bytes_mut` that avoids invalidating existing aliases into this memory.
286-
pub fn get_bytes_mut_ptr(&mut self, cx: &impl HasDataLayout, range: AllocRange) -> *mut [u8] {
294+
pub fn get_bytes_mut_ptr(
295+
&mut self,
296+
cx: &impl HasDataLayout,
297+
range: AllocRange,
298+
) -> AllocResult<*mut [u8]> {
287299
self.mark_init(range, true);
288-
// This also clears relocations that just overlap with the written range. So writing to some
289-
// byte can de-initialize its neighbors! See
290-
// <https://github.com/rust-lang/rust/issues/87184> for details.
291-
self.clear_relocations(cx, range);
300+
self.clear_relocations(cx, range)?;
292301

293302
assert!(range.end().bytes_usize() <= self.bytes.len()); // need to do our own bounds-check
294303
let begin_ptr = self.bytes.as_mut_ptr().wrapping_add(range.start.bytes_usize());
295304
let len = range.end().bytes_usize() - range.start.bytes_usize();
296-
ptr::slice_from_raw_parts_mut(begin_ptr, len)
305+
Ok(ptr::slice_from_raw_parts_mut(begin_ptr, len))
297306
}
298307
}
299308

300309
/// Reading and writing.
301-
impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
310+
impl<Tag: Provenance, Extra> Allocation<Tag, Extra> {
302311
/// Validates that `ptr.offset` and `ptr.offset + size` do not point to the middle of a
303312
/// relocation. If `allow_uninit_and_ptr` is `false`, also enforces that the memory in the
304313
/// given range contains neither relocations nor uninitialized bytes.
@@ -395,7 +404,7 @@ impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
395404
};
396405

397406
let endian = cx.data_layout().endian;
398-
let dst = self.get_bytes_mut(cx, range);
407+
let dst = self.get_bytes_mut(cx, range)?;
399408
write_target_uint(endian, dst, bytes).unwrap();
400409

401410
// See if we have to also write a relocation.
@@ -433,13 +442,16 @@ impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
433442
/// uninitialized. This is a somewhat odd "spooky action at a distance",
434443
/// but it allows strictly more code to run than if we would just error
435444
/// immediately in that case.
436-
fn clear_relocations(&mut self, cx: &impl HasDataLayout, range: AllocRange) {
445+
fn clear_relocations(&mut self, cx: &impl HasDataLayout, range: AllocRange) -> AllocResult
446+
where
447+
Tag: Provenance,
448+
{
437449
// Find the start and end of the given range and its outermost relocations.
438450
let (first, last) = {
439451
// Find all relocations overlapping the given range.
440452
let relocations = self.get_relocations(cx, range);
441453
if relocations.is_empty() {
442-
return;
454+
return Ok(());
443455
}
444456

445457
(
@@ -450,17 +462,27 @@ impl<Tag: Copy, Extra> Allocation<Tag, Extra> {
450462
let start = range.start;
451463
let end = range.end();
452464

453-
// Mark parts of the outermost relocations as uninitialized if they partially fall outside the
454-
// given range.
465+
// We need to handle clearing the relocations from parts of a pointer. See
466+
// <https://github.com/rust-lang/rust/issues/87184> for details.
455467
if first < start {
468+
if Tag::ERR_ON_PARTIAL_PTR_OVERWRITE {
469+
return Err(AllocError::PartialPointerOverwrite(first));
470+
}
456471
self.init_mask.set_range(first, start, false);
457472
}
458473
if last > end {
474+
if Tag::ERR_ON_PARTIAL_PTR_OVERWRITE {
475+
return Err(AllocError::PartialPointerOverwrite(
476+
last - cx.data_layout().pointer_size,
477+
));
478+
}
459479
self.init_mask.set_range(end, last, false);
460480
}
461481

462482
// Forget all the relocations.
463483
self.relocations.0.remove_range(first..last);
484+
485+
Ok(())
464486
}
465487

466488
/// Errors if there are relocations overlapping with the edges of the

Diff for: compiler/rustc_middle/src/mir/interpret/error.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ pub enum UnsupportedOpInfo {
404404
Unsupported(String),
405405
/// Encountered a pointer where we needed raw bytes.
406406
ReadPointerAsBytes,
407+
/// Overwriting parts of a pointer; the resulting state cannot be represented in our
408+
/// `Allocation` data structure.
409+
PartialPointerOverwrite(Pointer<AllocId>),
407410
//
408411
// The variants below are only reachable from CTFE/const prop, miri will never emit them.
409412
//
@@ -418,9 +421,12 @@ impl fmt::Display for UnsupportedOpInfo {
418421
use UnsupportedOpInfo::*;
419422
match self {
420423
Unsupported(ref msg) => write!(f, "{}", msg),
421-
ReadExternStatic(did) => write!(f, "cannot read from extern static ({:?})", did),
422-
ReadPointerAsBytes => write!(f, "unable to turn pointer into raw bytes",),
424+
ReadPointerAsBytes => write!(f, "unable to turn pointer into raw bytes"),
425+
PartialPointerOverwrite(ptr) => {
426+
write!(f, "unable to overwrite parts of a pointer in memory at {:?}", ptr)
427+
}
423428
ThreadLocalStatic(did) => write!(f, "cannot access thread local static ({:?})", did),
429+
ReadExternStatic(did) => write!(f, "cannot read from extern static ({:?})", did),
424430
}
425431
}
426432
}

Diff for: compiler/rustc_middle/src/mir/interpret/pointer.rs

+7
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ pub trait Provenance: Copy + fmt::Debug {
108108
/// If `false`, ptr-to-int casts are not supported. The offset *must* be relative in that case.
109109
const OFFSET_IS_ADDR: bool;
110110

111+
/// We also use this trait to control whether to abort execution when a pointer is being partially overwritten
112+
/// (this avoids a separate trait in `allocation.rs` just for this purpose).
113+
const ERR_ON_PARTIAL_PTR_OVERWRITE: bool;
114+
111115
/// Determines how a pointer should be printed.
112116
fn fmt(ptr: &Pointer<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result
113117
where
@@ -123,6 +127,9 @@ impl Provenance for AllocId {
123127
// so ptr-to-int casts are not possible (since we do not know the global physical offset).
124128
const OFFSET_IS_ADDR: bool = false;
125129

130+
// For now, do not allow this, so that we keep our options open.
131+
const ERR_ON_PARTIAL_PTR_OVERWRITE: bool = true;
132+
126133
fn fmt(ptr: &Pointer<Self>, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127134
// Forward `alternate` flag to `alloc_id` printing.
128135
if f.alternate() {

Diff for: compiler/rustc_mir/src/interpret/memory.rs

+11-4
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ impl<'a, 'mir, 'tcx, M: Machine<'mir, 'tcx>> std::fmt::Debug for DumpAllocs<'a,
907907
}
908908

909909
/// Reading and writing.
910-
impl<'tcx, 'a, Tag: Copy, Extra> AllocRefMut<'a, 'tcx, Tag, Extra> {
910+
impl<'tcx, 'a, Tag: Provenance, Extra> AllocRefMut<'a, 'tcx, Tag, Extra> {
911911
pub fn write_scalar(
912912
&mut self,
913913
range: AllocRange,
@@ -928,7 +928,7 @@ impl<'tcx, 'a, Tag: Copy, Extra> AllocRefMut<'a, 'tcx, Tag, Extra> {
928928
}
929929
}
930930

931-
impl<'tcx, 'a, Tag: Copy, Extra> AllocRef<'a, 'tcx, Tag, Extra> {
931+
impl<'tcx, 'a, Tag: Provenance, Extra> AllocRef<'a, 'tcx, Tag, Extra> {
932932
pub fn read_scalar(&self, range: AllocRange) -> InterpResult<'tcx, ScalarMaybeUninit<Tag>> {
933933
Ok(self
934934
.alloc
@@ -998,7 +998,11 @@ impl<'mir, 'tcx, M: Machine<'mir, 'tcx>> Memory<'mir, 'tcx, M> {
998998

999999
// Side-step AllocRef and directly access the underlying bytes more efficiently.
10001000
// (We are staying inside the bounds here so all is good.)
1001-
let bytes = alloc_ref.alloc.get_bytes_mut(&alloc_ref.tcx, alloc_ref.range);
1001+
let alloc_id = alloc_ref.alloc_id;
1002+
let bytes = alloc_ref
1003+
.alloc
1004+
.get_bytes_mut(&alloc_ref.tcx, alloc_ref.range)
1005+
.map_err(move |e| e.to_interp_error(alloc_id))?;
10021006
// `zip` would stop when the first iterator ends; we want to definitely
10031007
// cover all of `bytes`.
10041008
for dest in bytes {
@@ -1072,7 +1076,10 @@ impl<'mir, 'tcx, M: Machine<'mir, 'tcx>> Memory<'mir, 'tcx, M> {
10721076
let (dest_alloc, extra) = self.get_raw_mut(dest_alloc_id)?;
10731077
let dest_range = alloc_range(dest_offset, size * num_copies);
10741078
M::memory_written(extra, &mut dest_alloc.extra, dest.provenance, dest_range)?;
1075-
let dest_bytes = dest_alloc.get_bytes_mut_ptr(&tcx, dest_range).as_mut_ptr();
1079+
let dest_bytes = dest_alloc
1080+
.get_bytes_mut_ptr(&tcx, dest_range)
1081+
.map_err(|e| e.to_interp_error(dest_alloc_id))?
1082+
.as_mut_ptr();
10761083

10771084
if compressed.no_bytes_init() {
10781085
// Fast path: If all bytes are `uninit` then there is nothing to copy. The target range
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Test for the behavior described in <https://github.com/rust-lang/rust/issues/87184>.
2+
#![feature(const_mut_refs, const_raw_ptr_deref)]
3+
4+
const PARTIAL_OVERWRITE: () = {
5+
let mut p = &42;
6+
unsafe {
7+
let ptr: *mut _ = &mut p;
8+
*(ptr as *mut u8) = 123; //~ ERROR any use of this value
9+
//~| unable to overwrite parts of a pointer
10+
//~| WARN previously accepted
11+
}
12+
let x = *p;
13+
};
14+
15+
fn main() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
error: any use of this value will cause an error
2+
--> $DIR/partial_ptr_overwrite.rs:8:9
3+
|
4+
LL | / const PARTIAL_OVERWRITE: () = {
5+
LL | | let mut p = &42;
6+
LL | | unsafe {
7+
LL | | let ptr: *mut _ = &mut p;
8+
LL | | *(ptr as *mut u8) = 123;
9+
| | ^^^^^^^^^^^^^^^^^^^^^^^ unable to overwrite parts of a pointer in memory at alloc4
10+
... |
11+
LL | | let x = *p;
12+
LL | | };
13+
| |__-
14+
|
15+
= note: `#[deny(const_err)]` on by default
16+
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
17+
= note: for more information, see issue #71800 <https://github.com/rust-lang/rust/issues/71800>
18+
19+
error: aborting due to previous error
20+

0 commit comments

Comments
 (0)