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

Use clone instead of fork on pvf #2477

Merged
merged 67 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
5986873
use clone instead of fork for linux in prepare worker
jpserrat Nov 24, 2023
3564305
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Nov 24, 2023
510714a
minor fix
jpserrat Nov 24, 2023
3f81a53
minor fix
jpserrat Nov 24, 2023
a777c6e
remove unused import
jpserrat Nov 25, 2023
8c4a080
use raw fd instead of pipe on clone and fork call
jpserrat Nov 29, 2023
6e490ab
merge with master
jpserrat Nov 29, 2023
0bac33f
remove unused import
jpserrat Nov 29, 2023
8e313c8
fix fmt
jpserrat Nov 29, 2023
ec5b4af
remove unused import
jpserrat Nov 29, 2023
c512b43
remove os_pipe
jpserrat Dec 2, 2023
eca157c
use file instead of raw fd
jpserrat Dec 3, 2023
01c66e1
Merge branch 'master' into HEAD
mrcnski Dec 5, 2023
8e3e7a8
Merge branch 'master' into jpserrat/clone-instead-of-fork
mrcnski Dec 5, 2023
18ada83
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Dec 17, 2023
2fc4180
minor fixes
jpserrat Dec 18, 2023
6c39804
fmt manually
mrcnski Dec 18, 2023
5304a59
wrap File type with PipeFd
jpserrat Dec 19, 2023
ac86382
Merge branch 'jpserrat/clone-instead-of-fork' of github.com:Jpserrat/…
jpserrat Dec 19, 2023
4d9e5ef
fmt
jpserrat Dec 19, 2023
4cf8949
remove unnecessary `unsafe` block
jpserrat Dec 19, 2023
1df58cd
add clone to excute worker
jpserrat Dec 19, 2023
c85f326
add clone to excute worker
jpserrat Dec 19, 2023
e87f145
add documentation to PipeFd
jpserrat Dec 20, 2023
c9e3f5f
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Dec 20, 2023
f9e61cc
minor fixes
jpserrat Dec 20, 2023
e34aceb
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Dec 21, 2023
17363c0
Merge branch 'master' into jpserrat/clone-instead-of-fork
mrcnski Dec 22, 2023
fcbe008
Fix some errors
mrcnski Dec 22, 2023
b90a9b8
Fix some more stuff
mrcnski Dec 22, 2023
adc1213
Some fixes
mrcnski Dec 22, 2023
96f4451
Add check for clone capability; many fixes
mrcnski Dec 26, 2023
678acac
Update syscall lists
mrcnski Dec 26, 2023
4132b7b
Add prdoc! 🙂
mrcnski Dec 26, 2023
fd46250
Small update to prdoc
mrcnski Dec 26, 2023
9bf4e54
Merge branch 'jpserrat/clone-instead-of-fork' of github.com:Jpserrat/…
jpserrat Dec 26, 2023
3a8434f
change execute worker stack size
jpserrat Dec 26, 2023
a8c4229
minor fixes
jpserrat Dec 26, 2023
77b276f
minor fixes
jpserrat Dec 26, 2023
a931e91
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Dec 27, 2023
4c00ae0
Move exec param operations out of loop
mrcnski Dec 28, 2023
3366dc2
Remove any
mrcnski Dec 28, 2023
dc4dc1a
add solid proof for `deterministic_stack_limit`, fix comment for get_…
jpserrat Dec 28, 2023
c5775b4
Merge branch 'jpserrat/clone-instead-of-fork' of github.com:Jpserrat/…
jpserrat Dec 28, 2023
82ee6c4
fmt
jpserrat Dec 28, 2023
70d5600
Merge branch 'master' of github.com:paritytech/polkadot-sdk into jpse…
jpserrat Dec 28, 2023
f72d8ef
move execute worker thread number to a constant
jpserrat Dec 28, 2023
2e3140b
Update polkadot/node/core/pvf/execute-worker/src/lib.rs
mrcnski Dec 29, 2023
97acfdc
Remove brittle proof + account for threads for prepare process stack
mrcnski Dec 29, 2023
f07cc39
Merge branch 'master' into jpserrat/clone-instead-of-fork
mrcnski Dec 29, 2023
1f64577
Do fork as fallback if can_do_secure_clone is false
mrcnski Jan 1, 2024
c79f708
Merge remote-tracking branch 'Jpserrat/jpserrat/clone-instead-of-fork…
mrcnski Jan 1, 2024
c19682e
Merge remote-tracking branch 'origin/master' into jpserrat/clone-inst…
mrcnski Jan 1, 2024
ab87bca
Fix compile error
mrcnski Jan 1, 2024
48942cd
fix formatting; fix error due to merge
mrcnski Jan 1, 2024
08153fb
Fix compile errors
mrcnski Jan 2, 2024
422db3f
remove pdeathsig and associated tests
mrcnski Jan 2, 2024
fa7fdf8
A bit more cleanup
mrcnski Jan 2, 2024
c138f8f
Remove more unused code
mrcnski Jan 2, 2024
69713a7
Update syscall lists
mrcnski Jan 2, 2024
4b66d38
Parametrize clone_flags() on unshare security capability
mrcnski Jan 8, 2024
dcbb90b
Clarify comment
mrcnski Jan 8, 2024
ad62e9e
Remove test, it didn't update security status correctly
mrcnski Jan 8, 2024
4217eb9
implement AsRawFd and FromRawFd for the PipeFd struct
jpserrat Jan 9, 2024
891d126
Remove stack_size parameter
mrcnski Jan 12, 2024
bb0a998
Do security checks with env vars cleared
mrcnski Jan 12, 2024
aa62f4e
Merge branch 'master' into jpserrat/clone-instead-of-fork
mrcnski Jan 21, 2024
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
14 changes: 2 additions & 12 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions polkadot/node/core/pvf/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ sp-tracing = { path = "../../../../../substrate/primitives/tracing" }

[target.'cfg(target_os = "linux")'.dependencies]
landlock = "0.3.0"
nix = { version = "0.27.1", features = ["sched"] }
seccompiler = "0.4.0"

[dev-dependencies]
Expand Down
5 changes: 3 additions & 2 deletions polkadot/node/core/pvf/common/src/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,11 @@ pub enum JobError {
TimedOut,
#[error("An unexpected panic has occurred in the execution job: {0}")]
Panic(String),
/// Some error occurred when interfacing with the kernel.
#[error("Error interfacing with the kernel: {0}")]
Kernel(String),
#[error("Could not spawn the requested thread: {0}")]
CouldNotSpawnThread(String),
#[error("An error occurred in the CPU time monitor thread: {0}")]
CpuTimeMonitorThread(String),
#[error("Could not set pdeathsig: {0}")]
CouldNotSetPdeathsig(String),
}
13 changes: 8 additions & 5 deletions polkadot/node/core/pvf/common/src/executor_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,15 +140,18 @@ pub unsafe fn create_runtime_from_artifact_bytes(
executor_params: &ExecutorParams,
) -> Result<WasmtimeRuntime, WasmError> {
let mut config = DEFAULT_CONFIG.clone();
config.semantics = params_to_wasmtime_semantics(executor_params);
config.semantics = params_to_wasmtime_semantics(executor_params).0;

sc_executor_wasmtime::create_runtime_from_artifact_bytes::<HostFunctions>(
compiled_artifact_blob,
config,
)
}

pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> Semantics {
/// Takes the default config and overwrites any settings with existing executor parameters.
///
/// Returns the semantics as well as the stack limit (since we are guaranteed to have it).
pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> (Semantics, DeterministicStackLimit) {
let mut sem = DEFAULT_CONFIG.semantics.clone();
let mut stack_limit = sem
.deterministic_stack_limit
Expand All @@ -169,8 +172,8 @@ pub fn params_to_wasmtime_semantics(par: &ExecutorParams) -> Semantics {
ExecutorParam::PvfExecTimeout(_, _) => (), /* Not used here */
}
}
sem.deterministic_stack_limit = Some(stack_limit);
sem
sem.deterministic_stack_limit = Some(stack_limit.clone());
(sem, stack_limit)
mrcnski marked this conversation as resolved.
Show resolved Hide resolved
}

/// Runs the prevalidation on the given code. Returns a [`RuntimeBlob`] if it succeeds.
Expand All @@ -187,7 +190,7 @@ pub fn prepare(
blob: RuntimeBlob,
executor_params: &ExecutorParams,
) -> Result<Vec<u8>, sc_executor_common::error::WasmError> {
let semantics = params_to_wasmtime_semantics(executor_params);
let (semantics, _) = params_to_wasmtime_semantics(executor_params);
sc_executor_wasmtime::prepare_runtime_artifact(blob, &semantics)
}

Expand Down
2 changes: 2 additions & 0 deletions polkadot/node/core/pvf/common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub struct SecurityStatus {
pub can_enable_seccomp: bool,
/// Whether we are able to unshare the user namespace and change the filesystem root.
pub can_unshare_user_namespace_and_change_root: bool,
/// Whether we are able to call `clone` with all sandboxing flags.
pub can_do_secure_clone: bool,
}

/// A handshake with information for the worker.
Expand Down
160 changes: 128 additions & 32 deletions polkadot/node/core/pvf/common/src/worker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@

pub mod security;

use crate::{framed_recv_blocking, WorkerHandshake, LOG_TARGET};
use crate::{framed_recv_blocking, SecurityStatus, WorkerHandshake, LOG_TARGET};
use cpu_time::ProcessTime;
use futures::never::Never;
use parity_scale_codec::Decode;
use std::{
any::Any,
fmt, io,
os::unix::net::UnixStream,
fmt::{self},
fs::File,
io::{self, Read, Write},
os::{
fd::{AsRawFd, FromRawFd, RawFd},
unix::net::UnixStream,
},
path::PathBuf,
sync::mpsc::{Receiver, RecvTimeoutError},
time::Duration,
Expand Down Expand Up @@ -78,7 +83,7 @@ macro_rules! decl_worker_main {

"--check-can-enable-landlock" => {
#[cfg(target_os = "linux")]
let status = if let Err(err) = security::landlock::check_is_fully_enabled() {
let status = if let Err(err) = security::landlock::check_can_fully_enable() {
// Write the error to stderr, log it on the host-side.
eprintln!("{}", err);
-1
Expand All @@ -91,7 +96,7 @@ macro_rules! decl_worker_main {
},
"--check-can-enable-seccomp" => {
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
let status = if let Err(err) = security::seccomp::check_is_fully_enabled() {
let status = if let Err(err) = security::seccomp::check_can_fully_enable() {
// Write the error to stderr, log it on the host-side.
eprintln!("{}", err);
-1
Expand All @@ -107,7 +112,7 @@ macro_rules! decl_worker_main {
let cache_path_tempdir = std::path::Path::new(&args[2]);
#[cfg(target_os = "linux")]
let status = if let Err(err) =
security::change_root::check_is_fully_enabled(&cache_path_tempdir)
security::change_root::check_can_fully_enable(&cache_path_tempdir)
{
// Write the error to stderr, log it on the host-side.
eprintln!("{}", err);
Expand All @@ -119,6 +124,21 @@ macro_rules! decl_worker_main {
let status = -1;
std::process::exit(status)
},
"--check-can-do-secure-clone" => {
#[cfg(target_os = "linux")]
// SAFETY: new process is spawned within a single threaded process. This
// invariant is enforced by tests.
let status = if let Err(err) = unsafe { security::clone::check_can_fully_clone() } {
// Write the error to stderr, log it on the host-side.
eprintln!("{}", err);
-1
} else {
0
};
#[cfg(not(target_os = "linux"))]
let status = -1;
std::process::exit(status)
},

"test-sleep" => {
std::thread::sleep(std::time::Duration::from_secs(5));
Expand Down Expand Up @@ -171,6 +191,84 @@ macro_rules! decl_worker_main {
};
}

//taken from the os_pipe crate. Copied here to reduce one dependency and
// because its type-safe abstractions do not play well with nix's clone
#[cfg(not(target_os = "macos"))]
pub fn pipe2_cloexec() -> io::Result<(libc::c_int, libc::c_int)> {
let mut fds: [libc::c_int; 2] = [0; 2];
let res = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
if res != 0 {
return Err(io::Error::last_os_error())
}
Ok((fds[0], fds[1]))
}

#[cfg(target_os = "macos")]
pub fn pipe2_cloexec() -> io::Result<(libc::c_int, libc::c_int)> {
let mut fds: [libc::c_int; 2] = [0; 2];
let res = unsafe { libc::pipe(fds.as_mut_ptr()) };
if res != 0 {
return Err(io::Error::last_os_error())
}
let res = unsafe { libc::fcntl(fds[0], libc::F_SETFD, libc::FD_CLOEXEC) };
if res != 0 {
return Err(io::Error::last_os_error())
}
let res = unsafe { libc::fcntl(fds[1], libc::F_SETFD, libc::FD_CLOEXEC) };
if res != 0 {
return Err(io::Error::last_os_error())
}
Ok((fds[0], fds[1]))
}

/// A wrapper around a file descriptor used to encapsulate and restrict
/// functionality for pipe operations.
pub struct PipeFd {
mrcnski marked this conversation as resolved.
Show resolved Hide resolved
file: File,
}
mrcnski marked this conversation as resolved.
Show resolved Hide resolved

impl AsRawFd for PipeFd {
/// Returns the raw file descriptor associated with this `PipeFd`
fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd()
}
}

impl FromRawFd for PipeFd {
/// Creates a new `PipeFd` instance from a raw file descriptor.
///
/// # Safety
///
/// The fd passed in must be an owned file descriptor; in particular, it must be open.
unsafe fn from_raw_fd(fd: RawFd) -> Self {
PipeFd { file: File::from_raw_fd(fd) }
}
}

impl Read for PipeFd {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file.read(buf)
}

fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
self.file.read_to_end(buf)
}
}

impl Write for PipeFd {
mrcnski marked this conversation as resolved.
Show resolved Hide resolved
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file.write(buf)
}

fn flush(&mut self) -> io::Result<()> {
self.file.flush()
}

fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.file.write_all(buf)
}
}

/// Some allowed overhead that we account for in the "CPU time monitor" thread's sleeps, on the
/// child process.
pub const JOB_TIMEOUT_OVERHEAD: Duration = Duration::from_millis(50);
Expand All @@ -192,14 +290,12 @@ impl fmt::Display for WorkerKind {
}
}

// Some fields are only used for logging, and dead-code analysis ignores Debug.
#[allow(dead_code)]
#[derive(Debug)]
pub struct WorkerInfo {
pid: u32,
kind: WorkerKind,
version: Option<String>,
worker_dir_path: PathBuf,
pub pid: u32,
pub kind: WorkerKind,
pub version: Option<String>,
pub worker_dir_path: PathBuf,
}

// NOTE: The worker version must be passed in so that we accurately get the version of the worker,
Expand All @@ -218,7 +314,7 @@ pub fn run_worker<F>(
worker_version: Option<&str>,
mut event_loop: F,
) where
F: FnMut(UnixStream, PathBuf) -> io::Result<Never>,
F: FnMut(UnixStream, &WorkerInfo, SecurityStatus) -> io::Result<Never>,
{
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut worker_info = WorkerInfo {
Expand Down Expand Up @@ -250,11 +346,8 @@ pub fn run_worker<F>(
}

// Make sure that we can read the worker dir path, and log its contents.
let entries = || -> Result<Vec<_>, io::Error> {
std::fs::read_dir(&worker_info.worker_dir_path)?
.map(|res| res.map(|e| e.file_name()))
.collect()
}();
let entries: io::Result<Vec<_>> = std::fs::read_dir(&worker_info.worker_dir_path)
.and_then(|d| d.map(|res| res.map(|e| e.file_name())).collect());
match entries {
Ok(entries) =>
gum::trace!(target: LOG_TARGET, ?worker_info, "content of worker dir: {:?}", entries),
Expand Down Expand Up @@ -284,6 +377,22 @@ pub fn run_worker<F>(
{
gum::trace!(target: LOG_TARGET, ?security_status, "Enabling security features");

// First, make sure env vars were cleared, to match the environment we perform the checks
// within. (In theory, running checks with different env vars could result in different
// outcomes of the checks.)
if !security::check_env_vars_were_cleared(&worker_info) {
let err = "not all env vars were cleared when spawning the process";
gum::error!(
target: LOG_TARGET,
?worker_info,
"{}",
err
);
if security_status.secure_validator_mode {
worker_shutdown(worker_info, err);
}
}

// Call based on whether we can change root. Error out if it should work but fails.
//
// NOTE: This should not be called in a multi-threaded context (i.e. inside the tokio
Expand Down Expand Up @@ -337,23 +446,10 @@ pub fn run_worker<F>(
}
}
}

if !security::check_env_vars_were_cleared(&worker_info) {
let err = "not all env vars were cleared when spawning the process";
gum::error!(
target: LOG_TARGET,
?worker_info,
"{}",
err
);
if security_status.secure_validator_mode {
worker_shutdown(worker_info, err);
}
}
}

// Run the main worker loop.
let err = event_loop(stream, worker_info.worker_dir_path.clone())
let err = event_loop(stream, &worker_info, security_status)
// It's never `Ok` because it's `Ok(Never)`.
.unwrap_err();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ pub fn enable_for_worker(worker_info: &WorkerInfo) -> Result<()> {
///
/// NOTE: This should not be called in a multi-threaded context. `unshare(2)`:
/// "CLONE_NEWUSER requires that the calling process is not threaded."
#[cfg(target_os = "linux")]
pub fn check_is_fully_enabled(tempdir: &Path) -> Result<()> {
pub fn check_can_fully_enable(tempdir: &Path) -> Result<()> {
let worker_dir_path = tempdir.to_owned();
try_restrict(&WorkerInfo {
pid: std::process::id(),
Expand All @@ -69,7 +68,6 @@ pub fn check_is_fully_enabled(tempdir: &Path) -> Result<()> {
///
/// NOTE: This should not be called in a multi-threaded context. `unshare(2)`:
/// "CLONE_NEWUSER requires that the calling process is not threaded."
#[cfg(target_os = "linux")]
fn try_restrict(worker_info: &WorkerInfo) -> Result<()> {
// TODO: Remove this once this is stable: https://github.com/rust-lang/rust/issues/105723
macro_rules! cstr_ptr {
Expand All @@ -78,12 +76,6 @@ fn try_restrict(worker_info: &WorkerInfo) -> Result<()> {
};
}

gum::trace!(
target: LOG_TARGET,
?worker_info,
"unsharing the user namespace and calling pivot_root",
);

let worker_dir_path_c = CString::new(worker_info.worker_dir_path.as_os_str().as_bytes())
.expect("on unix; the path will never contain 0 bytes; qed");

Expand Down
Loading