Skip to content

Commit a7fa6be

Browse files
committed
test(stack-encrypt): hold the heap spy's gate across a leak watch
watched_for_leaks reset LIVE and set TRACKING before watched took GATE, so under threaded cargo test another watch could erase its records or turn tracking off mid-watch. Take the gate first and run a gate-free inner watch, and release LIVE before asserting so the negative control does not poison it for later watches.
1 parent f90867b commit a7fa6be

1 file changed

Lines changed: 37 additions & 11 deletions

File tree

‎packages/stack-encrypt/tests/heap_hygiene.rs‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,23 @@ fn scrub_stack() {
250250
/// Run `f` with this thread watched, and fail if any block it freed held
251251
/// the marker.
252252
fn watched<T>(what: &str, f: impl FnOnce() -> T) -> T {
253-
let _gate = GATE.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
253+
let _gate = gate();
254+
watched_in_gate(what, f)
255+
}
256+
257+
/// Take [`GATE`], which serializes every watch: the thread-local flags
258+
/// keep other threads' allocations out of a watch, but [`FOUND`],
259+
/// [`UPSTREAM`], [`LIVE`] and [`TRACKING`] are shared, and a test running
260+
/// alongside on another thread (plain `cargo test`) would otherwise reset
261+
/// or switch them off mid-watch. A test that panicked holding it (a
262+
/// `should_panic` control) poisons it; the state it guards is reset on
263+
/// entry, so the poison is ignored.
264+
fn gate() -> std::sync::MutexGuard<'static, ()> {
265+
GATE.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
266+
}
267+
268+
/// [`watched`], for a caller already holding [`GATE`].
269+
fn watched_in_gate<T>(what: &str, f: impl FnOnce() -> T) -> T {
254270
FOUND.lock().unwrap().clear();
255271
*UPSTREAM.lock().unwrap() = 0;
256272
scrub_stack();
@@ -276,6 +292,9 @@ fn watched<T>(what: &str, f: impl FnOnce() -> T) -> T {
276292
/// than freed, which the free-time scan cannot see. `f` returns nothing,
277293
/// so nothing it built is meant to outlive it.
278294
fn watched_for_leaks(what: &str, f: impl FnOnce()) {
295+
// Held from the reset of `LIVE` to the last read of it, so no other
296+
// watch can clear this one's records or turn `TRACKING` off under it.
297+
let _gate = gate();
279298
{
280299
let mut live = LIVE.lock().unwrap();
281300
live.len = 0;
@@ -290,20 +309,27 @@ fn watched_for_leaks(what: &str, f: impl FnOnce()) {
290309
}
291310
}
292311
let stop = Stop;
293-
watched(what, f);
312+
watched_in_gate(what, f);
294313
drop(stop);
295-
let live = LIVE.lock().unwrap();
314+
// Read under the lock and released before asserting, so a failing
315+
// assertion (the negative control's, by design) does not poison
316+
// `LIVE` for the watches that follow in the same process.
317+
let (overflowed, leaked) = {
318+
let live = LIVE.lock().unwrap();
319+
let leaked: Vec<usize> = live.blocks[..live.len]
320+
.iter()
321+
// SAFETY: every recorded block is still allocated: a free
322+
// removes its record while `TRACKING` is on, and it was on
323+
// until `stop`.
324+
.filter(|(ptr, size)| unsafe { holds_mark(*ptr as *const u8, *size) })
325+
.map(|(_, size)| *size)
326+
.collect();
327+
(live.overflowed, leaked)
328+
};
296329
assert!(
297-
!live.overflowed,
330+
!overflowed,
298331
"{what}: more than {LIVE_SLOTS} blocks were live at once; raise LIVE_SLOTS"
299332
);
300-
let leaked: Vec<usize> = live.blocks[..live.len]
301-
.iter()
302-
// SAFETY: every recorded block is still allocated: a free removes
303-
// its record while `TRACKING` is on, and it was on until `stop`.
304-
.filter(|(ptr, size)| unsafe { holds_mark(*ptr as *const u8, *size) })
305-
.map(|(_, size)| *size)
306-
.collect();
307333
assert!(
308334
leaked.is_empty(),
309335
"{what}: {} heap blocks allocated during the watch are still live with plaintext in them (sizes {leaked:?})",

0 commit comments

Comments
 (0)