Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

PVF: add landlock sandboxing #7303

Merged
merged 19 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
20 changes: 8 additions & 12 deletions node/core/pvf/common/src/worker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ pub mod thread {
/// Runs a worker thread. Will first enable security features, and afterwards notify the threads waiting on the
/// condvar. Catches panics during execution and resumes the panics after triggering the condvar, so that the
/// waiting thread is notified on panics.
///
/// # Returns
///
/// Returns the thread's join handle. Calling `.join()` on it returns the result of executing
/// `f()`, as well as whether we were able to enable sandboxing.
pub fn spawn_worker_thread<F, R>(
name: &str,
f: F,
Expand All @@ -240,18 +245,9 @@ pub mod thread {
F: Send + 'static + panic::UnwindSafe,
R: Send + 'static,
{
thread::Builder::new().name(name.into()).spawn(move || {
cond_notify_on_done(
|| {
#[cfg(target_os = "linux")]
let _ = crate::worker::security::landlock::try_restrict_thread();

f()
},
cond,
outcome,
)
})
thread::Builder::new()
.name(name.into())
.spawn(move || cond_notify_on_done(f, cond, outcome))
}

/// Runs a worker thread with the given stack size. See [`spawn_worker_thread`].
Expand Down
49 changes: 38 additions & 11 deletions node/core/pvf/common/src/worker/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,28 @@
//!
//! This is needed because workers are used to compile and execute untrusted code (PVFs).

/// To what degree landlock is enabled. It's a separate struct from `RulesetStatus` because that is
/// only available on Linux, plus this has a nicer name.
pub enum LandlockStatus {
FullyEnforced,
PartiallyEnforced,
NotEnforced,
/// Thread panicked, we don't know what the status is.
Unavailable,
}

impl LandlockStatus {
#[cfg(target_os = "linux")]
pub fn from_ruleset_status(ruleset_status: ::landlock::RulesetStatus) -> Self {
use ::landlock::RulesetStatus::*;
match ruleset_status {
FullyEnforced => LandlockStatus::FullyEnforced,
PartiallyEnforced => LandlockStatus::PartiallyEnforced,
NotEnforced => LandlockStatus::NotEnforced,
}
}
}

/// The [landlock] docs say it best:
///
/// > "Landlock is a security feature available since Linux 5.13. The goal is to enable to restrict
Expand All @@ -30,10 +52,7 @@
/// [landlock]: https://docs.rs/landlock/latest/landlock/index.html
#[cfg(target_os = "linux")]
pub mod landlock {
// Export for checking the status.
pub use landlock::RulesetStatus;

use landlock::{Access, AccessFs, Ruleset, RulesetAttr, RulesetError, ABI};
use landlock::{Access, AccessFs, Ruleset, RulesetAttr, RulesetError, RulesetStatus, ABI};

/// Landlock ABI version. Use the latest version supported by our reference kernel version.
///
Expand Down Expand Up @@ -73,14 +92,22 @@ pub mod landlock {
}
}

/// Returns a single bool indicating whether the given landlock ABI is fully enabled on the
/// current Linux environment.
/// Basaed on the given `status`, returns a single bool indicating whether the given landlock
/// ABI is fully enabled on the current Linux environment.
///
/// NOTE: Secure validators must be *fully* enabled. See "Determinism" in [`LANDLOCK_ABI`].
pub fn status_is_fully_enabled(
status: &Result<RulesetStatus, Box<dyn std::error::Error>>,
) -> bool {
matches!(status, Ok(RulesetStatus::FullyEnforced))
}

/// Runs a check for landlock and returns a single bool indicating whether the given landlock
/// ABI is fully enabled on the current Linux environment.
///
/// NOTE: Secure validators *should* have this *fully* enabled for the reference ABI. Even having
/// this partially enabled (which landlock does in a best-effort capacity) may lead to
/// indeterminism. See "Determinism" under [`LANDLOCK_ABI`].
pub fn is_fully_enabled() -> bool {
matches!(get_status(), Ok(RulesetStatus::FullyEnforced))
/// NOTE: Secure validators must be *fully* enabled. See "Determinism" in [`LANDLOCK_ABI`].
pub fn check_is_fully_enabled() -> bool {
mrcnski marked this conversation as resolved.
Show resolved Hide resolved
status_is_fully_enabled(&get_status())
}

/// Tries to restrict the current thread with the following landlock access controls:
Expand Down
46 changes: 37 additions & 9 deletions node/core/pvf/execute-worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ use polkadot_node_core_pvf_common::{
execute::{Handshake, Response},
framed_recv, framed_send,
worker::{
bytes_to_path, cpu_time_monitor_loop, stringify_panic_payload,
bytes_to_path, cpu_time_monitor_loop,
security::LandlockStatus,
stringify_panic_payload,
thread::{self, WaitOutcome},
worker_event_loop,
},
Expand Down Expand Up @@ -136,11 +138,22 @@ pub fn worker_entrypoint(socket_path: &str, node_version: Option<&str>) {
let execute_thread = thread::spawn_worker_thread_with_stack_size(
"execute thread",
move || {
validate_using_artifact(
&compiled_artifact_blob,
&params,
executor_2,
cpu_time_start,
// Try to enable landlock.
#[cfg(target_os = "linux")]
let landlock_status = polkadot_node_core_pvf_common::worker::security::landlock::try_restrict_thread()
alexggh marked this conversation as resolved.
Show resolved Hide resolved
.map(LandlockStatus::from_ruleset_status)
.map_err(|e| e.to_string());
#[cfg(not(target_os = "linux"))]
let landlock_status: Result<LandlockStatus, String> = Ok(LandlockStatus::NotEnforced);

(
validate_using_artifact(
&compiled_artifact_blob,
&params,
executor_2,
cpu_time_start,
),
landlock_status,
)
},
Arc::clone(&condvar),
Expand All @@ -153,9 +166,24 @@ pub fn worker_entrypoint(socket_path: &str, node_version: Option<&str>) {
let response = match outcome {
WaitOutcome::Finished => {
let _ = cpu_time_monitor_tx.send(());
execute_thread
.join()
.unwrap_or_else(|e| Response::Panic(stringify_panic_payload(e)))
let (result, landlock_status) = execute_thread.join().unwrap_or_else(|e| {
(
Response::Panic(stringify_panic_payload(e)),
Ok(LandlockStatus::Unavailable),
)
});

// Log if landlock threw an error.
if let Err(err) = landlock_status {
gum::warn!(
target: LOG_TARGET,
%worker_pid,
"error enabling landlock: {}",
err
);
}

result
},
// If the CPU thread is not selected, we signal it to end, the join handle is
// dropped and the thread will finish in the background.
Expand Down
33 changes: 28 additions & 5 deletions node/core/pvf/prepare-worker/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ use polkadot_node_core_pvf_common::{
prepare::{MemoryStats, PrepareStats},
pvf::PvfPrepData,
worker::{
bytes_to_path, cpu_time_monitor_loop, stringify_panic_payload,
bytes_to_path, cpu_time_monitor_loop,
security::LandlockStatus,
stringify_panic_payload,
thread::{self, WaitOutcome},
worker_event_loop,
},
Expand Down Expand Up @@ -151,13 +153,21 @@ pub fn worker_entrypoint(socket_path: &str, node_version: Option<&str>) {
let prepare_thread = thread::spawn_worker_thread(
"prepare thread",
move || {
// Try to enable landlock.
#[cfg(target_os = "linux")]
let landlock_status = polkadot_node_core_pvf_common::worker::security::landlock::try_restrict_thread()
.map(LandlockStatus::from_ruleset_status)
.map_err(|e| e.to_string());
#[cfg(not(target_os = "linux"))]
let landlock_status: Result<LandlockStatus, String> = Ok(LandlockStatus::NotEnforced);

let result = prepare_artifact(pvf, cpu_time_start);

// Get the `ru_maxrss` stat. If supported, call getrusage for the thread.
#[cfg(target_os = "linux")]
let result = result.map(|(artifact, elapsed)| (artifact, elapsed, get_max_rss_thread()));

result
(result, landlock_status)
},
Arc::clone(&condvar),
WaitOutcome::Finished,
Expand All @@ -170,13 +180,16 @@ pub fn worker_entrypoint(socket_path: &str, node_version: Option<&str>) {
let _ = cpu_time_monitor_tx.send(());

match prepare_thread.join().unwrap_or_else(|err| {
Err(PrepareError::Panic(stringify_panic_payload(err)))
(
Err(PrepareError::Panic(stringify_panic_payload(err))),
Ok(LandlockStatus::Unavailable),
)
}) {
Err(err) => {
(Err(err), _) => {
// Serialized error will be written into the socket.
Err(err)
},
Ok(ok) => {
(Ok(ok), landlock_status) => {
#[cfg(not(target_os = "linux"))]
let (artifact, cpu_time_elapsed) = ok;
#[cfg(target_os = "linux")]
Expand All @@ -192,6 +205,16 @@ pub fn worker_entrypoint(socket_path: &str, node_version: Option<&str>) {
max_rss: extract_max_rss_stat(max_rss, worker_pid),
};

// Log if landlock threw an error.
if let Err(err) = landlock_status {
gum::warn!(
target: LOG_TARGET,
%worker_pid,
"error enabling landlock: {}",
err
);
}

// Write the serialized artifact into a temp file.
//
// PVF host only keeps artifacts statuses in its memory, successfully
Expand Down
2 changes: 1 addition & 1 deletion node/core/pvf/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -866,7 +866,7 @@ fn warn_if_no_landlock() {
{
use polkadot_node_core_pvf_common::worker::security::landlock;
let status = landlock::get_status();
if !matches!(status, Ok(landlock::RulesetStatus::FullyEnforced)) {
if !landlock::status_is_fully_enabled(&status) {
let abi = landlock::LANDLOCK_ABI as u8;
gum::warn!(
target: LOG_TARGET,
Expand Down