From 3b490fa6c36b2daf9ba1475541a1d0fd2390a713 Mon Sep 17 00:00:00 2001 From: Andy Dixon Date: Mon, 12 Oct 2026 09:16:14 +0100 Subject: [PATCH] fix: redact scripted login passwords from secret-free exports; secret kinds are an enum - A JSON export without secrets now blanks login-script 'send' steps that answer a password-like 'wait' prompt (SSH and Telnet), not just the proxy password and inline TOTP seed (SEC-20261012-011, partial). - Credential kinds were string literals in the session manager, profile editor and secret store; they are now SecretKind with one ALL list, so the profile-delete purge can't miss a kind (F19). Moving proxyPassword out of the profile record is left open: it changes .link/.nexus compatibility with the Go app and needs an owner decision. --- rust/app/src/profile_editor.rs | 36 ++++++++-------- rust/core/src/secrets.rs | 78 +++++++++++++++++++++++++--------- rust/core/src/service.rs | 48 ++++++++++++++++++--- rust/core/src/sessions.rs | 41 +++++++++--------- 4 files changed, 142 insertions(+), 61 deletions(-) diff --git a/rust/app/src/profile_editor.rs b/rust/app/src/profile_editor.rs index 6be2b2e..55f8f23 100644 --- a/rust/app/src/profile_editor.rs +++ b/rust/app/src/profile_editor.rs @@ -10,6 +10,7 @@ use egui_phosphor::regular as icon; use nexus_core::clients::serial; use nexus_core::keyfiles; use nexus_core::models::*; +use nexus_core::secrets::SecretKind; use nexus_core::validation::{ValidationError, validate_profile}; use parking_lot::Mutex; use std::collections::{BTreeMap, HashMap}; @@ -28,7 +29,7 @@ pub struct ProfileEditor { rooms: String, /// Secrets typed into the editor, by session-manager kind ("password", "github-token", …); /// stored when the profile is saved. - secrets: BTreeMap<&'static str, String>, + secrets: BTreeMap, totp: String, totp_show: bool, totp_saved: bool, @@ -310,9 +311,9 @@ impl ProfileEditor { Ok(saved) => { for (kind, v) in &self.secrets { if !v.is_empty() - && let Err(e) = s.svc.set_profile_secret(&saved.id, kind, v) + && let Err(e) = s.svc.set_profile_secret(&saved.id, *kind, v) { - s.error(format!("Couldn't store the {kind} secret: {e}")); + s.error(format!("Couldn't store the {} secret: {e}", kind.as_str())); } } s.success(if self.is_new { "Profile created" } else { "Profile updated" }); @@ -387,7 +388,7 @@ impl ProfileEditor { section(ui, "Authentication", |ui| { mode(ui, "Password Mode", &mut c.secret_mode); if c.secret_mode == SecretMode::Saved { - secret(ui, "Password", secrets, "password", "Leave blank to keep the saved password"); + secret(ui, "Password", secrets, SecretKind::Password, "Leave blank to keep the saved password"); } label(ui, "Auth Methods"); ui.horizontal_wrapped(|ui| { @@ -410,7 +411,7 @@ impl ProfileEditor { widgets::hint(ui, "Select a key from your .ssh folder or type a full path"); mode(ui, "Passphrase Mode", &mut c.key_secret_mode); if c.key_secret_mode == SecretMode::Saved { - secret(ui, "Passphrase", secrets, "passphrase", "Leave blank to keep the saved passphrase"); + secret(ui, "Passphrase", secrets, SecretKind::Passphrase, "Leave blank to keep the saved passphrase"); } }); section(ui, "Host Verification", |ui| { @@ -527,7 +528,7 @@ impl ProfileEditor { *totp_saved = false; s.info("TOTP secret removed."); } else { - match s.svc.set_profile_secret(&id, "totp", &v) { + match s.svc.set_profile_secret(&id, SecretKind::Totp, &v) { Ok(()) => { totp.clear(); *totp_saved = true; @@ -627,7 +628,7 @@ impl ProfileEditor { err(ui, errs, "github.baseURL"); mode(ui, "Token Storage", &mut c.secret_mode); if c.secret_mode == SecretMode::Saved { - secret(ui, "Personal Access Token", secrets, "github-token", "Leave blank to keep the saved token"); + secret(ui, "Personal Access Token", secrets, SecretKind::GithubToken, "Leave blank to keep the saved token"); } }); section(ui, "Refresh", |ui| { @@ -646,7 +647,7 @@ impl ProfileEditor { err(ui, errs, "vnc.vncPort"); mode(ui, "Password Storage", &mut c.secret_mode); if c.secret_mode == SecretMode::Saved { - secret(ui, "VNC Password", secrets, "vnc-password", "Leave blank to keep the saved password"); + secret(ui, "VNC Password", secrets, SecretKind::VncPassword, "Leave blank to keep the saved password"); } }); section(ui, "Display", |ui| { @@ -700,7 +701,7 @@ impl ProfileEditor { if c.server_password_required { mode(ui, "Server Password Storage", &mut c.server_password_mode); if c.server_password_mode == SecretMode::Saved { - secret(ui, "Server Password", secrets, "irc-server-password", "Leave blank to keep the saved password"); + secret(ui, "Server Password", secrets, SecretKind::IrcServerPassword, "Leave blank to keep the saved password"); } } }); @@ -711,7 +712,7 @@ impl ProfileEditor { err(ui, errs, "irc.nickServAccount"); mode(ui, "Password Storage", &mut c.nick_serv_mode); if c.nick_serv_mode == SecretMode::Saved { - secret(ui, "NickServ Password", secrets, "irc-nickserv-password", "Leave blank to keep the saved password"); + secret(ui, "NickServ Password", secrets, SecretKind::IrcNickServPassword, "Leave blank to keep the saved password"); } } }); @@ -980,7 +981,7 @@ fn reconnect(ui: &mut Ui, on: &mut bool, tries: &mut i64) { } /// A password field whose value is stored in the vault/keychain under `kind` on save. -fn secret(ui: &mut Ui, l: &str, secrets: &mut BTreeMap<&'static str, String>, kind: &'static str, hint: &str) { +fn secret(ui: &mut Ui, l: &str, secrets: &mut BTreeMap, kind: SecretKind, hint: &str) { label(ui, l); ui.add(TextEdit::singleline(secrets.entry(kind).or_default()).password(true).hint_text(hint).desired_width(f32::INFINITY)); } @@ -1148,7 +1149,7 @@ struct Tunnel<'a> { mode: &'a mut SecretMode, } -fn tunnel(ui: &mut Ui, kind: &str, c: Tunnel, keys: &[String], checks: &mut KeyChecks, secrets: &mut BTreeMap<&'static str, String>, errs: &[ValidationError]) { +fn tunnel(ui: &mut Ui, kind: &str, c: Tunnel, keys: &[String], checks: &mut KeyChecks, secrets: &mut BTreeMap, errs: &[ValidationError]) { check(ui, "Enable SSH Tunnel", c.on, ""); if !*c.on { return; @@ -1168,12 +1169,11 @@ fn tunnel(ui: &mut Ui, kind: &str, c: Tunnel, keys: &[String], checks: &mut KeyC } mode(ui, "SSH Secret Storage", c.mode); if *c.mode == SecretMode::Saved { - // Kinds match the session manager's tunnel secrets ("vnc-ssh-password", "irc-ssh-passphrase"). - let (l, k): (&str, &'static str) = match (kind, c.auth.as_str()) { - ("vnc", "password") => ("SSH Password", "vnc-ssh-password"), - ("vnc", _) => ("SSH Key Passphrase", "vnc-ssh-passphrase"), - (_, "password") => ("SSH Password", "irc-ssh-password"), - _ => ("SSH Key Passphrase", "irc-ssh-passphrase"), + let (l, k) = match (kind, c.auth.as_str()) { + ("vnc", "password") => ("SSH Password", SecretKind::VncSshPassword), + ("vnc", _) => ("SSH Key Passphrase", SecretKind::VncSshPassphrase), + (_, "password") => ("SSH Password", SecretKind::IrcSshPassword), + _ => ("SSH Key Passphrase", SecretKind::IrcSshPassphrase), }; secret(ui, l, secrets, k, "Leave blank to keep the saved secret"); } diff --git a/rust/core/src/secrets.rs b/rust/core/src/secrets.rs index 269d07e..5aff27e 100644 --- a/rust/core/src/secrets.rs +++ b/rust/core/src/secrets.rs @@ -16,20 +16,60 @@ use zeroize::Zeroizing; const VAULT_FILE: &str = "vault.json"; const MAX_VAULT_BYTES: u64 = 10 * 1024 * 1024; -/// Every credential kind the session manager stores under `profile::`. -pub const PROFILE_SECRET_KINDS: [&str; 11] = [ - "password", - "passphrase", - "totp", - "github-token", - "vnc-password", - "vnc-ssh-password", - "vnc-ssh-passphrase", - "irc-server-password", - "irc-nickserv-password", - "irc-ssh-password", - "irc-ssh-passphrase", -]; +/// A credential stored per profile under `profile::`. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SecretKind { + Password, + Passphrase, + Totp, + GithubToken, + VncPassword, + VncSshPassword, + VncSshPassphrase, + IrcServerPassword, + IrcNickServPassword, + IrcSshPassword, + IrcSshPassphrase, +} + +impl SecretKind { + /// Every kind, so deleting a profile can purge them all from the keychain. A new variant + /// must be added here as well as to `as_str`. + pub const ALL: [SecretKind; 11] = [ + Self::Password, + Self::Passphrase, + Self::Totp, + Self::GithubToken, + Self::VncPassword, + Self::VncSshPassword, + Self::VncSshPassphrase, + Self::IrcServerPassword, + Self::IrcNickServPassword, + Self::IrcSshPassword, + Self::IrcSshPassphrase, + ]; + + /// The storage name, shared with the Go app. + pub fn as_str(self) -> &'static str { + match self { + Self::Password => "password", + Self::Passphrase => "passphrase", + Self::Totp => "totp", + Self::GithubToken => "github-token", + Self::VncPassword => "vnc-password", + Self::VncSshPassword => "vnc-ssh-password", + Self::VncSshPassphrase => "vnc-ssh-passphrase", + Self::IrcServerPassword => "irc-server-password", + Self::IrcNickServPassword => "irc-nickserv-password", + Self::IrcSshPassword => "irc-ssh-password", + Self::IrcSshPassphrase => "irc-ssh-passphrase", + } + } + + pub fn key(self, profile_id: &str) -> String { + format!("profile:{profile_id}:{}", self.as_str()) + } +} /// Argon2id with the Go parameters: t=3, m=128 MiB, p=4, 32-byte key. The key is wiped on drop. pub fn derive_key(password: &str, salt: &[u8]) -> Result> { @@ -306,8 +346,8 @@ impl Secrets { /// Removes every credential saved for a profile from both stores. pub fn delete_profile(&self, profile_id: &str) { if self.keychain_ok { - for kind in PROFILE_SECRET_KINDS { - self.keychain.delete(&format!("profile:{profile_id}:{kind}")); + for kind in SecretKind::ALL { + self.keychain.delete(&kind.key(profile_id)); } } let _ = self.vault.delete_prefix(&format!("profile:{profile_id}:")); @@ -395,12 +435,12 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); let s = Secrets { vault: Vault::new(dir.clone()), keychain: Keychain::new("NexusTerminalTest", None), keychain_ok: false }; s.vault.initialise("pw").unwrap(); - for kind in PROFILE_SECRET_KINDS { - s.vault.set(&format!("profile:p1:{kind}"), "v").unwrap(); + for kind in SecretKind::ALL { + s.vault.set(&kind.key("p1"), "v").unwrap(); } s.vault.set("profile:p2:password", "keep").unwrap(); s.delete_profile("p1"); - assert!(PROFILE_SECRET_KINDS.iter().all(|k| s.get(&format!("profile:p1:{k}")).is_none())); + assert!(SecretKind::ALL.iter().all(|k| s.get(&k.key("p1")).is_none())); assert_eq!(s.get("profile:p2:password").as_deref(), Some("keep")); std::fs::remove_dir_all(dir).unwrap(); } diff --git a/rust/core/src/service.rs b/rust/core/src/service.rs index 6bb7e51..736480e 100644 --- a/rust/core/src/service.rs +++ b/rust/core/src/service.rs @@ -5,6 +5,7 @@ use crate::clients::sftp::{self, FileEntry}; use crate::config::AppConfig; use crate::models::*; +use crate::secrets::SecretKind; use crate::secrets::{KEYCHAIN_SERVICE, Keychain, LEGACY_KEYCHAIN_SERVICE, Secrets, Vault}; use crate::sessions::Manager; use crate::storage::Store; @@ -127,21 +128,21 @@ impl Service { // --- credentials -------------------------------------------------------------------- - pub fn set_profile_secret(&self, profile_id: &str, kind: &str, value: &str) -> Result<()> { - self.secrets.set_checked(&format!("profile:{profile_id}:{kind}"), value) + pub fn set_profile_secret(&self, profile_id: &str, kind: SecretKind, value: &str) -> Result<()> { + self.secrets.set_checked(&kind.key(profile_id), value) } pub fn has_totp(&self, profile_id: &str) -> bool { - self.secrets.get(&format!("profile:{profile_id}:totp")).is_some() + self.secrets.get(&SecretKind::Totp.key(profile_id)).is_some() } pub fn totp_code(&self, profile_id: &str) -> Result { - let s = self.secrets.get(&format!("profile:{profile_id}:totp")).ok_or("no TOTP secret configured for this profile")?; + let s = self.secrets.get(&SecretKind::Totp.key(profile_id)).ok_or("no TOTP secret configured for this profile")?; crate::clients::ssh::totp_code(&s).map_err(|e| format!("failed to generate TOTP code: {e}").into()) } pub fn delete_totp(&self, profile_id: &str) { - self.secrets.delete(&format!("profile:{profile_id}:totp")); + self.secrets.delete(&SecretKind::Totp.key(profile_id)); } // --- known hosts, prefs, snippets, audit -------------------------------------------- @@ -226,6 +227,10 @@ impl Service { if let Some(s) = &mut p.ssh { s.proxy_password.clear(); s.totp_secret.clear(); + redact_login_script(&mut s.login_scripts); + } + if let Some(t) = &mut p.telnet { + redact_login_script(&mut t.login_scripts); } } } @@ -390,3 +395,36 @@ impl Service { self.store.list_audit(limit).unwrap_or_default() } } + +/// Blanks `send` steps that answer a password-like prompt (the `wait` before them), the documented +/// way to script a login, so a "without secrets" export doesn't carry them. +fn redact_login_script(steps: &mut [LoginScriptStep]) { + let mut after_secret_prompt = false; + for s in steps { + match s.kind.as_str() { + "wait" => { + let v = s.value.to_lowercase(); + after_secret_prompt = ["password", "passphrase", "passcode", "pin", "token", "secret"].iter().any(|k| v.contains(k)); + } + "send" if after_secret_prompt => { + s.value.clear(); + after_secret_prompt = false; + } + _ => {} + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn login_script_passwords_are_redacted() { + let step = |k: &str, v: &str| LoginScriptStep { kind: k.into(), value: v.into(), timeout: 0 }; + let mut steps = vec![step("wait", "login:"), step("send", "andy"), step("wait", "Password:"), step("send", "mysecretpassword"), step("send", "uptime")]; + redact_login_script(&mut steps); + let values: Vec<&str> = steps.iter().map(|s| s.value.as_str()).collect(); + assert_eq!(values, ["login:", "andy", "Password:", "", "uptime"]); + } +} diff --git a/rust/core/src/sessions.rs b/rust/core/src/sessions.rs index ff7c515..78dbdbf 100644 --- a/rust/core/src/sessions.rs +++ b/rust/core/src/sessions.rs @@ -8,7 +8,7 @@ use crate::clients::ssh::{HostKeyCheck, HostKeyInfo, SecretFn, SshClient}; use crate::clients::{self, Callbacks, github, irc, raw, serial, telnet}; use crate::models::*; use crate::recording::{self, Recorder}; -use crate::secrets::Secrets; +use crate::secrets::{SecretKind, Secrets}; use crate::storage::Store; use parking_lot::Mutex; use std::collections::{HashMap, HashSet}; @@ -482,10 +482,10 @@ impl Manager { /// are saved once the connection succeeds; session-only values are asked once per session and /// reused on reconnects (so e.g. a NickServ password entered once keeps identifying after a /// drop). - pub fn secret_fn(&self, session: &str, profile_id: &str, kind: &str, mode: SecretMode, prompt: Prompt) -> SecretFn { - let key = format!("profile:{profile_id}:{kind}"); + pub fn secret_fn(&self, session: &str, profile_id: &str, kind: SecretKind, mode: SecretMode, prompt: Prompt) -> SecretFn { + let key = kind.key(profile_id); let (secrets, store) = (self.secrets.clone(), self.store.clone()); - let (pid, kind) = (profile_id.to_string(), kind.to_string()); + let (pid, kind) = (profile_id.to_string(), kind.as_str()); let state: SessionSecrets = self.sessions.lock().get(session).map(|s| s.session_secrets.clone()).unwrap_or_default(); Arc::new(move || { if mode == SecretMode::Saved { @@ -629,13 +629,13 @@ impl Manager { let mut p = p.clone(); let sc = p.ssh.get_or_insert_with(SshConfig::defaults); if sc.totp_secret.is_empty() - && let Some(t) = self.secrets.get(&format!("profile:{}:totp", p.id)) + && let Some(t) = self.secrets.get(&SecretKind::Totp.key(&p.id)) { sc.totp_secret = t; } let sc = sc.clone(); - let password = self.secret_fn(id, &p.id, "password", sc.secret_mode, prompt.clone()); - let passphrase = self.secret_fn(id, &p.id, "passphrase", sc.key_secret_mode, prompt); + let password = self.secret_fn(id, &p.id, SecretKind::Password, sc.secret_mode, prompt.clone()); + let passphrase = self.secret_fn(id, &p.id, SecretKind::Passphrase, sc.key_secret_mode, prompt); let c = SshClient::new(&p, self.terminal_callbacks(id, epoch)); c.connect(self.host_key_check(id, sc.host_key_policy), Some(password), Some(passphrase))?; for pf in sc.port_forwards.iter().filter(|f| f.enabled) { @@ -672,8 +672,10 @@ impl Manager { c.auth_methods = vec![if method.is_empty() { "publickey".into() } else { method.into() }]; c.private_key_path = key.into(); c.agent_auth = agent; - let password = (method == "password").then(|| self.secret_fn(id, profile_id, &format!("{kind}-ssh-password"), mode, prompt.clone())); - let passphrase = self.secret_fn(id, profile_id, &format!("{kind}-ssh-passphrase"), mode, prompt); + let (pw_kind, pp_kind) = + if kind == "vnc" { (SecretKind::VncSshPassword, SecretKind::VncSshPassphrase) } else { (SecretKind::IrcSshPassword, SecretKind::IrcSshPassphrase) }; + let password = (method == "password").then(|| self.secret_fn(id, profile_id, pw_kind, mode, prompt.clone())); + let passphrase = self.secret_fn(id, profile_id, pp_kind, mode, prompt); let t = SshClient::new(&tp, Callbacks::noop()); t.connect_tunnel_only(self.host_key_check(id, HostKeyPolicy::Ask), password, Some(passphrase))?; Ok(t) @@ -681,7 +683,7 @@ impl Manager { fn connect_github(self: &Arc, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result { let gh = p.github.clone().ok_or_else(|| format!("GitHub configuration missing on profile {:?}", p.id))?; - let token = self.secret_fn(id, &p.id, "github-token", gh.secret_mode, prompt.clone())().map_err(|e| format!("obtaining GitHub token: {e}"))?; + let token = self.secret_fn(id, &p.id, SecretKind::GithubToken, gh.secret_mode, prompt.clone())().map_err(|e| format!("obtaining GitHub token: {e}"))?; if token.is_empty() { return err("GitHub token cannot be empty".into()); } @@ -694,7 +696,7 @@ impl Manager { let retry = auth.then(|| prompt("GitHub token did not work. Please enter a valid token:").ok()).flatten().filter(|t| !t.is_empty()); let Some(t) = retry else { return err(format!("GitHub authentication failed: {e}")) }; if gh.secret_mode == SecretMode::Saved { - let _ = self.with(id, |s| s.session_secrets.lock().pending.insert(format!("profile:{}:github-token", p.id), t.clone())); + let _ = self.with(id, |s| s.session_secrets.lock().pending.insert(SecretKind::GithubToken.key(&p.id), t.clone())); } client = github::Client::new(&gh.base_url, &t); client.validate_token().map_err(|e| format!("GitHub authentication failed: {e}"))? @@ -742,12 +744,13 @@ impl Manager { fn connect_irc(self: &Arc, id: &str, epoch: u64, p: &Profile, prompt: Prompt) -> Result { let ic = p.irc.clone().ok_or("IRC configuration missing")?; let server_pass = if ic.server_password_required { - self.secret_fn(id, &p.id, "irc-server-password", ic.server_password_mode, prompt.clone())().map_err(|e| format!("IRC server password: {e}"))? + self.secret_fn(id, &p.id, SecretKind::IrcServerPassword, ic.server_password_mode, prompt.clone())() + .map_err(|e| format!("IRC server password: {e}"))? } else { String::new() }; let ns_pass = if ic.nick_serv_identify { - self.secret_fn(id, &p.id, "irc-nickserv-password", ic.nick_serv_mode, prompt.clone())().map_err(|e| format!("NickServ password: {e}"))? + self.secret_fn(id, &p.id, SecretKind::IrcNickServPassword, ic.nick_serv_mode, prompt.clone())().map_err(|e| format!("NickServ password: {e}"))? } else { String::new() }; @@ -852,7 +855,7 @@ impl Manager { } else { None }; - let password = self.secret_fn(id, &p.id, "vnc-password", vc.secret_mode, prompt); + let password = self.secret_fn(id, &p.id, SecretKind::VncPassword, vc.secret_mode, prompt); let cb = self.terminal_callbacks(id, epoch); let (me, sid) = (self.clone(), id.to_string()); let (me2, sid2) = (self.clone(), id.to_string()); @@ -1474,7 +1477,7 @@ mod tests { } })); let p = Profile { host: "127.0.0.1".into(), port: 1, ..Profile::new(Protocol::Raw) }; - let key = format!("profile:{}:password", p.id); + let key = SecretKind::Password.key(&p.id); let info = |id: &str| SessionInfo { id: id.into(), profile_id: p.id.clone(), @@ -1489,7 +1492,7 @@ mod tests { error: String::new(), }; let asked = Arc::new(AtomicUsize::new(0)); - let resolve = |m: &Arc, id: &str| m.secret_fn(id, &p.id, "password", SecretMode::Saved, counting_prompt(asked.clone()))().unwrap(); + let resolve = |m: &Arc, id: &str| m.secret_fn(id, &p.id, SecretKind::Password, SecretMode::Saved, counting_prompt(asked.clone()))().unwrap(); // Locked store: the credential can't be saved, and the user is told. let e = m.new_session(info("s0"), Some(p.clone()), String::new(), SessionSecrets::default()); @@ -1545,17 +1548,17 @@ mod tests { let asked = Arc::new(AtomicUsize::new(0)); // Two separate resolutions (as two connects of the same session make) prompt once. for _ in 0..2 { - let f = m.secret_fn("s1", &p.id, "irc-nickserv-password", SecretMode::SessionOnly, counting_prompt(asked.clone())); + let f = m.secret_fn("s1", &p.id, SecretKind::IrcNickServPassword, SecretMode::SessionOnly, counting_prompt(asked.clone())); assert_eq!(f().unwrap(), "hunter2"); } assert_eq!(asked.load(Ordering::SeqCst), 1); // Ask-every-time still prompts each time. - let f = m.secret_fn("s1", &p.id, "irc-nickserv-password", SecretMode::AskEveryTime, counting_prompt(asked.clone())); + let f = m.secret_fn("s1", &p.id, SecretKind::IrcNickServPassword, SecretMode::AskEveryTime, counting_prompt(asked.clone())); f().unwrap(); assert_eq!(asked.load(Ordering::SeqCst), 2); // An explicit reconnect carries the cache to the new session id. let new = m.reconnect("s1").unwrap(); - let f = m.secret_fn(&new, &p.id, "irc-nickserv-password", SecretMode::SessionOnly, counting_prompt(asked.clone())); + let f = m.secret_fn(&new, &p.id, SecretKind::IrcNickServPassword, SecretMode::SessionOnly, counting_prompt(asked.clone())); assert_eq!(f().unwrap(), "hunter2"); assert_eq!(asked.load(Ordering::SeqCst), 2); m.close_all();