use crate::{Config, SecurityStatus, LOG_TARGET};
use futures::join;
use std::{fmt, path::Path};
pub async fn check_security_status(config: &Config) -> Result<SecurityStatus, String> {
let Config { prepare_worker_program_path, secure_validator_mode, cache_path, .. } = config;
let (landlock, seccomp, change_root, secure_clone) = join!(
check_landlock(prepare_worker_program_path),
check_seccomp(prepare_worker_program_path),
check_can_unshare_user_namespace_and_change_root(prepare_worker_program_path, cache_path),
check_can_do_secure_clone(prepare_worker_program_path),
);
let full_security_status = FullSecurityStatus::new(
*secure_validator_mode,
landlock,
seccomp,
change_root,
secure_clone,
);
let security_status = full_security_status.as_partial();
if full_security_status.err_occurred() {
print_secure_mode_error_or_warning(&full_security_status);
if !full_security_status.all_errs_allowed() {
return Err("could not enable Secure Validator Mode; check logs".into())
}
}
if security_status.secure_validator_mode {
gum::info!(
target: LOG_TARGET,
"👮♀️ Running in Secure Validator Mode. \
It is highly recommended that you operate according to our security guidelines. \
\nMore information: https://wiki.polkadot.network/docs/maintain-guides-secure-validator#secure-validator-mode"
);
}
Ok(security_status)
}
struct FullSecurityStatus {
partial: SecurityStatus,
errs: Vec<SecureModeError>,
}
impl FullSecurityStatus {
fn new(
secure_validator_mode: bool,
landlock: SecureModeResult,
seccomp: SecureModeResult,
change_root: SecureModeResult,
secure_clone: SecureModeResult,
) -> Self {
Self {
partial: SecurityStatus {
secure_validator_mode,
can_enable_landlock: landlock.is_ok(),
can_enable_seccomp: seccomp.is_ok(),
can_unshare_user_namespace_and_change_root: change_root.is_ok(),
can_do_secure_clone: secure_clone.is_ok(),
},
errs: [landlock, seccomp, change_root, secure_clone]
.into_iter()
.filter_map(|result| result.err())
.collect(),
}
}
fn as_partial(&self) -> SecurityStatus {
self.partial.clone()
}
fn err_occurred(&self) -> bool {
!self.errs.is_empty()
}
fn all_errs_allowed(&self) -> bool {
!self.partial.secure_validator_mode ||
self.errs.iter().all(|err| err.is_allowed_in_secure_mode(&self.partial))
}
fn errs_string(&self) -> String {
self.errs
.iter()
.map(|err| {
format!(
"\n - {}{}",
if err.is_allowed_in_secure_mode(&self.partial) { "Optional: " } else { "" },
err
)
})
.collect()
}
}
type SecureModeResult = std::result::Result<(), SecureModeError>;
#[derive(Debug)]
enum SecureModeError {
CannotEnableLandlock { err: String, abi: u8 },
CannotEnableSeccomp(String),
CannotUnshareUserNamespaceAndChangeRoot(String),
CannotDoSecureClone(String),
}
impl SecureModeError {
fn is_allowed_in_secure_mode(&self, security_status: &SecurityStatus) -> bool {
use SecureModeError::*;
match self {
CannotEnableLandlock { .. } =>
security_status.can_unshare_user_namespace_and_change_root,
CannotEnableSeccomp(_) => false,
CannotUnshareUserNamespaceAndChangeRoot(_) => security_status.can_enable_landlock,
CannotDoSecureClone(_) => true,
}
}
}
impl fmt::Display for SecureModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use SecureModeError::*;
match self {
CannotEnableLandlock{err, abi} => write!(f, "Cannot enable landlock (ABI {abi}), a Linux 5.13+ kernel security feature: {err}"),
CannotEnableSeccomp(err) => write!(f, "Cannot enable seccomp, a Linux-specific kernel security feature: {err}"),
CannotUnshareUserNamespaceAndChangeRoot(err) => write!(f, "Cannot unshare user namespace and change root, which are Linux-specific kernel security features: {err}"),
CannotDoSecureClone(err) => write!(f, "Cannot call clone with all sandboxing flags, a Linux-specific kernel security features: {err}"),
}
}
}
fn print_secure_mode_error_or_warning(security_status: &FullSecurityStatus) {
let all_errs_allowed = security_status.all_errs_allowed();
let errs_string = security_status.errs_string();
if all_errs_allowed {
gum::warn!(
target: LOG_TARGET,
"{}{}",
crate::SECURE_MODE_WARNING,
errs_string,
);
} else {
gum::error!(
target: LOG_TARGET,
"{}{}{}",
crate::SECURE_MODE_ERROR,
errs_string,
crate::IGNORE_SECURE_MODE_TIP
);
}
}
async fn check_can_unshare_user_namespace_and_change_root(
prepare_worker_program_path: &Path,
cache_path: &Path,
) -> SecureModeResult {
let cache_dir_tempdir = tempfile::Builder::new()
.prefix("check-can-unshare-")
.tempdir_in(cache_path)
.map_err(|err| {
SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(format!(
"could not create a temporary directory in {:?}: {}",
cache_path, err
))
})?;
spawn_process_for_security_check(
prepare_worker_program_path,
"--check-can-unshare-user-namespace-and-change-root",
&[cache_dir_tempdir.path()],
)
.await
.map_err(|err| SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(err))
}
async fn check_landlock(prepare_worker_program_path: &Path) -> SecureModeResult {
let abi = polkadot_node_core_pvf_common::worker::security::landlock::LANDLOCK_ABI as u8;
spawn_process_for_security_check(
prepare_worker_program_path,
"--check-can-enable-landlock",
std::iter::empty::<&str>(),
)
.await
.map_err(|err| SecureModeError::CannotEnableLandlock { err, abi })
}
#[cfg(target_arch = "x86_64")]
async fn check_seccomp(prepare_worker_program_path: &Path) -> SecureModeResult {
spawn_process_for_security_check(
prepare_worker_program_path,
"--check-can-enable-seccomp",
std::iter::empty::<&str>(),
)
.await
.map_err(|err| SecureModeError::CannotEnableSeccomp(err))
}
#[cfg(not(target_arch = "x86_64"))]
async fn check_seccomp(_: &Path) -> SecureModeResult {
Err(SecureModeError::CannotEnableSeccomp(
"only supported on CPUs from the x86_64 family (usually Intel or AMD)".into(),
))
}
async fn check_can_do_secure_clone(prepare_worker_program_path: &Path) -> SecureModeResult {
spawn_process_for_security_check(
prepare_worker_program_path,
"--check-can-do-secure-clone",
std::iter::empty::<&str>(),
)
.await
.map_err(|err| SecureModeError::CannotDoSecureClone(err))
}
async fn spawn_process_for_security_check<I, S>(
prepare_worker_program_path: &Path,
check_arg: &'static str,
extra_args: I,
) -> Result<(), String>
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
let mut command = tokio::process::Command::new(prepare_worker_program_path);
command.env_clear();
if let Ok(value) = std::env::var("RUST_LOG") {
command.env("RUST_LOG", value);
}
match command.arg(check_arg).args(extra_args).output().await {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => {
let stderr = std::str::from_utf8(&output.stderr)
.expect("child process writes a UTF-8 string to stderr; qed")
.trim();
if stderr.is_empty() {
Err("not available".into())
} else {
Err(format!("not available: {}", stderr))
}
},
Err(err) => Err(format!("could not start child process: {}", err)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_secure_mode_error_optionality() {
let err = SecureModeError::CannotEnableLandlock { err: String::new(), abi: 3 };
assert!(err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: false,
can_enable_seccomp: false,
can_unshare_user_namespace_and_change_root: true,
can_do_secure_clone: true,
}));
assert!(!err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: false,
can_enable_seccomp: true,
can_unshare_user_namespace_and_change_root: false,
can_do_secure_clone: false,
}));
let err = SecureModeError::CannotEnableSeccomp(String::new());
assert!(!err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: false,
can_enable_seccomp: false,
can_unshare_user_namespace_and_change_root: true,
can_do_secure_clone: true,
}));
assert!(!err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: false,
can_enable_seccomp: true,
can_unshare_user_namespace_and_change_root: false,
can_do_secure_clone: false,
}));
let err = SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(String::new());
assert!(err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: true,
can_enable_seccomp: false,
can_unshare_user_namespace_and_change_root: false,
can_do_secure_clone: false,
}));
assert!(!err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: false,
can_enable_seccomp: true,
can_unshare_user_namespace_and_change_root: false,
can_do_secure_clone: false,
}));
let err = SecureModeError::CannotDoSecureClone(String::new());
assert!(err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: true,
can_enable_landlock: true,
can_enable_seccomp: true,
can_unshare_user_namespace_and_change_root: true,
can_do_secure_clone: true,
}));
assert!(err.is_allowed_in_secure_mode(&SecurityStatus {
secure_validator_mode: false,
can_enable_landlock: false,
can_enable_seccomp: false,
can_unshare_user_namespace_and_change_root: false,
can_do_secure_clone: false,
}));
}
}