diff --git a/src/admin/server/commands.rs b/src/admin/server/commands.rs index 28962ef2..77b20377 100644 --- a/src/admin/server/commands.rs +++ b/src/admin/server/commands.rs @@ -1,4 +1,4 @@ -use conduit::{warn, Result}; +use conduit::{warn, Error, Result}; use ruma::events::room::message::RoomMessageEventContent; use crate::services; @@ -102,7 +102,17 @@ pub(super) async fn reload(_body: Vec<&str>) -> Result } #[cfg(unix)] -pub(super) async fn restart(_body: Vec<&str>) -> Result { +pub(super) async fn restart(_body: Vec<&str>, force: bool) -> Result { + use conduit::utils::sys::current_exe_deleted; + + if !force && current_exe_deleted() { + return Err(Error::Err( + "The server cannot be restarted because the executable was tampered with. If this is expected use --force \ + to override." + .to_owned(), + )); + } + services().server.restart()?; Ok(RoomMessageEventContent::notice_plain("Restarting server...")) diff --git a/src/admin/server/mod.rs b/src/admin/server/mod.rs index 13f6e265..b4222251 100644 --- a/src/admin/server/mod.rs +++ b/src/admin/server/mod.rs @@ -51,7 +51,10 @@ pub(super) enum ServerCommand { #[cfg(unix)] /// - Restart the server - Restart, + Restart { + #[arg(short, long)] + force: bool, + }, /// - Shutdown the server Shutdown, @@ -77,7 +80,9 @@ pub(super) async fn process(command: ServerCommand, body: Vec<&str>) -> Result reload(body).await?, #[cfg(unix)] - ServerCommand::Restart => restart(body).await?, + ServerCommand::Restart { + force, + } => restart(body, force).await?, ServerCommand::Shutdown => shutdown(body).await?, }) } diff --git a/src/core/utils/sys.rs b/src/core/utils/sys.rs index 825ec903..6c396921 100644 --- a/src/core/utils/sys.rs +++ b/src/core/utils/sys.rs @@ -34,3 +34,34 @@ pub fn available_parallelism() -> usize { .expect("Unable to query for available parallelism.") .get() } + +/// Return a possibly corrected std::env::current_exe() even if the path is +/// marked deleted. +/// +/// # Safety +/// This function is declared unsafe because the original result was altered for +/// security purposes, and altering it back ignores those urposes and should be +/// understood by the user. +pub unsafe fn current_exe() -> Result { + use std::path::PathBuf; + + let exe = std::env::current_exe()?; + match exe.to_str() { + None => Ok(exe), + Some(str) => Ok(str + .strip_suffix(" (deleted)") + .map(PathBuf::from) + .unwrap_or(exe)), + } +} + +/// Determine if the server's executable was removed or replaced. This is a +/// specific check; useful for successful restarts. May not be available or +/// accurate on all platforms; defaults to false. +#[must_use] +pub fn current_exe_deleted() -> bool { + std::env::current_exe().map_or(false, |exe| { + exe.to_str() + .map_or(false, |exe| exe.ends_with(" (deleted)")) + }) +} diff --git a/src/main/restart.rs b/src/main/restart.rs index 559d7446..00de9f21 100644 --- a/src/main/restart.rs +++ b/src/main/restart.rs @@ -2,10 +2,20 @@ use std::{env, os::unix::process::CommandExt, process::Command}; -use conduit::{debug, info}; +use conduit::{debug, info, utils}; pub(super) fn restart() -> ! { - let exe = env::current_exe().expect("program path must be identified and available"); + // SAFETY: We have allowed an override for the case where the current_exe() has + // been replaced or removed. By default the server will fail to restart if the + // binary has been replaced (i.e. by cargo); this is for security purposes. + // Command::exec() used to panic in that case. + // + // We can (and do) prevent that panic by checking the result of current_exe() + // prior to committing to restart, returning an error to the user without any + // unexpected shutdown. In a nutshell that is the execuse for this unsafety. + // Nevertheless, we still want a way to override the restart preventation (i.e. + // admin server restart --force). + let exe = unsafe { utils::sys::current_exe().expect("program path must be available") }; let envs = env::vars(); let args = env::args().skip(1); debug!(?exe, ?args, ?envs, "Restart");