Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor pvf security module #3047

Merged
merged 6 commits into from
Feb 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions polkadot/node/core/pvf/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ See also:
Running `cargo test` in the `pvf/` directory will run unit and integration
tests.

**Note:** some tests run only under Linux, amd64, and/or with the
**Note:** some tests run only under Linux, x86-64, and/or with the
`ci-only-tests` feature enabled.

See the general [Testing][testing] instructions for more information on
Expand All @@ -34,8 +34,8 @@ RUST_LOG=parachain::pvf=trace zombienet --provider=native spawn zombienet_tests/
## Testing on Linux

Some of the PVF functionality, especially related to security, is Linux-only,
and some is amd64-only. If you touch anything security-related, make sure to
test on Linux amd64! If you're on a Mac, you can either run a VM or you can hire
and some is x86-64-only. If you touch anything security-related, make sure to
test on Linux x86-64! If you're on a Mac, you can either run a VM or you can hire
a VPS and use the open-source tool [EternalTerminal][et] to connect to it.[^et]

[^et]: Unlike ssh, ET preserves your session across disconnects, and unlike
Expand Down
2 changes: 2 additions & 0 deletions polkadot/node/core/pvf/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ sp-tracing = { path = "../../../../../substrate/primitives/tracing" }
[target.'cfg(target_os = "linux")'.dependencies]
landlock = "0.3.0"
nix = { version = "0.27.1", features = ["sched"] }

[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
seccompiler = "0.4.0"

[dev-dependencies]
Expand Down
30 changes: 30 additions & 0 deletions polkadot/node/core/pvf/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,33 @@ pub fn framed_recv_blocking(r: &mut (impl Read + Unpin)) -> io::Result<Vec<u8>>
r.read_exact(&mut buf)?;
Ok(buf)
}

#[cfg(all(test, not(feature = "test-utils")))]
mod tests {
use super::*;

#[test]
fn default_secure_status() {
let status = SecurityStatus::default();
assert!(
!status.secure_validator_mode,
"secure_validator_mode is false for default security status"
);
assert!(
!status.can_enable_landlock,
"can_enable_landlock is false for default security status"
);
assert!(
!status.can_enable_seccomp,
"can_enable_seccomp is false for default security status"
);
assert!(
!status.can_unshare_user_namespace_and_change_root,
"can_unshare_user_namespace_and_change_root is false for default security status"
);
assert!(
!status.can_do_secure_clone,
"can_do_secure_clone is false for default security status"
);
}
}
26 changes: 24 additions & 2 deletions polkadot/node/core/pvf/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
artifacts::{ArtifactId, ArtifactPathId, ArtifactState, Artifacts},
execute::{self, PendingExecutionRequest},
metrics::Metrics,
prepare, security, Priority, SecurityStatus, ValidationError, LOG_TARGET,
prepare, Priority, SecurityStatus, ValidationError, LOG_TARGET,
};
use always_assert::never;
use futures::{
Expand Down Expand Up @@ -225,10 +225,32 @@ pub async fn start(

// Run checks for supported security features once per host startup. If some checks fail, warn
// if Secure Validator Mode is disabled and return an error otherwise.
let security_status = match security::check_security_status(&config).await {
#[cfg(target_os = "linux")]
let security_status = match crate::security::check_security_status(&config).await {
Ok(ok) => ok,
Err(err) => return Err(SubsystemError::Context(err)),
};
#[cfg(not(target_os = "linux"))]
let security_status = if config.secure_validator_mode {
gum::error!(
target: LOG_TARGET,
"{}{}{}",
crate::SECURE_MODE_ERROR,
crate::SECURE_LINUX_NOTE,
crate::IGNORE_SECURE_MODE_TIP
);
return Err(SubsystemError::Context(
"could not enable Secure Validator Mode for non-Linux; check logs".into(),
));
} else {
gum::warn!(
target: LOG_TARGET,
"{}{}",
crate::SECURE_MODE_WARNING,
crate::SECURE_LINUX_NOTE,
);
SecurityStatus::default()
};

let (to_host_tx, to_host_rx) = mpsc::channel(HOST_MESSAGE_QUEUE_SIZE);

Expand Down
20 changes: 20 additions & 0 deletions polkadot/node/core/pvf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ mod host;
mod metrics;
mod prepare;
mod priority;
#[cfg(target_os = "linux")]
mod security;
mod worker_interface;

Expand Down Expand Up @@ -135,3 +136,22 @@ pub fn get_worker_version(worker_path: &Path) -> std::io::Result<String> {
.trim()
.to_string())
}

// Trying to run securely and some mandatory errors occurred.
pub(crate) const SECURE_MODE_ERROR: &'static str =
"🚨 Your system cannot securely run a validator. \
\nRunning validation of malicious PVF code has a higher risk of compromising this machine.";
// Some errors occurred when running insecurely, or some optional errors occurred when running
// securely.
pub(crate) const SECURE_MODE_WARNING: &'static str = "🚨 Some security issues have been detected. \
\nRunning validation of malicious PVF code has a higher risk of compromising this machine.";
// Message to be printed only when running securely and mandatory errors occurred.
pub(crate) const IGNORE_SECURE_MODE_TIP: &'static str =
"\nYou can ignore this error with the `--insecure-validator-i-know-what-i-do` \
command line argument if you understand and accept the risks of running insecurely. \
With this flag, security features are enabled on a best-effort basis, but not mandatory. \
\nMore information: https://wiki.polkadot.network/docs/maintain-guides-secure-validator#secure-validator-mode";
// Only Linux supports security features
#[cfg(not(target_os = "linux"))]
pub(crate) const SECURE_LINUX_NOTE: &'static str = "\nSecure mode is enabled only for Linux \
\nand a full secure mode is enabled only for Linux x86-64.";
160 changes: 53 additions & 107 deletions polkadot/node/core/pvf/src/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,37 +169,23 @@ impl fmt::Display for SecureModeError {

/// Print an error if Secure Validator Mode and some mandatory errors occurred, warn otherwise.
fn print_secure_mode_error_or_warning(security_status: &FullSecurityStatus) {
// Trying to run securely and some mandatory errors occurred.
const SECURE_MODE_ERROR: &'static str = "🚨 Your system cannot securely run a validator. \
\nRunning validation of malicious PVF code has a higher risk of compromising this machine.";
// Some errors occurred when running insecurely, or some optional errors occurred when running
// securely.
const SECURE_MODE_WARNING: &'static str = "🚨 Some security issues have been detected. \
\nRunning validation of malicious PVF code has a higher risk of compromising this machine.";
// Message to be printed only when running securely and mandatory errors occurred.
const IGNORE_SECURE_MODE_TIP: &'static str =
"\nYou can ignore this error with the `--insecure-validator-i-know-what-i-do` \
command line argument if you understand and accept the risks of running insecurely. \
With this flag, security features are enabled on a best-effort basis, but not mandatory. \
\nMore information: https://wiki.polkadot.network/docs/maintain-guides-secure-validator#secure-validator-mode";

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,
"{}{}",
SECURE_MODE_WARNING,
crate::SECURE_MODE_WARNING,
errs_string,
);
} else {
gum::error!(
target: LOG_TARGET,
"{}{}{}",
SECURE_MODE_ERROR,
crate::SECURE_MODE_ERROR,
errs_string,
IGNORE_SECURE_MODE_TIP
crate::IGNORE_SECURE_MODE_TIP
);
}
}
Expand All @@ -210,122 +196,82 @@ fn print_secure_mode_error_or_warning(security_status: &FullSecurityStatus) {
/// to running the check in a worker, we try it... in a worker. The expected return status is 0 on
/// success and -1 on failure.
async fn check_can_unshare_user_namespace_and_change_root(
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
prepare_worker_program_path: &Path,
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))] cache_path: &Path,
cache_path: &Path,
) -> SecureModeResult {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
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))
} else {
Err(SecureModeError::CannotUnshareUserNamespaceAndChangeRoot(
"only available on Linux".into()
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))
}

/// Check if landlock is supported and return an error if not.
///
/// We do this check by spawning a new process and trying to sandbox it. To get as close as possible
/// to running the check in a worker, we try it... in a worker. The expected return status is 0 on
/// success and -1 on failure.
async fn check_landlock(
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
prepare_worker_program_path: &Path,
) -> SecureModeResult {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
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 })
} else {
Err(SecureModeError::CannotEnableLandlock {
err: "only available on Linux".into(),
abi: 0,
})
}
}
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 })
}

/// Check if seccomp is supported and return an error if not.
///
/// We do this check by spawning a new process and trying to sandbox it. To get as close as possible
/// to running the check in a worker, we try it... in a worker. The expected return status is 0 on
/// success and -1 on failure.
async fn check_seccomp(
#[cfg_attr(not(all(target_os = "linux", target_arch = "x86_64")), allow(unused_variables))]
prepare_worker_program_path: &Path,
) -> SecureModeResult {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
cfg_if::cfg_if! {
if #[cfg(target_arch = "x86_64")] {
spawn_process_for_security_check(
prepare_worker_program_path,
"--check-can-enable-seccomp",
std::iter::empty::<&str>(),
).await.map_err(|err| SecureModeError::CannotEnableSeccomp(err))
} else {
Err(SecureModeError::CannotEnableSeccomp(
"only supported on CPUs from the x86_64 family (usually Intel or AMD)".into()
))
}
}
} else {
cfg_if::cfg_if! {
if #[cfg(target_arch = "x86_64")] {
Err(SecureModeError::CannotEnableSeccomp(
"only supported on Linux".into()
))
} else {
Err(SecureModeError::CannotEnableSeccomp(
"only supported on Linux and on CPUs from the x86_64 family (usually Intel or AMD).".into()
))
}
}
}
}

#[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(),
))
}

/// Check if we can call `clone` with all sandboxing flags, and return an error if not.
///
/// We do this check by spawning a new process and trying to sandbox it. To get as close as possible
/// to running the check in a worker, we try it... in a worker. The expected return status is 0 on
/// success and -1 on failure.
async fn check_can_do_secure_clone(
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
prepare_worker_program_path: &Path,
) -> SecureModeResult {
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
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))
} else {
Err(SecureModeError::CannotDoSecureClone(
"only available on Linux".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))
}

#[cfg(target_os = "linux")]
async fn spawn_process_for_security_check<I, S>(
prepare_worker_program_path: &Path,
check_arg: &'static str,
Expand Down
Loading