Rollup merge of #143634 - nia-e:init-and-wildcards, r=RalfJung

interpret/allocation: expose init + write_wildcards on a range

Part of https://github.com/rust-lang/miri/pull/4456, so that we can mark down when a foreign access to our memory happened. Should this also move `prepare_for_native_access()` itself into Miri, given that everything there can be implemented on Miri's side?

r? `````@RalfJung`````
This commit is contained in:
Matthias Krüger 2025-07-13 15:15:58 +02:00 committed by GitHub
commit 762b3143fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 42 additions and 28 deletions

View file

@ -1498,7 +1498,7 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
dest_alloc
.write_uninit(&tcx, dest_range)
.map_err(|e| e.to_interp_error(dest_alloc_id))?;
// We can forget about the provenance, this is all not initialized anyway.
// `write_uninit` also resets the provenance, so we are done.
return interp_ok(());
}

View file

@ -101,6 +101,8 @@ pub struct Allocation<Prov: Provenance = CtfeProvenance, Extra = (), Bytes = Box
/// at the given offset.
provenance: ProvenanceMap<Prov>,
/// Denotes which part of this allocation is initialized.
///
/// Invariant: the uninitialized parts have no provenance.
init_mask: InitMask,
/// The alignment of the allocation to detect unaligned reads.
/// (`Align` guarantees that this is a power of two.)
@ -796,24 +798,19 @@ impl<Prov: Provenance, Extra, Bytes: AllocBytes> Allocation<Prov, Extra, Bytes>
Ok(())
}
/// Initialize all previously uninitialized bytes in the entire allocation, and set
/// provenance of everything to `Wildcard`. Before calling this, make sure all
/// provenance in this allocation is exposed!
pub fn prepare_for_native_access(&mut self) {
let full_range = AllocRange { start: Size::ZERO, size: Size::from_bytes(self.len()) };
// Overwrite uninitialized bytes with 0, to ensure we don't leak whatever their value happens to be.
for chunk in self.init_mask.range_as_init_chunks(full_range) {
if !chunk.is_init() {
let uninit_bytes = &mut self.bytes
[chunk.range().start.bytes_usize()..chunk.range().end.bytes_usize()];
uninit_bytes.fill(0);
}
}
// Mark everything as initialized now.
self.mark_init(full_range, true);
// Set provenance of all bytes to wildcard.
self.provenance.write_wildcards(self.len());
/// Mark all bytes in the given range as initialised and reset the provenance
/// to wildcards. This entirely breaks the normal mechanisms for tracking
/// initialisation and is only provided for Miri operating in native-lib
/// mode. UB will be missed if the underlying bytes were not actually written to.
///
/// If `range` is `None`, defaults to performing this on the whole allocation.
pub fn process_native_write(&mut self, cx: &impl HasDataLayout, range: Option<AllocRange>) {
let range = range.unwrap_or_else(|| AllocRange {
start: Size::ZERO,
size: Size::from_bytes(self.len()),
});
self.mark_init(range, true);
self.provenance.write_wildcards(cx, range);
}
/// Remove all provenance in the given memory range.

View file

@ -212,21 +212,37 @@ impl<Prov: Provenance> ProvenanceMap<Prov> {
Ok(())
}
/// Overwrites all provenance in the allocation with wildcard provenance.
/// Overwrites all provenance in the given range with wildcard provenance.
/// Pointers partially overwritten will have their provenances preserved
/// bytewise on their remaining bytes.
///
/// Provided for usage in Miri and panics otherwise.
pub fn write_wildcards(&mut self, alloc_size: usize) {
pub fn write_wildcards(&mut self, cx: &impl HasDataLayout, range: AllocRange) {
assert!(
Prov::OFFSET_IS_ADDR,
"writing wildcard provenance is not supported when `OFFSET_IS_ADDR` is false"
);
let wildcard = Prov::WILDCARD.unwrap();
// Remove all pointer provenances, then write wildcards into the whole byte range.
self.ptrs.clear();
let last = Size::from_bytes(alloc_size);
let bytes = self.bytes.get_or_insert_with(Box::default);
for offset in Size::ZERO..last {
// Remove pointer provenances that overlap with the range, then readd the edge ones bytewise.
let ptr_range = Self::adjusted_range_ptrs(range, cx);
let ptrs = self.ptrs.range(ptr_range.clone());
if let Some((offset, prov)) = ptrs.first() {
for byte_ofs in *offset..range.start {
bytes.insert(byte_ofs, *prov);
}
}
if let Some((offset, prov)) = ptrs.last() {
for byte_ofs in range.end()..*offset + cx.data_layout().pointer_size() {
bytes.insert(byte_ofs, *prov);
}
}
self.ptrs.remove_range(ptr_range);
// Overwrite bytewise provenance.
for offset in range.start..range.end() {
bytes.insert(offset, wildcard);
}
}

View file

@ -231,7 +231,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
.collect::<Vec<libffi::high::Arg<'_>>>();
// Prepare all exposed memory (both previously exposed, and just newly exposed since a
// pointer was passed as argument).
// pointer was passed as argument). Uninitialised memory is left as-is, but any data
// exposed this way is garbage anyway.
this.visit_reachable_allocs(this.exposed_allocs(), |this, alloc_id, info| {
// If there is no data behind this pointer, skip this.
if !matches!(info.kind, AllocKind::LiveData) {
@ -251,8 +252,8 @@ pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
// Prepare for possible write from native code if mutable.
if info.mutbl.is_mut() {
let alloc = &mut this.get_alloc_raw_mut(alloc_id)?.0;
alloc.prepare_for_native_access();
let (alloc, cx) = this.get_alloc_raw_mut(alloc_id)?;
alloc.process_native_write(&cx.tcx, None);
// Also expose *mutable* provenance for the interpreter-level allocation.
std::hint::black_box(alloc.get_bytes_unchecked_raw_mut().expose_provenance());
}