mysqladm/core/
bootstrap.rs

1use std::{fs, path::PathBuf};
2
3use anyhow::Context;
4use clap_verbosity_flag::Verbosity;
5use nix::libc::{EXIT_SUCCESS, exit};
6use std::os::unix::net::UnixStream as StdUnixStream;
7use tokio::net::UnixStream as TokioUnixStream;
8
9use crate::{
10    core::common::{
11        DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executable_is_suid_or_sgid,
12    },
13    server::{config::read_config_from_path, server_loop::handle_requests_for_single_session},
14};
15
16/// Determine whether we will make a connection to an external server
17/// or start an internal server with elevated privileges.
18///
19/// If neither is feasible, an error is returned.
20fn will_connect_to_external_server(
21    server_socket_path: Option<&PathBuf>,
22    // This parameter is only used in suid-sgid-mode
23    #[allow(unused_variables)] config_path: Option<&PathBuf>,
24) -> anyhow::Result<bool> {
25    if server_socket_path.is_some() {
26        return Ok(true);
27    }
28
29    #[cfg(feature = "suid-sgid-mode")]
30    if config_path.is_some() {
31        return Ok(false);
32    }
33
34    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
35        return Ok(true);
36    }
37
38    #[cfg(feature = "suid-sgid-mode")]
39    if fs::metadata(DEFAULT_CONFIG_PATH).is_ok() {
40        return Ok(false);
41    }
42
43    #[cfg(feature = "suid-sgid-mode")]
44    anyhow::bail!("No socket path or config path provided, and no default socket or config found");
45
46    #[cfg(not(feature = "suid-sgid-mode"))]
47    anyhow::bail!("No socket path provided, and no default socket found");
48}
49
50/// This function is used to bootstrap the connection to the server.
51/// This can happen in two ways:
52///
53/// 1. If a socket path is provided, or exists in the default location,
54///    the function will connect to the socket and authenticate with the
55///    server to ensure that the server knows the uid of the client.
56///
57/// 2. If a config path is provided, or exists in the default location,
58///    and the config is readable, the function will assume it is either
59///    setuid or setgid, and will fork a child process to run the server
60///    with the provided config. The server will exit silently by itself
61///    when it is done, and this function will only return for the client
62///    with the socket for the server.
63///
64/// If neither of these options are available, the function will fail.
65///
66/// Note that this function is also responsible for setting up logging,
67/// because in the case of an internal server, we need to drop privileges
68/// before we can initialize logging.
69///
70/// **WARNING:** This function may be run with elevated privileges.
71pub fn bootstrap_server_connection_and_drop_privileges(
72    server_socket_path: Option<PathBuf>,
73    config: Option<PathBuf>,
74    verbose: Verbosity,
75) -> anyhow::Result<StdUnixStream> {
76    if will_connect_to_external_server(server_socket_path.as_ref(), config.as_ref())? {
77        assert!(
78            !executable_is_suid_or_sgid()?,
79            "The executable should not be SUID or SGID when connecting to an external server"
80        );
81
82        env_logger::Builder::new()
83            .filter_level(verbose.log_level_filter())
84            .init();
85
86        connect_to_external_server(server_socket_path)
87    } else if cfg!(feature = "suid-sgid-mode") {
88        // NOTE: We need to be really careful with the code up until this point,
89        //       as we might be running with elevated privileges.
90        let server_connection = bootstrap_internal_server_and_drop_privs(config)?;
91
92        env_logger::Builder::new()
93            .filter_level(verbose.log_level_filter())
94            .init();
95
96        Ok(server_connection)
97    } else {
98        anyhow::bail!("SUID/SGID support is not enabled, cannot start internal server");
99    }
100}
101
102fn connect_to_external_server(
103    server_socket_path: Option<PathBuf>,
104) -> anyhow::Result<StdUnixStream> {
105    // TODO: ensure this is both readable and writable
106    if let Some(socket_path) = server_socket_path {
107        log::debug!("Connecting to socket at {:?}", socket_path);
108        return match StdUnixStream::connect(socket_path) {
109            Ok(socket) => Ok(socket),
110            Err(e) => match e.kind() {
111                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
112                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
113                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
114            },
115        };
116    }
117
118    if fs::metadata(DEFAULT_SOCKET_PATH).is_ok() {
119        log::debug!("Connecting to default socket at {:?}", DEFAULT_SOCKET_PATH);
120        return match StdUnixStream::connect(DEFAULT_SOCKET_PATH) {
121            Ok(socket) => Ok(socket),
122            Err(e) => match e.kind() {
123                std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
124                std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
125                _ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
126            },
127        };
128    }
129
130    anyhow::bail!("No socket path provided, and no default socket found");
131}
132
133// TODO: this function is security critical, it should be integration tested
134//       in isolation.
135/// Drop privileges to the real user and group of the process.
136/// If the process is not running with elevated privileges, this function
137/// is a no-op.
138fn drop_privs() -> anyhow::Result<()> {
139    log::debug!("Dropping privileges");
140    let real_uid = nix::unistd::getuid();
141    let real_gid = nix::unistd::getgid();
142
143    nix::unistd::setuid(real_uid).context("Failed to drop privileges")?;
144    nix::unistd::setgid(real_gid).context("Failed to drop privileges")?;
145
146    debug_assert_eq!(nix::unistd::getuid(), real_uid);
147    debug_assert_eq!(nix::unistd::getgid(), real_gid);
148
149    log::debug!("Privileges dropped successfully");
150    Ok(())
151}
152
153fn bootstrap_internal_server_and_drop_privs(
154    config_path: Option<PathBuf>,
155) -> anyhow::Result<StdUnixStream> {
156    if let Some(config_path) = config_path {
157        if !executable_is_suid_or_sgid()? {
158            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
159        }
160
161        // ensure config exists and is readable
162        if fs::metadata(&config_path).is_err() {
163            return Err(anyhow::anyhow!("Config file not found or not readable"));
164        }
165
166        log::debug!("Starting server with config at {:?}", config_path);
167        let socket = invoke_server_with_config(config_path)?;
168        drop_privs()?;
169        return Ok(socket);
170    };
171
172    let config_path = PathBuf::from(DEFAULT_CONFIG_PATH);
173    if fs::metadata(&config_path).is_ok() {
174        if !executable_is_suid_or_sgid()? {
175            anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
176        }
177        log::debug!("Starting server with default config at {:?}", config_path);
178        let socket = invoke_server_with_config(config_path)?;
179        drop_privs()?;
180        return Ok(socket);
181    };
182
183    anyhow::bail!("No config path provided, and no default config found");
184}
185
186// TODO: we should somehow ensure that the forked process is killed on completion,
187//       just in case the client does not behave properly.
188/// Fork a child process to run the server with the provided config.
189/// The server will exit silently by itself when it is done, and this function
190/// will only return for the client with the socket for the server.
191fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result<StdUnixStream> {
192    let (server_socket, client_socket) = StdUnixStream::pair()?;
193    let unix_user = UnixUser::from_uid(nix::unistd::getuid().as_raw())?;
194
195    match (unsafe { nix::unistd::fork() }).context("Failed to fork")? {
196        nix::unistd::ForkResult::Parent { child } => {
197            log::debug!("Forked child process with PID {}", child);
198            Ok(client_socket)
199        }
200        nix::unistd::ForkResult::Child => {
201            log::debug!("Running server in child process");
202
203            match run_forked_server(config_path, server_socket, unix_user) {
204                Err(e) => Err(e),
205                Ok(_) => unreachable!(),
206            }
207        }
208    }
209}
210
211/// Run the server in the forked child process.
212/// This function will not return, but will exit the process with a success code.
213fn run_forked_server(
214    config_path: PathBuf,
215    server_socket: StdUnixStream,
216    unix_user: UnixUser,
217) -> anyhow::Result<()> {
218    let config = read_config_from_path(Some(config_path))?;
219
220    let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread()
221        .enable_all()
222        .build()
223        .unwrap()
224        .block_on(async {
225            let socket = TokioUnixStream::from_std(server_socket)?;
226            handle_requests_for_single_session(socket, &unix_user, &config).await?;
227            Ok(())
228        });
229
230    result?;
231
232    unsafe {
233        exit(EXIT_SUCCESS);
234    }
235}