Skip to content

Commit f00e7af

Browse files
Imberflurtaiki-e
authored andcommitted
Fix use after free of task in FuturesUnordered when dropped future panics (#2886)
1 parent 33c46b3 commit f00e7af

File tree

4 files changed

+68
-18
lines changed

4 files changed

+68
-18
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,10 +246,16 @@ jobs:
246246
- uses: taiki-e/checkout-action@v1
247247
- name: Install Rust
248248
run: rustup toolchain install nightly --component miri && rustup default nightly
249-
- run: cargo miri test --workspace --all-features
249+
- run: cargo miri test --workspace --all-features -- --skip panic_on_drop_fut
250250
env:
251251
MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-disable-isolation
252252
RUSTFLAGS: ${{ env.RUSTFLAGS }} -Z randomize-layout
253+
# This test is expected to leak.
254+
- run: cargo miri test --workspace --all-features --test stream_futures_unordered -- panic_on_drop_fut
255+
env:
256+
MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-disable-isolation -Zmiri-ignore-leaks
257+
RUSTDOCFLAGS: ${{ env.RUSTDOCFLAGS }} -Z randomize-layout
258+
RUSTFLAGS: ${{ env.RUSTFLAGS }} -Z randomize-layout
253259

254260
san:
255261
name: cargo test -Z sanitizer=${{ matrix.sanitizer }}
@@ -268,7 +274,7 @@ jobs:
268274
run: rustup toolchain install nightly --component rust-src && rustup default nightly
269275
# https://github.com/google/sanitizers/issues/1716 / https://github.com/actions/runner-images/issues/9491
270276
- run: sudo sysctl vm.mmap_rnd_bits=28
271-
- run: cargo -Z build-std test --workspace --all-features --target x86_64-unknown-linux-gnu --lib --tests
277+
- run: cargo -Z build-std test --workspace --all-features --target x86_64-unknown-linux-gnu --lib --tests -- --skip panic_on_drop_fut
272278
env:
273279
# TODO: Once `cfg(sanitize = "..")` is stable, replace
274280
# `cfg(futures_sanitizer)` with `cfg(sanitize = "..")` and remove

futures-util/src/stream/futures_unordered/mod.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -256,16 +256,6 @@ impl<Fut> FuturesUnordered<Fut> {
256256
// `wake` from doing any work in the future
257257
let prev = task.queued.swap(true, SeqCst);
258258

259-
// Drop the future, even if it hasn't finished yet. This is safe
260-
// because we're dropping the future on the thread that owns
261-
// `FuturesUnordered`, which correctly tracks `Fut`'s lifetimes and
262-
// such.
263-
unsafe {
264-
// Set to `None` rather than `take()`ing to prevent moving the
265-
// future.
266-
*task.future.get() = None;
267-
}
268-
269259
// If the queued flag was previously set, then it means that this task
270260
// is still in our internal ready to run queue. We then transfer
271261
// ownership of our reference count to the ready to run queue, and it'll
@@ -277,8 +267,25 @@ impl<Fut> FuturesUnordered<Fut> {
277267
// enqueue the task, so our task will never see the ready to run queue
278268
// again. The task itself will be deallocated once all reference counts
279269
// have been dropped elsewhere by the various wakers that contain it.
280-
if prev {
281-
mem::forget(task);
270+
//
271+
// Use ManuallyDrop to transfer the reference count ownership before
272+
// dropping the future so unwinding won't release the reference count.
273+
let md_slot;
274+
let task = if prev {
275+
md_slot = mem::ManuallyDrop::new(task);
276+
&*md_slot
277+
} else {
278+
&task
279+
};
280+
281+
// Drop the future, even if it hasn't finished yet. This is safe
282+
// because we're dropping the future on the thread that owns
283+
// `FuturesUnordered`, which correctly tracks `Fut`'s lifetimes and
284+
// such.
285+
unsafe {
286+
// Set to `None` rather than `take()`ing to prevent moving the
287+
// future.
288+
*task.future.get() = None;
282289
}
283290
}
284291

@@ -567,15 +574,27 @@ impl<Fut> FuturesUnordered<Fut> {
567574

568575
impl<Fut> Drop for FuturesUnordered<Fut> {
569576
fn drop(&mut self) {
577+
// Before the strong reference to the queue is dropped we need all
578+
// futures to be dropped. See note at the bottom of this method.
579+
//
580+
// If there is a panic before this completes, we leak the queue.
581+
struct LeakQueueOnDrop<'a, Fut>(&'a mut FuturesUnordered<Fut>);
582+
impl<Fut> Drop for LeakQueueOnDrop<'_, Fut> {
583+
fn drop(&mut self) {
584+
mem::forget(Arc::clone(&self.0.ready_to_run_queue));
585+
}
586+
}
587+
let guard = LeakQueueOnDrop(self);
570588
// When a `FuturesUnordered` is dropped we want to drop all futures
571589
// associated with it. At the same time though there may be tons of
572590
// wakers flying around which contain `Task<Fut>` references
573591
// inside them. We'll let those naturally get deallocated.
574-
while !self.head_all.get_mut().is_null() {
575-
let head = *self.head_all.get_mut();
576-
let task = unsafe { self.unlink(head) };
577-
self.release_task(task);
592+
while !guard.0.head_all.get_mut().is_null() {
593+
let head = *guard.0.head_all.get_mut();
594+
let task = unsafe { guard.0.unlink(head) };
595+
guard.0.release_task(task);
578596
}
597+
mem::forget(guard); // safe to release strong reference to queue
579598

580599
// Note that at this point we could still have a bunch of tasks in the
581600
// ready to run queue. None of those tasks, however, have futures

futures-util/src/stream/futures_unordered/ready_to_run_queue.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ pub(super) struct ReadyToRunQueue<Fut> {
2727
/// An MPSC queue into which the tasks containing the futures are inserted
2828
/// whenever the future inside is scheduled for polling.
2929
impl<Fut> ReadyToRunQueue<Fut> {
30+
// FIXME: this takes raw pointer without safety conditions.
31+
3032
/// The enqueue function from the 1024cores intrusive MPSC queue algorithm.
3133
pub(super) fn enqueue(&self, task: *const Task<Fut>) {
3234
unsafe {

futures/tests/stream_futures_unordered.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,26 @@ fn clear_in_loop() {
406406
}
407407
});
408408
}
409+
410+
// https://github.com/rust-lang/futures-rs/issues/2863#issuecomment-2219441515
411+
#[test]
412+
#[should_panic]
413+
fn panic_on_drop_fut() {
414+
struct BadFuture;
415+
416+
impl Drop for BadFuture {
417+
fn drop(&mut self) {
418+
panic!()
419+
}
420+
}
421+
422+
impl Future for BadFuture {
423+
type Output = ();
424+
425+
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
426+
Poll::Pending
427+
}
428+
}
429+
430+
FuturesUnordered::default().push(BadFuture);
431+
}

0 commit comments

Comments
 (0)