PVF worker: Add seccomp restrictions (restrict networking) (#2009)

This commit is contained in:
Marcin S
2023-10-31 11:08:08 +01:00
committed by GitHub
parent 2d9426f1cc
commit 9faea380dc
27 changed files with 1376 additions and 714 deletions
+10 -7
View File
@@ -28,7 +28,7 @@ async fn execute_good_block_on_parent() {
let block_data = BlockData { state: 0, add: 512 };
let host = TestHost::new();
let host = TestHost::new().await;
let ret = host
.validate_candidate(
@@ -56,7 +56,7 @@ async fn execute_good_chain_on_parent() {
let mut parent_hash = [0; 32];
let mut last_state = 0;
let host = TestHost::new();
let host = TestHost::new().await;
for (number, add) in (0..10).enumerate() {
let parent_head =
@@ -98,7 +98,7 @@ async fn execute_bad_block_on_parent() {
add: 256,
};
let host = TestHost::new();
let host = TestHost::new().await;
let _err = host
.validate_candidate(
@@ -117,7 +117,7 @@ async fn execute_bad_block_on_parent() {
#[tokio::test]
async fn stress_spawn() {
let host = std::sync::Arc::new(TestHost::new());
let host = std::sync::Arc::new(TestHost::new().await);
async fn execute(host: std::sync::Arc<TestHost>) {
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
@@ -149,9 +149,12 @@ async fn stress_spawn() {
// With one worker, run multiple execution jobs serially. They should not conflict.
#[tokio::test]
async fn execute_can_run_serially() {
let host = std::sync::Arc::new(TestHost::new_with_config(|cfg| {
cfg.execute_workers_max_num = 1;
}));
let host = std::sync::Arc::new(
TestHost::new_with_config(|cfg| {
cfg.execute_workers_max_num = 1;
})
.await,
);
async fn execute(host: std::sync::Arc<TestHost>) {
let parent_head = HeadData { number: 0, parent_hash: [0; 32], post_state: hash_state(0) };
+117 -11
View File
@@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Polkadot. If not, see <http://www.gnu.org/licenses/>.
#[cfg(feature = "ci-only-tests")]
use assert_matches::assert_matches;
use parity_scale_codec::Encode as _;
use polkadot_node_core_pvf::{
@@ -24,6 +23,8 @@ use polkadot_node_core_pvf::{
};
use polkadot_parachain_primitives::primitives::{BlockData, ValidationParams, ValidationResult};
use polkadot_primitives::ExecutorParams;
#[cfg(target_os = "linux")]
use rusty_fork::rusty_fork_test;
#[cfg(feature = "ci-only-tests")]
use polkadot_primitives::ExecutorParam;
@@ -43,11 +44,11 @@ struct TestHost {
}
impl TestHost {
fn new() -> Self {
Self::new_with_config(|_| ())
async fn new() -> Self {
Self::new_with_config(|_| ()).await
}
fn new_with_config<F>(f: F) -> Self
async fn new_with_config<F>(f: F) -> Self
where
F: FnOnce(&mut Config),
{
@@ -61,7 +62,7 @@ impl TestHost {
execute_worker_path,
);
f(&mut config);
let (host, task) = start(config, Metrics::default());
let (host, task) = start(config, Metrics::default()).await;
let _ = tokio::task::spawn(task);
Self { cache_dir, host: Mutex::new(host) }
}
@@ -127,7 +128,7 @@ impl TestHost {
#[tokio::test]
async fn terminates_on_timeout() {
let host = TestHost::new();
let host = TestHost::new().await;
let start = std::time::Instant::now();
let result = host
@@ -153,11 +154,113 @@ async fn terminates_on_timeout() {
assert!(duration < TEST_EXECUTION_TIMEOUT * JOB_TIMEOUT_WALL_CLOCK_FACTOR);
}
#[cfg(target_os = "linux")]
fn kill_by_sid_and_name(sid: i32, exe_name: &'static str) {
use procfs::process;
let all_processes: Vec<process::Process> = process::all_processes()
.expect("Can't read /proc")
.filter_map(|p| match p {
Ok(p) => Some(p), // happy path
Err(e) => match e {
// process vanished during iteration, ignore it
procfs::ProcError::NotFound(_) => None,
x => {
panic!("some unknown error: {}", x);
},
},
})
.collect();
for process in all_processes {
if process.stat().unwrap().session == sid &&
process.exe().unwrap().to_str().unwrap().contains(exe_name)
{
assert_eq!(unsafe { libc::kill(process.pid(), 9) }, 0);
}
}
}
// Run these tests in their own processes with rusty-fork. They work by each creating a new session,
// then killing the worker process that matches the session ID and expected worker name.
#[cfg(target_os = "linux")]
rusty_fork_test! {
// What happens when the prepare worker dies in the middle of a job?
#[test]
fn prepare_worker_killed_during_job() {
const PROCESS_NAME: &'static str = "polkadot-prepare-worker";
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let host = TestHost::new().await;
// Create a new session and get the session ID.
let sid = unsafe { libc::setsid() };
assert!(sid > 0);
let (result, _) = futures::join!(
// Choose a job that would normally take the entire timeout.
host.precheck_pvf(rococo_runtime::WASM_BINARY.unwrap(), Default::default()),
// Run a future that kills the job in the middle of the timeout.
async {
tokio::time::sleep(TEST_PREPARATION_TIMEOUT / 2).await;
kill_by_sid_and_name(sid, PROCESS_NAME);
}
);
assert_matches!(result, Err(PrepareError::IoErr(_)));
})
}
// What happens when the execute worker dies in the middle of a job?
#[test]
fn execute_worker_killed_during_job() {
const PROCESS_NAME: &'static str = "polkadot-execute-worker";
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let host = TestHost::new().await;
// Create a new session and get the session ID.
let sid = unsafe { libc::setsid() };
assert!(sid > 0);
// Prepare the artifact ahead of time.
let binary = halt::wasm_binary_unwrap();
host.precheck_pvf(binary, Default::default()).await.unwrap();
let (result, _) = futures::join!(
// Choose an job that would normally take the entire timeout.
host.validate_candidate(
binary,
ValidationParams {
block_data: BlockData(Vec::new()),
parent_head: Default::default(),
relay_parent_number: 1,
relay_parent_storage_root: Default::default(),
},
Default::default(),
),
// Run a future that kills the job in the middle of the timeout.
async {
tokio::time::sleep(TEST_EXECUTION_TIMEOUT / 2).await;
kill_by_sid_and_name(sid, PROCESS_NAME);
}
);
assert_matches!(
result,
Err(ValidationError::InvalidCandidate(InvalidCandidate::AmbiguousWorkerDeath))
);
})
}
}
#[cfg(feature = "ci-only-tests")]
#[tokio::test]
async fn ensure_parallel_execution() {
// Run some jobs that do not complete, thus timing out.
let host = TestHost::new();
let host = TestHost::new().await;
let execute_pvf_future_1 = host.validate_candidate(
halt::wasm_binary_unwrap(),
ValidationParams {
@@ -204,7 +307,8 @@ async fn ensure_parallel_execution() {
async fn execute_queue_doesnt_stall_if_workers_died() {
let host = TestHost::new_with_config(|cfg| {
cfg.execute_workers_max_num = 5;
});
})
.await;
// Here we spawn 8 validation jobs for the `halt` PVF and share those between 5 workers. The
// first five jobs should timeout and the workers killed. For the next 3 jobs a new batch of
@@ -241,7 +345,8 @@ async fn execute_queue_doesnt_stall_if_workers_died() {
async fn execute_queue_doesnt_stall_with_varying_executor_params() {
let host = TestHost::new_with_config(|cfg| {
cfg.execute_workers_max_num = 2;
});
})
.await;
let executor_params_1 = ExecutorParams::default();
let executor_params_2 = ExecutorParams::from(&[ExecutorParam::StackLogicalMax(1024)][..]);
@@ -289,7 +394,7 @@ async fn execute_queue_doesnt_stall_with_varying_executor_params() {
// Test that deleting a prepared artifact does not lead to a dispute when we try to execute it.
#[tokio::test]
async fn deleting_prepared_artifact_does_not_dispute() {
let host = TestHost::new();
let host = TestHost::new().await;
let cache_dir = host.cache_dir.path();
let _stats = host.precheck_pvf(halt::wasm_binary_unwrap(), Default::default()).await.unwrap();
@@ -334,7 +439,8 @@ async fn deleting_prepared_artifact_does_not_dispute() {
async fn prepare_can_run_serially() {
let host = TestHost::new_with_config(|cfg| {
cfg.prepare_workers_hard_max_num = 1;
});
})
.await;
let _stats = host
.precheck_pvf(::adder::wasm_binary_unwrap(), Default::default())