From 35fca2865ac8b8589e292299c8dca5f87102638f Mon Sep 17 00:00:00 2001 From: Kamalesh Palanisamy Date: Thu, 13 Aug 2026 03:40:20 +0000 Subject: [PATCH] rust: fix whole-runtime deadlock in escape_to_async on multi-thread runtimes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit escape_to_async bridges sync Dafny code back to async on multi-thread tokio runtimes with block_in_place + Handle::block_on, which parks the calling worker on a future that the caller's own runtime must drive. When that worker is the last active one — every other worker parked and the shared IO/timer driver unowned — the bridged future can never make progress and the entire runtime deadlocks permanently: no timers, no IO, no task is ever polled again, unrecoverable without killing the process. tokio's documentation explicitly warns that a future passed to Handle::block_on from within a runtime must not depend on that runtime for progress. Run the future on a scoped thread with a fresh current-thread runtime instead — the same pattern the CurrentThread arm already uses — so the bridged future never depends on the caller's runtime for progress on any flavor. The cost is one thread spawn + runtime build per escaped call, which only happens on key-material cache misses. Fixes the deadlock reported in #899. --- .../src/deps/com_amazonaws_dynamodb/client.rs | 35 +++++++++++-------- .../esdk/src/deps/com_amazonaws_kms/client.rs | 35 +++++++++++-------- releases/rust/esdk/src/escape.rs | 35 +++++++++++-------- 3 files changed, 60 insertions(+), 45 deletions(-) diff --git a/releases/rust/esdk/src/deps/com_amazonaws_dynamodb/client.rs b/releases/rust/esdk/src/deps/com_amazonaws_dynamodb/client.rs index 56f13bf93..2cacfdb1f 100644 --- a/releases/rust/esdk/src/deps/com_amazonaws_dynamodb/client.rs +++ b/releases/rust/esdk/src/deps/com_amazonaws_dynamodb/client.rs @@ -4,7 +4,6 @@ use std::future::Future; use tokio::runtime::Builder; use tokio::runtime::Handle; -use tokio::runtime::RuntimeFlavor; pub fn escape_to_async(fut: F) -> O where @@ -12,20 +11,26 @@ where O: Send, { match Handle::try_current() { - Ok(handle) => match handle.runtime_flavor() { - RuntimeFlavor::CurrentThread => std::thread::scope(move |t| { - t.spawn(move || { - Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(fut) - }) - .join() - .unwrap() - }), - _ => tokio::task::block_in_place(move || handle.block_on(fut)), - }, + // Any ambient runtime: run the future on a scoped thread with its own + // runtime. Blocking the calling thread is what a sync bridge does, but + // the future must never depend on the caller's runtime for progress: + // `block_in_place` + `Handle::block_on` parks a worker on a future + // that same runtime has to drive, and when that worker is the last + // one awake (the shared IO/timer driver unowned, all other workers + // parked) the future can never complete and the whole runtime + // deadlocks permanently. A fresh current-thread runtime drives the + // future independently on every flavor. + Ok(_) => std::thread::scope(move |t| { + t.spawn(move || { + Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(fut) + }) + .join() + .unwrap() + }), Err(_) => Builder::new_current_thread() .enable_all() .build() diff --git a/releases/rust/esdk/src/deps/com_amazonaws_kms/client.rs b/releases/rust/esdk/src/deps/com_amazonaws_kms/client.rs index 257df0e3f..e54517b46 100644 --- a/releases/rust/esdk/src/deps/com_amazonaws_kms/client.rs +++ b/releases/rust/esdk/src/deps/com_amazonaws_kms/client.rs @@ -4,7 +4,6 @@ use std::future::Future; use tokio::runtime::Builder; use tokio::runtime::Handle; -use tokio::runtime::RuntimeFlavor; pub fn escape_to_async(fut: F) -> O where @@ -12,20 +11,26 @@ where O: Send, { match Handle::try_current() { - Ok(handle) => match handle.runtime_flavor() { - RuntimeFlavor::CurrentThread => std::thread::scope(move |t| { - t.spawn(move || { - Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(fut) - }) - .join() - .unwrap() - }), - _ => tokio::task::block_in_place(move || handle.block_on(fut)), - }, + // Any ambient runtime: run the future on a scoped thread with its own + // runtime. Blocking the calling thread is what a sync bridge does, but + // the future must never depend on the caller's runtime for progress: + // `block_in_place` + `Handle::block_on` parks a worker on a future + // that same runtime has to drive, and when that worker is the last + // one awake (the shared IO/timer driver unowned, all other workers + // parked) the future can never complete and the whole runtime + // deadlocks permanently. A fresh current-thread runtime drives the + // future independently on every flavor. + Ok(_) => std::thread::scope(move |t| { + t.spawn(move || { + Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(fut) + }) + .join() + .unwrap() + }), Err(_) => Builder::new_current_thread() .enable_all() .build() diff --git a/releases/rust/esdk/src/escape.rs b/releases/rust/esdk/src/escape.rs index 414aed3e8..153d31e63 100644 --- a/releases/rust/esdk/src/escape.rs +++ b/releases/rust/esdk/src/escape.rs @@ -4,7 +4,6 @@ use std::future::Future; use tokio::runtime::Builder; use tokio::runtime::Handle; -use tokio::runtime::RuntimeFlavor; pub(crate) fn escape_to_async(fut: F) -> O where @@ -12,20 +11,26 @@ where O: Send, { match Handle::try_current() { - Ok(handle) => match handle.runtime_flavor() { - RuntimeFlavor::CurrentThread => std::thread::scope(move |t| { - t.spawn(move || { - Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(fut) - }) - .join() - .unwrap() - }), - _ => tokio::task::block_in_place(move || handle.block_on(fut)), - }, + // Any ambient runtime: run the future on a scoped thread with its own + // runtime. Blocking the calling thread is what a sync bridge does, but + // the future must never depend on the caller's runtime for progress: + // `block_in_place` + `Handle::block_on` parks a worker on a future + // that same runtime has to drive, and when that worker is the last + // one awake (the shared IO/timer driver unowned, all other workers + // parked) the future can never complete and the whole runtime + // deadlocks permanently. A fresh current-thread runtime drives the + // future independently on every flavor. + Ok(_) => std::thread::scope(move |t| { + t.spawn(move || { + Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(fut) + }) + .join() + .unwrap() + }), Err(_) => Builder::new_current_thread() .enable_all() .build()