Skip to content

Commit 2d4f685

Browse files
authored
Fix use after free of task in FuturesUnordered when dropped future panics (#2886)
1 parent 99ebe58 commit 2d4f685

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,11 +246,17 @@ 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
RUSTDOCFLAGS: ${{ env.RUSTDOCFLAGS }} -Z randomize-layout
253253
RUSTFLAGS: ${{ env.RUSTFLAGS }} -Z randomize-layout
254+
# This test is expected to leak.
255+
- run: cargo miri test --workspace --all-features --test stream_futures_unordered -- panic_on_drop_fut
256+
env:
257+
MIRIFLAGS: -Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-disable-isolation -Zmiri-ignore-leaks
258+
RUSTDOCFLAGS: ${{ env.RUSTDOCFLAGS }} -Z randomize-layout
259+
RUSTFLAGS: ${{ env.RUSTFLAGS }} -Z randomize-layout
254260

255261
san:
256262
name: cargo test -Z sanitizer=${{ matrix.sanitizer }}
@@ -269,7 +275,7 @@ jobs:
269275
run: rustup toolchain install nightly --component rust-src && rustup default nightly
270276
# https://github.com/google/sanitizers/issues/1716 / https://github.com/actions/runner-images/issues/9491
271277
- run: sudo sysctl vm.mmap_rnd_bits=28
272-
- run: cargo -Z build-std test --workspace --all-features --target x86_64-unknown-linux-gnu --lib --tests
278+
- run: cargo -Z build-std test --workspace --all-features --target x86_64-unknown-linux-gnu --lib --tests -- --skip panic_on_drop_fut
273279
env:
274280
# TODO: Once `cfg(sanitize = "..")` is stable, replace
275281
# `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
@@ -255,16 +255,6 @@ impl<Fut> FuturesUnordered<Fut> {
255255
// `wake` from doing any work in the future
256256
let prev = task.queued.swap(true, SeqCst);
257257

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

@@ -566,15 +573,27 @@ impl<Fut> FuturesUnordered<Fut> {
566573

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

579598
// Note that at this point we could still have a bunch of tasks in the
580599
// 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)