fix(offline-cache): run the secret-vault open off the async pool (no zbus panic)

The CMAF download opened the SecretBox (OS keyring -> secret-service -> a
synchronous zbus D-Bus round-trip) directly on a tokio async worker, which
panicked ("Cannot start a runtime from within a runtime") and poisoned the
vault mutex — every later attempt then failed and fell back to the legacy
plain-FLAC path. Playback never hit this because load_cmaf_bundle already runs
inside spawn_blocking.

- downloader: wrap the vault open + key wrap in tokio::task::spawn_blocking
  (off the async pool), mirroring the playback decrypt path.
- secret_vault: get_or_init recovers a poisoned mutex (into_inner) instead of
  wedging the vault for the rest of the session.

Verified at runtime: CMAF bundle now persists (tracks-cmaf/<id>/) and plays
back decrypted; no panic, no legacy fallback.

cc
This commit is contained in:
vicrodh committed 2026-05-28 08:28:46 -06:00
1 parent d5f23c38d8
commit 4041499114
2 files changed
+26 -13

No files matched your search

+21 -10
View File
@@ -320,16 +320,27 @@ pub(crate) async fn try_cmaf_offline_download(
.await?
};
// Open (or lazily init) the secret vault and wrap the keying material
// before it touches the filesystem.
let vault = crate::secret_vault::get_or_init(&offline_root_path)
.map_err(|e| format!("SecretBox init failed: {}", e))?;
let content_key_wrapped = vault
.wrap(&bundle.content_key)
.map_err(|e| format!("Failed to wrap content_key: {}", e))?;
let infos_wrapped = vault
.wrap(bundle.infos.as_bytes())
.map_err(|e| format!("Failed to wrap infos: {}", e))?;
// Open the secret vault and wrap the keying material on a BLOCKING thread.
// The OS keyring (secret-service) does a synchronous D-Bus round-trip via
// zbus, which PANICS ("cannot start a runtime from within a runtime") when
// run on an async worker. spawn_blocking moves it off the async pool — the
// same reason the playback decrypt path already uses spawn_blocking.
let offline_root_for_vault = offline_root_path.clone();
let content_key = bundle.content_key;
let infos = bundle.infos.clone();
let (content_key_wrapped, infos_wrapped) = tokio::task::spawn_blocking(move || {
let vault = crate::secret_vault::get_or_init(&offline_root_for_vault)
.map_err(|e| format!("SecretBox init failed: {}", e))?;
let ck = vault
.wrap(&content_key)
.map_err(|e| format!("Failed to wrap content_key: {}", e))?;
let inf = vault
.wrap(infos.as_bytes())
.map_err(|e| format!("Failed to wrap infos: {}", e))?;
Ok::<(Vec<u8>, Vec<u8>), String>((ck, inf))
})
.await
.map_err(|e| format!("vault task join failed: {}", e))??;
// Persist the encrypted bundle to disk.
let (layout, total_bytes) =
+5 -3
View File
@@ -19,9 +19,11 @@ static VAULT: OnceLock<std::sync::Mutex<Option<SecretBox>>> = OnceLock::new();
/// subsequent calls return the cached handle regardless.
pub fn get_or_init(storage_dir: &Path) -> Result<SecretBox, SecretError> {
let cell = VAULT.get_or_init(|| std::sync::Mutex::new(None));
let mut guard = cell
.lock()
.map_err(|_| SecretError::Other("secret vault mutex poisoned".to_string()))?;
// Recover from a poisoned mutex rather than wedging the vault for the rest
// of the session: a panic during a prior `SecretBox::open` (e.g. the zbus
// "runtime within a runtime" panic this guards against) leaves the inner
// `Option` as `None`, so recovering simply retries the open.
let mut guard = cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(existing) = guard.as_ref() {
return Ok(existing.clone());
}