Skip to content

Commit 23eb90f

Browse files
committed
Auto merge of #109061 - saethlin:leak-backtraces, r=oli-obk
Add a backtrace to Allocation, display it in leak reports This addresses rust-lang/miri#2813 Information like this from diagnostics is indispensable for diagnosing problems that are difficult to reproduce such as https://github.com/rust-lang/miri-test-libstd/actions/runs/4395316008/jobs/7697019211#step:4:770 (which has not been reproduced or diagnosed).
2 parents d0f204e + fb68292 commit 23eb90f

17 files changed

+191
-62
lines changed

compiler/rustc_const_eval/src/interpret/eval_context.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -132,11 +132,10 @@ pub struct Frame<'mir, 'tcx, Prov: Provenance = AllocId, Extra = ()> {
132132
}
133133

134134
/// What we store about a frame in an interpreter backtrace.
135-
#[derive(Debug)]
135+
#[derive(Clone, Debug)]
136136
pub struct FrameInfo<'tcx> {
137137
pub instance: ty::Instance<'tcx>,
138138
pub span: Span,
139-
pub lint_root: Option<hir::HirId>,
140139
}
141140

142141
#[derive(Clone, Copy, Eq, PartialEq, Debug)] // Miri debug-prints these
@@ -947,10 +946,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
947946
// This deliberately does *not* honor `requires_caller_location` since it is used for much
948947
// more than just panics.
949948
for frame in stack.iter().rev() {
950-
let lint_root = frame.lint_root();
951949
let span = frame.current_span();
952-
953-
frames.push(FrameInfo { span, instance: frame.instance, lint_root });
950+
frames.push(FrameInfo { span, instance: frame.instance });
954951
}
955952
trace!("generate stacktrace: {:#?}", frames);
956953
frames

compiler/rustc_const_eval/src/interpret/machine.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ pub trait Machine<'mir, 'tcx>: Sized {
104104
type FrameExtra;
105105

106106
/// Extra data stored in every allocation.
107-
type AllocExtra: Debug + Clone + 'static;
107+
type AllocExtra: Debug + Clone + 'tcx;
108108

109109
/// Type for the bytes of the allocation.
110110
type Bytes: AllocBytes + 'static;

compiler/rustc_const_eval/src/interpret/memory.rs

+15-12
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
215215
self.allocate_raw_ptr(alloc, kind)
216216
}
217217

218-
/// This can fail only of `alloc` contains provenance.
218+
/// This can fail only if `alloc` contains provenance.
219219
pub fn allocate_raw_ptr(
220220
&mut self,
221221
alloc: Allocation,
@@ -807,9 +807,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
807807
DumpAllocs { ecx: self, allocs }
808808
}
809809

810-
/// Print leaked memory. Allocations reachable from `static_roots` or a `Global` allocation
811-
/// are not considered leaked. Leaks whose kind `may_leak()` returns true are not reported.
812-
pub fn leak_report(&self, static_roots: &[AllocId]) -> usize {
810+
/// Find leaked allocations. Allocations reachable from `static_roots` or a `Global` allocation
811+
/// are not considered leaked, as well as leaks whose kind's `may_leak()` returns true.
812+
pub fn find_leaked_allocations(
813+
&self,
814+
static_roots: &[AllocId],
815+
) -> Vec<(AllocId, MemoryKind<M::MemoryKind>, Allocation<M::Provenance, M::AllocExtra, M::Bytes>)>
816+
{
813817
// Collect the set of allocations that are *reachable* from `Global` allocations.
814818
let reachable = {
815819
let mut reachable = FxHashSet::default();
@@ -833,14 +837,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
833837
};
834838

835839
// All allocations that are *not* `reachable` and *not* `may_leak` are considered leaking.
836-
let leaks: Vec<_> = self.memory.alloc_map.filter_map_collect(|&id, &(kind, _)| {
837-
if kind.may_leak() || reachable.contains(&id) { None } else { Some(id) }
838-
});
839-
let n = leaks.len();
840-
if n > 0 {
841-
eprintln!("The following memory was leaked: {:?}", self.dump_allocs(leaks));
842-
}
843-
n
840+
self.memory.alloc_map.filter_map_collect(|id, (kind, alloc)| {
841+
if kind.may_leak() || reachable.contains(id) {
842+
None
843+
} else {
844+
Some((*id, *kind, alloc.clone()))
845+
}
846+
})
844847
}
845848
}
846849

src/tools/miri/README.md

+9-5
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,22 @@ environment variable. We first document the most relevant and most commonly used
301301
* `-Zmiri-disable-isolation` disables host isolation. As a consequence,
302302
the program has access to host resources such as environment variables, file
303303
systems, and randomness.
304+
* `-Zmiri-disable-leak-backtraces` disables backtraces reports for memory leaks. By default, a
305+
backtrace is captured for every allocation when it is created, just in case it leaks. This incurs
306+
some memory overhead to store data that is almost never used. This flag is implied by
307+
`-Zmiri-ignore-leaks`.
308+
* `-Zmiri-env-forward=<var>` forwards the `var` environment variable to the interpreted program. Can
309+
be used multiple times to forward several variables. Execution will still be deterministic if the
310+
value of forwarded variables stays the same. Has no effect if `-Zmiri-disable-isolation` is set.
311+
* `-Zmiri-ignore-leaks` disables the memory leak checker, and also allows some
312+
remaining threads to exist when the main thread exits.
304313
* `-Zmiri-isolation-error=<action>` configures Miri's response to operations
305314
requiring host access while isolation is enabled. `abort`, `hide`, `warn`,
306315
and `warn-nobacktrace` are the supported actions. The default is to `abort`,
307316
which halts the machine. Some (but not all) operations also support continuing
308317
execution with a "permission denied" error being returned to the program.
309318
`warn` prints a full backtrace when that happens; `warn-nobacktrace` is less
310319
verbose. `hide` hides the warning entirely.
311-
* `-Zmiri-env-forward=<var>` forwards the `var` environment variable to the interpreted program. Can
312-
be used multiple times to forward several variables. Execution will still be deterministic if the
313-
value of forwarded variables stays the same. Has no effect if `-Zmiri-disable-isolation` is set.
314-
* `-Zmiri-ignore-leaks` disables the memory leak checker, and also allows some
315-
remaining threads to exist when the main thread exits.
316320
* `-Zmiri-num-cpus` states the number of available CPUs to be reported by miri. By default, the
317321
number of available CPUs is `1`. Note that this flag does not affect how miri handles threads in
318322
any way.

src/tools/miri/src/bin/miri.rs

+3
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,8 @@ fn main() {
359359
isolation_enabled = Some(false);
360360
}
361361
miri_config.isolated_op = miri::IsolatedOp::Allow;
362+
} else if arg == "-Zmiri-disable-leak-backtraces" {
363+
miri_config.collect_leak_backtraces = false;
362364
} else if arg == "-Zmiri-disable-weak-memory-emulation" {
363365
miri_config.weak_memory_emulation = false;
364366
} else if arg == "-Zmiri-track-weak-memory-loads" {
@@ -385,6 +387,7 @@ fn main() {
385387
};
386388
} else if arg == "-Zmiri-ignore-leaks" {
387389
miri_config.ignore_leaks = true;
390+
miri_config.collect_leak_backtraces = false;
388391
} else if arg == "-Zmiri-panic-on-unsupported" {
389392
miri_config.panic_on_unsupported = true;
390393
} else if arg == "-Zmiri-tag-raw-pointers" {

src/tools/miri/src/borrow_tracker/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ pub enum AllocState {
352352
TreeBorrows(Box<RefCell<tree_borrows::AllocState>>),
353353
}
354354

355-
impl machine::AllocExtra {
355+
impl machine::AllocExtra<'_> {
356356
#[track_caller]
357357
pub fn borrow_tracker_sb(&self) -> &RefCell<stacked_borrows::AllocState> {
358358
match self.borrow_tracker {

src/tools/miri/src/diagnostics.rs

+36-3
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ pub enum NonHaltingDiagnostic {
105105
}
106106

107107
/// Level of Miri specific diagnostics
108-
enum DiagLevel {
108+
pub enum DiagLevel {
109109
Error,
110110
Warning,
111111
Note,
@@ -114,7 +114,7 @@ enum DiagLevel {
114114
/// Attempts to prune a stacktrace to omit the Rust runtime, and returns a bool indicating if any
115115
/// frames were pruned. If the stacktrace does not have any local frames, we conclude that it must
116116
/// be pointing to a problem in the Rust runtime itself, and do not prune it at all.
117-
fn prune_stacktrace<'tcx>(
117+
pub fn prune_stacktrace<'tcx>(
118118
mut stacktrace: Vec<FrameInfo<'tcx>>,
119119
machine: &MiriMachine<'_, 'tcx>,
120120
) -> (Vec<FrameInfo<'tcx>>, bool) {
@@ -338,12 +338,45 @@ pub fn report_error<'tcx, 'mir>(
338338
None
339339
}
340340

341+
pub fn report_leaks<'mir, 'tcx>(
342+
ecx: &InterpCx<'mir, 'tcx, MiriMachine<'mir, 'tcx>>,
343+
leaks: Vec<(AllocId, MemoryKind<MiriMemoryKind>, Allocation<Provenance, AllocExtra<'tcx>>)>,
344+
) {
345+
let mut any_pruned = false;
346+
for (id, kind, mut alloc) in leaks {
347+
let Some(backtrace) = alloc.extra.backtrace.take() else {
348+
continue;
349+
};
350+
let (backtrace, pruned) = prune_stacktrace(backtrace, &ecx.machine);
351+
any_pruned |= pruned;
352+
report_msg(
353+
DiagLevel::Error,
354+
&format!(
355+
"memory leaked: {id:?} ({}, size: {:?}, align: {:?}), allocated here:",
356+
kind,
357+
alloc.size().bytes(),
358+
alloc.align.bytes()
359+
),
360+
vec![],
361+
vec![],
362+
vec![],
363+
&backtrace,
364+
&ecx.machine,
365+
);
366+
}
367+
if any_pruned {
368+
ecx.tcx.sess.diagnostic().note_without_error(
369+
"some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace",
370+
);
371+
}
372+
}
373+
341374
/// Report an error or note (depending on the `error` argument) with the given stacktrace.
342375
/// Also emits a full stacktrace of the interpreter stack.
343376
/// We want to present a multi-line span message for some errors. Diagnostics do not support this
344377
/// directly, so we pass the lines as a `Vec<String>` and display each line after the first with an
345378
/// additional `span_label` or `note` call.
346-
fn report_msg<'tcx>(
379+
pub fn report_msg<'tcx>(
347380
diag_level: DiagLevel,
348381
title: &str,
349382
span_msg: Vec<String>,

src/tools/miri/src/eval.rs

+15-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::thread;
1010
use log::info;
1111

1212
use crate::borrow_tracker::RetagFields;
13+
use crate::diagnostics::report_leaks;
1314
use rustc_data_structures::fx::FxHashSet;
1415
use rustc_hir::def::Namespace;
1516
use rustc_hir::def_id::DefId;
@@ -145,6 +146,8 @@ pub struct MiriConfig {
145146
pub num_cpus: u32,
146147
/// Requires Miri to emulate pages of a certain size
147148
pub page_size: Option<u64>,
149+
/// Whether to collect a backtrace when each allocation is created, just in case it leaks.
150+
pub collect_leak_backtraces: bool,
148151
}
149152

150153
impl Default for MiriConfig {
@@ -179,6 +182,7 @@ impl Default for MiriConfig {
179182
gc_interval: 10_000,
180183
num_cpus: 1,
181184
page_size: None,
185+
collect_leak_backtraces: true,
182186
}
183187
}
184188
}
@@ -457,10 +461,17 @@ pub fn eval_entry<'tcx>(
457461
}
458462
// Check for memory leaks.
459463
info!("Additonal static roots: {:?}", ecx.machine.static_roots);
460-
let leaks = ecx.leak_report(&ecx.machine.static_roots);
461-
if leaks != 0 {
462-
tcx.sess.err("the evaluated program leaked memory");
463-
tcx.sess.note_without_error("pass `-Zmiri-ignore-leaks` to disable this check");
464+
let leaks = ecx.find_leaked_allocations(&ecx.machine.static_roots);
465+
if !leaks.is_empty() {
466+
report_leaks(&ecx, leaks);
467+
let leak_message = "the evaluated program leaked memory, pass `-Zmiri-ignore-leaks` to disable this check";
468+
if ecx.machine.collect_leak_backtraces {
469+
// If we are collecting leak backtraces, each leak is a distinct error diagnostic.
470+
tcx.sess.note_without_error(leak_message);
471+
} else {
472+
// If we do not have backtraces, we just report an error without any span.
473+
tcx.sess.err(leak_message);
474+
};
464475
// Ignore the provided return code - let the reported error
465476
// determine the return code.
466477
return None;

src/tools/miri/src/machine.rs

+39-12
Original file line numberDiff line numberDiff line change
@@ -253,20 +253,25 @@ impl ProvenanceExtra {
253253

254254
/// Extra per-allocation data
255255
#[derive(Debug, Clone)]
256-
pub struct AllocExtra {
256+
pub struct AllocExtra<'tcx> {
257257
/// Global state of the borrow tracker, if enabled.
258258
pub borrow_tracker: Option<borrow_tracker::AllocState>,
259-
/// Data race detection via the use of a vector-clock,
260-
/// this is only added if it is enabled.
259+
/// Data race detection via the use of a vector-clock.
260+
/// This is only added if it is enabled.
261261
pub data_race: Option<data_race::AllocState>,
262-
/// Weak memory emulation via the use of store buffers,
263-
/// this is only added if it is enabled.
262+
/// Weak memory emulation via the use of store buffers.
263+
/// This is only added if it is enabled.
264264
pub weak_memory: Option<weak_memory::AllocState>,
265+
/// A backtrace to where this allocation was allocated.
266+
/// As this is recorded for leak reports, it only exists
267+
/// if this allocation is leakable. The backtrace is not
268+
/// pruned yet; that should be done before printing it.
269+
pub backtrace: Option<Vec<FrameInfo<'tcx>>>,
265270
}
266271

267-
impl VisitTags for AllocExtra {
272+
impl VisitTags for AllocExtra<'_> {
268273
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
269-
let AllocExtra { borrow_tracker, data_race, weak_memory } = self;
274+
let AllocExtra { borrow_tracker, data_race, weak_memory, backtrace: _ } = self;
270275

271276
borrow_tracker.visit_tags(visit);
272277
data_race.visit_tags(visit);
@@ -467,12 +472,17 @@ pub struct MiriMachine<'mir, 'tcx> {
467472
pub(crate) gc_interval: u32,
468473
/// The number of blocks that passed since the last BorTag GC pass.
469474
pub(crate) since_gc: u32,
475+
470476
/// The number of CPUs to be reported by miri.
471477
pub(crate) num_cpus: u32,
478+
472479
/// Determines Miri's page size and associated values
473480
pub(crate) page_size: u64,
474481
pub(crate) stack_addr: u64,
475482
pub(crate) stack_size: u64,
483+
484+
/// Whether to collect a backtrace when each allocation is created, just in case it leaks.
485+
pub(crate) collect_leak_backtraces: bool,
476486
}
477487

478488
impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
@@ -581,6 +591,7 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
581591
page_size,
582592
stack_addr,
583593
stack_size,
594+
collect_leak_backtraces: config.collect_leak_backtraces,
584595
}
585596
}
586597

@@ -728,6 +739,7 @@ impl VisitTags for MiriMachine<'_, '_> {
728739
page_size: _,
729740
stack_addr: _,
730741
stack_size: _,
742+
collect_leak_backtraces: _,
731743
} = self;
732744

733745
threads.visit_tags(visit);
@@ -773,7 +785,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
773785
type ExtraFnVal = Dlsym;
774786

775787
type FrameExtra = FrameExtra<'tcx>;
776-
type AllocExtra = AllocExtra;
788+
type AllocExtra = AllocExtra<'tcx>;
777789

778790
type Provenance = Provenance;
779791
type ProvenanceExtra = ProvenanceExtra;
@@ -967,9 +979,24 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
967979
)
968980
});
969981
let buffer_alloc = ecx.machine.weak_memory.then(weak_memory::AllocState::new_allocation);
982+
983+
// If an allocation is leaked, we want to report a backtrace to indicate where it was
984+
// allocated. We don't need to record a backtrace for allocations which are allowed to
985+
// leak.
986+
let backtrace = if kind.may_leak() || !ecx.machine.collect_leak_backtraces {
987+
None
988+
} else {
989+
Some(ecx.generate_stacktrace())
990+
};
991+
970992
let alloc: Allocation<Provenance, Self::AllocExtra> = alloc.adjust_from_tcx(
971993
&ecx.tcx,
972-
AllocExtra { borrow_tracker, data_race: race_alloc, weak_memory: buffer_alloc },
994+
AllocExtra {
995+
borrow_tracker,
996+
data_race: race_alloc,
997+
weak_memory: buffer_alloc,
998+
backtrace,
999+
},
9731000
|ptr| ecx.global_base_pointer(ptr),
9741001
)?;
9751002
Ok(Cow::Owned(alloc))
@@ -1049,7 +1076,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
10491076
fn before_memory_read(
10501077
_tcx: TyCtxt<'tcx>,
10511078
machine: &Self,
1052-
alloc_extra: &AllocExtra,
1079+
alloc_extra: &AllocExtra<'tcx>,
10531080
(alloc_id, prov_extra): (AllocId, Self::ProvenanceExtra),
10541081
range: AllocRange,
10551082
) -> InterpResult<'tcx> {
@@ -1069,7 +1096,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
10691096
fn before_memory_write(
10701097
_tcx: TyCtxt<'tcx>,
10711098
machine: &mut Self,
1072-
alloc_extra: &mut AllocExtra,
1099+
alloc_extra: &mut AllocExtra<'tcx>,
10731100
(alloc_id, prov_extra): (AllocId, Self::ProvenanceExtra),
10741101
range: AllocRange,
10751102
) -> InterpResult<'tcx> {
@@ -1089,7 +1116,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
10891116
fn before_memory_deallocation(
10901117
_tcx: TyCtxt<'tcx>,
10911118
machine: &mut Self,
1092-
alloc_extra: &mut AllocExtra,
1119+
alloc_extra: &mut AllocExtra<'tcx>,
10931120
(alloc_id, prove_extra): (AllocId, Self::ProvenanceExtra),
10941121
range: AllocRange,
10951122
) -> InterpResult<'tcx> {

src/tools/miri/src/tag_gc.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ impl VisitTags for Operand<Provenance> {
125125
}
126126
}
127127

128-
impl VisitTags for Allocation<Provenance, AllocExtra> {
128+
impl VisitTags for Allocation<Provenance, AllocExtra<'_>> {
129129
fn visit_tags(&self, visit: &mut dyn FnMut(BorTag)) {
130130
for prov in self.provenance().provenances() {
131131
prov.visit_tags(visit);

src/tools/miri/tests/fail/memleak.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//@error-pattern: the evaluated program leaked memory
1+
//@error-pattern: memory leaked
22
//@normalize-stderr-test: ".*│.*" -> "$$stripped$$"
33

44
fn main() {

0 commit comments

Comments
 (0)