From: Robert Obkircher <r.obkircher@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [PATCH v1 proxmox-backup 10/11] client: catalog shell: avoid unsafe transmute
Date: Thu, 26 Feb 2026 15:40:24 +0100 [thread overview]
Message-ID: <20260226144033.211039-11-r.obkircher@proxmox.com> (raw)
In-Reply-To: <20260226144033.211039-1-r.obkircher@proxmox.com>
Wrap data in a Mutex instead of storing a stack pointer in a global
usize variable.
The previous version created two simultaneous mutable references to
the same memory while readline invoked the completion callback.
Accoriding to a clippy warning, transmuting a pointer to an integer
and back was also undefined behavior, because it doesn't preserve
pointer provenance. The pointer would have also been left dangling if
the `Shell::shell` future was cancelled or panicked.
Signed-off-by: Robert Obkircher <r.obkircher@proxmox.com>
---
pbs-client/src/catalog_shell.rs | 95 ++++++++++++++++++---------------
1 file changed, 53 insertions(+), 42 deletions(-)
diff --git a/pbs-client/src/catalog_shell.rs b/pbs-client/src/catalog_shell.rs
index 5e62a62a..1871172b 100644
--- a/pbs-client/src/catalog_shell.rs
+++ b/pbs-client/src/catalog_shell.rs
@@ -7,6 +7,7 @@ use std::ops::ControlFlow;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::pin::Pin;
+use std::sync::Mutex;
use anyhow::{bail, format_err, Error};
use nix::dir::Dir;
@@ -35,7 +36,7 @@ type FileEntry = pxar::accessor::aio::FileEntry<Reader>;
const MAX_SYMLINK_COUNT: usize = 40;
-static mut SHELL: Option<usize> = None;
+static SHELL: Mutex<Option<Shell>> = Mutex::new(None);
/// This list defines all the shell commands and their properties
/// using the api schema
@@ -103,8 +104,10 @@ pub fn catalog_shell_cli() -> CommandLineInterface {
}
fn complete_path(complete_me: &str, _map: &HashMap<String, String>) -> Vec<String> {
- let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
- match block_on(shell.complete_path(complete_me)) {
+ let result = block_on(Shell::with(async |shell| {
+ shell.complete_path(complete_me).await
+ }));
+ match result {
Ok(list) => list,
Err(err) => {
error!("error during completion: {}", err);
@@ -124,7 +127,7 @@ async fn exit() -> Result<(), Error> {
#[api(input: { properties: {} })]
/// List the current working directory.
async fn pwd_command() -> Result<(), Error> {
- Shell::with(move |shell| shell.pwd()).await
+ Shell::with(Shell::pwd).await
}
#[api(
@@ -141,7 +144,7 @@ async fn pwd_command() -> Result<(), Error> {
/// Change the current working directory to the new directory
async fn cd_command(path: Option<String>) -> Result<(), Error> {
let path = path.as_ref().map(Path::new);
- Shell::with(move |shell| shell.cd(path)).await
+ Shell::with(async |shell| shell.cd(path).await).await
}
#[api(
@@ -158,7 +161,7 @@ async fn cd_command(path: Option<String>) -> Result<(), Error> {
/// List the content of working directory or given path.
async fn ls_command(path: Option<String>) -> Result<(), Error> {
let path = path.as_ref().map(Path::new);
- Shell::with(move |shell| shell.ls(path)).await
+ Shell::with(async |shell| shell.ls(path).await).await
}
#[api(
@@ -176,7 +179,7 @@ async fn ls_command(path: Option<String>) -> Result<(), Error> {
/// This is expensive because the data has to be read from the pxar archive, which means reading
/// over the network.
async fn stat_command(path: String) -> Result<(), Error> {
- Shell::with(move |shell| shell.stat(PathBuf::from(path))).await
+ Shell::with(async |shell| shell.stat(PathBuf::from(path)).await).await
}
#[api(
@@ -194,7 +197,7 @@ async fn stat_command(path: String) -> Result<(), Error> {
/// This will return an error if the entry is already present in the list or
/// if an invalid path was provided.
async fn select_command(path: String) -> Result<(), Error> {
- Shell::with(move |shell| shell.select(PathBuf::from(path))).await
+ Shell::with(async |shell| shell.select(PathBuf::from(path)).await).await
}
#[api(
@@ -212,13 +215,13 @@ async fn select_command(path: String) -> Result<(), Error> {
/// This will return an error if the entry was not found in the list of entries
/// selected for restore.
async fn deselect_command(path: String) -> Result<(), Error> {
- Shell::with(move |shell| shell.deselect(PathBuf::from(path))).await
+ Shell::with(async |shell| shell.deselect(PathBuf::from(path)).await).await
}
#[api( input: { properties: { } })]
/// Clear the list of files selected for restore.
async fn clear_selected_command() -> Result<(), Error> {
- Shell::with(move |shell| shell.deselect_all()).await
+ Shell::with(async |shell| shell.deselect_all().await).await
}
#[api(
@@ -235,7 +238,7 @@ async fn clear_selected_command() -> Result<(), Error> {
)]
/// List entries currently selected for restore.
async fn list_selected_command(patterns: bool) -> Result<(), Error> {
- Shell::with(move |shell| shell.list_selected(patterns)).await
+ Shell::with(async |shell| shell.list_selected(patterns).await).await
}
#[api(
@@ -255,7 +258,7 @@ async fn list_selected_command(patterns: bool) -> Result<(), Error> {
)]
/// Find entries in the catalog matching the given match pattern.
async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
- Shell::with(move |shell| shell.find(pattern, select)).await
+ Shell::with(async |shell| shell.find(pattern, select).await).await
}
#[api(
@@ -272,7 +275,7 @@ async fn find_command(pattern: String, select: bool) -> Result<(), Error> {
///
/// Target must not exist on the clients filesystem.
async fn restore_selected_command(target: String) -> Result<(), Error> {
- Shell::with(move |shell| shell.restore_selected(PathBuf::from(target))).await
+ Shell::with(async |shell| shell.restore_selected(PathBuf::from(target)).await).await
}
#[api(
@@ -295,7 +298,7 @@ async fn restore_selected_command(target: String) -> Result<(), Error> {
/// subset of this sub-archive.
/// If pattern is not present or empty, the full archive is restored to target.
async fn restore_command(target: String, pattern: Option<String>) -> Result<(), Error> {
- Shell::with(move |shell| shell.restore(PathBuf::from(target), pattern)).await
+ Shell::with(async |shell| shell.restore(PathBuf::from(target), pattern).await).await
}
/// TODO: Should we use this to fix `step()`? Make path resolution behave more like described in
@@ -305,9 +308,6 @@ async fn restore_command(target: String, pattern: Option<String>) -> Result<(),
/// trailing `Component::CurDir` entries. Since we only support regular paths we'll roll our own
/// here:
pub struct Shell {
- /// Readline instance handling input and callbacks
- rl: rustyline::Editor<CliHelper, rustyline::history::MemHistory>,
-
/// Interactive prompt.
prompt: String,
@@ -351,13 +351,6 @@ impl Shell {
archive_name: &str,
archive: Accessor,
) -> Result<Self, Error> {
- let cli_helper = CliHelper::new(catalog_shell_cli());
- let mut rl = rustyline::Editor::<CliHelper, _>::with_history(
- rustyline::Config::default(),
- rustyline::history::MemHistory::new(),
- )?;
- rl.set_helper(Some(cli_helper));
-
let mut position = Vec::new();
if let Some(catalog) = catalog.as_mut() {
let catalog_root = catalog.root()?;
@@ -385,7 +378,6 @@ impl Shell {
}
let mut this = Self {
- rl,
prompt: String::new(),
catalog,
selected: HashMap::new(),
@@ -396,28 +388,40 @@ impl Shell {
Ok(this)
}
- async fn with<'a, Fut, R, F>(call: F) -> Result<R, Error>
+ async fn with<R, F>(call: F) -> Result<R, Error>
where
- F: FnOnce(&'a mut Shell) -> Fut,
- Fut: Future<Output = Result<R, Error>>,
- F: 'a,
- Fut: 'a,
- R: 'static,
+ F: AsyncFnOnce(&mut Shell) -> Result<R, Error>,
{
- let shell: &mut Shell = unsafe { std::mem::transmute(SHELL.unwrap()) };
- call(&mut *shell).await
+ // Under the assumption that only one shell command is executed at a
+ // time, moving out of a std mutex like this ensures data integrity
+ // after panics (or cancellations) and detects deadlocks from recursion.
+ let mut shell = SHELL
+ .lock()
+ .unwrap()
+ .take()
+ .ok_or_else(|| format_err!("expected SHELL"))?;
+ let result = call(&mut shell).await;
+ *SHELL.lock().unwrap() = Some(shell);
+ result
}
- pub async fn shell(mut self) -> Result<(), Error> {
- let this = &mut self;
- unsafe {
- SHELL = Some(this as *mut Shell as usize);
- }
- while let Ok(line) = this.rl.readline(&this.prompt) {
+ pub async fn shell(self) -> Result<(), Error> {
+ let mut prompt = self.prompt.clone();
+
+ *SHELL.lock().unwrap() = Some(self);
+
+ let cli_helper = CliHelper::new(catalog_shell_cli());
+ let mut rl = rustyline::Editor::<CliHelper, _>::with_history(
+ rustyline::Config::default(),
+ rustyline::history::MemHistory::new(),
+ )?;
+ rl.set_helper(Some(cli_helper));
+
+ while let Ok(line) = rl.readline(&prompt) {
if line == "exit" {
break;
}
- let helper = this.rl.helper().unwrap();
+ let helper = rl.helper().unwrap();
let args = match cli::shellword_split(&line) {
Ok(args) => args,
Err(err) => {
@@ -429,9 +433,16 @@ impl Shell {
let _ =
cli::handle_command_future(helper.cmd_def(), "", args, cli::CliEnvironment::new())
.await;
- let _ = this.rl.add_history_entry(line);
- this.update_prompt();
+ let _ = rl.add_history_entry(line);
+
+ if let Some(shell) = &mut *SHELL.lock().unwrap() {
+ shell.update_prompt();
+ prompt = shell.prompt.clone();
+ } else {
+ bail!("SHELL missing at prompt update");
+ }
}
+ *SHELL.lock().unwrap() = None;
Ok(())
}
--
2.47.3
next prev parent reply other threads:[~2026-02-26 14:40 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-02-26 14:40 [PATCH v1 proxmox-backup 00/11] fix various warnings Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 01/11] datastore: remove allow(clippy::cast_ptr_alignment) attribute Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 02/11] api: use checked_div for compression ratio calculation Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 03/11] datastore+server: sort by key instead of using comparison functions Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 04/11] api: remove unnecessary ampersand to fix clippy warning Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 05/11] bin: debug: use pattern match instead of is_some+unwrap Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 06/11] bin: proxy: fix clippy warning about unnecesary use of find_map Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 07/11] datastore: silence warning about too many arguments Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 08/11] client: catalog shell: avoid unnecessary block_on in async code Robert Obkircher
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 09/11] client: catalog shell: combine multiple block_on calls into one Robert Obkircher
2026-02-26 14:40 ` Robert Obkircher [this message]
2026-02-26 14:40 ` [PATCH v1 proxmox-backup 11/11] tape: media catalog: use Flock wrapper instead of deprecated function Robert Obkircher
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260226144033.211039-11-r.obkircher@proxmox.com \
--to=r.obkircher@proxmox.com \
--cc=pbs-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox