Skip to content

Commit

Permalink
Add extension API for Linux sandbox control
Browse files Browse the repository at this point in the history
This patch adds two new fields to the `runSandboxed` extension API which
allow controlling the minimum required Linux landlock ABI version and
whether the command should still be executed when sandboxing is not
possible.
  • Loading branch information
cd-work committed Sep 15, 2023
1 parent 42c9dfa commit bd91108
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 51 deletions.
6 changes: 2 additions & 4 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ once_cell = "1.12.0"
deno_runtime = { version = "0.122.0" }
deno_core = { version = "0.199.0" }
deno_ast = { version = "0.27.2", features = ["transpiling"] }
birdcage = { version = "0.3.1" }
birdcage = { version = "0.3.1", path = "../../birdcage" }
libc = "0.2.135"
ignore = { version = "0.4.20", optional = true }
uuid = "1.4.1"
Expand Down
8 changes: 8 additions & 0 deletions cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,14 @@ pub fn add_subcommands(command: Command) -> Command {
.help("Do not add any default sandbox exceptions")
.long("strict")
.action(ArgAction::SetTrue),
Arg::new("best-effort")
.help("Skip sandboxing if it is not supported")
.long("best-effort")
.action(ArgAction::SetTrue),
Arg::new("min-landlock-abi")
.help("Minimum required landlock ABI")
.long("min-landlock-abi")
.value_name("ABI"),
Arg::new("cmd").help("Command to be executed").value_name("CMD").required(true),
Arg::new("args")
.help("Command arguments")
Expand Down
66 changes: 58 additions & 8 deletions cli/src/commands/extensions/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ struct ProcessException {
net: bool,
#[serde(default)]
strict: bool,
#[serde(default)]
ignore_sandboxing_errors: bool,
#[serde(default)]
minimum_landlock_version: u8,
}

#[cfg(unix)]
Expand Down Expand Up @@ -393,6 +397,41 @@ async fn parse_lockfile(
Ok(PackageLock { packages: parsed.packages, format: parsed.format })
}

/// Return error when trying to sandbox on Windows.
#[op]
#[cfg(not(unix))]
fn run_sandboxed(process: Process) -> Result<ProcessOutput> {
if process.skip_sandbox_if_unsupported {
// TODO: Remember this choice.
print_user_warning!("Extension sandboxing is not supported on this platform.");
let should_continue = Confirm::new()
.with_prompt("Do you wish to continue without sandboxing?")
.default(true)
.interact()?;

if !should_continue {
return Err(anyhow!("Unsandboxed extension execution has been denied."));
}

let output = Command::new(process.cmd)
.args(process.args)
.stdin(process.stdin)
.stdout(process.stdout)
.stderr(process.stderr)
.output()?;

Ok(ProcessOutput {
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
success: output.status.success(),
signal: output.status.signal(),
code: output.status.code(),
})
} else {
Err(anyhow!("Extension sandboxing is not supported on this platform"))
}
}

/// Run a command inside a sandbox.
///
/// This runs the supplied command in a sandbox, without restricting the
Expand All @@ -403,6 +442,8 @@ async fn parse_lockfile(
fn run_sandboxed(op_state: Rc<RefCell<OpState>>, process: Process) -> Result<ProcessOutput> {
let Process { cmd, args, stdin, stdout, stderr, exceptions } = process;

let min_landlock_abi = exceptions.minimum_landlock_version;
let best_effort = exceptions.ignore_sandboxing_errors;
let strict = exceptions.strict;
let state = ExtensionState::from(op_state);
let resolved_permissions =
Expand All @@ -413,7 +454,13 @@ fn run_sandboxed(op_state: Rc<RefCell<OpState>>, process: Process) -> Result<Pro
sandbox_args.push("sandbox".into());

// Create CLI arguments for `phylum sandbox` permission exceptions.
add_permission_args(&mut sandbox_args, &resolved_permissions, strict)?;
add_permission_args(
&mut sandbox_args,
&resolved_permissions,
strict,
best_effort,
min_landlock_abi,
)?;

// Add sandboxed command arguments.
sandbox_args.push("--".into());
Expand Down Expand Up @@ -450,11 +497,21 @@ fn add_permission_args<'a>(
sandbox_args: &mut Vec<Cow<'a, str>>,
permissions: &'a permissions::Permissions,
strict: bool,
best_effort: bool,
min_landlock_abi: u8,
) -> Result<()> {
if strict {
sandbox_args.push("--strict".into());
}

if best_effort {
sandbox_args.push("--best-effort".into());
}

if min_landlock_abi > 1 {
sandbox_args.push(format!("--min-landlock-abi={min_landlock_abi}").into())
}

// Add filesystem exception arguments.
let home_dir = dirs::home_dir()?;
for path in permissions.read.sandbox_paths().iter() {
Expand Down Expand Up @@ -494,13 +551,6 @@ fn add_permission_args<'a>(
Ok(())
}

/// Return error when trying to sandbox on Windows.
#[op]
#[cfg(not(unix))]
fn run_sandboxed(_process: Process) -> Result<ProcessOutput> {
Err(anyhow!("Extension sandboxing is not supported on this platform"))
}

#[op]
fn op_permissions(op_state: Rc<RefCell<OpState>>) -> permissions::Permissions {
let state = ExtensionState::from(op_state);
Expand Down
53 changes: 18 additions & 35 deletions cli/src/commands/extensions/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use std::{env, fs};
use anyhow::{anyhow, Result};
#[cfg(unix)]
use birdcage::error::{Error as SandboxError, Result as SandboxResult};
#[cfg(target_os = "linux")]
use birdcage::linux::LANDLOCK_ABI;
#[cfg(unix)]
use birdcage::{Birdcage, Exception, Sandbox};
use deno_runtime::permissions::PermissionsOptions;
Expand Down Expand Up @@ -188,40 +190,6 @@ impl Permissions {
&& self.unsandboxed_run.get().is_none()
}

/// Build a sandbox matching the requested permissions.
#[cfg(unix)]
pub fn build_sandbox(&self) -> Result<Birdcage> {
let mut birdcage = default_sandbox()?;

for path in self.read.sandbox_paths().iter().map(PathBuf::from) {
add_exception(&mut birdcage, Exception::Read(path))?;
}
for path in self.write.sandbox_paths().iter().map(PathBuf::from) {
add_exception(&mut birdcage, Exception::Write(path))?;
}
for path in self.run.sandbox_paths().iter() {
let absolute_path = resolve_bin_path(path);
add_exception(&mut birdcage, Exception::ExecuteAndRead(absolute_path))?;
}

if self.net.get().is_some() {
birdcage.add_exception(Exception::Networking)?;
}

let env_exceptions = match &self.env {
Permission::Boolean(true) => vec![Exception::FullEnvironment],
Permission::Boolean(false) => Vec::new(),
Permission::List(keys) => {
keys.iter().map(|key| Exception::Environment(key.clone())).collect()
},
};
for exception in env_exceptions {
add_exception(&mut birdcage, exception)?;
}

Ok(birdcage)
}

pub fn subset_of(&self, other: &Permissions) -> Result<Permissions> {
let err_ctx = |name: &'static str| move |e| anyhow!("Invalid {name} permissions: {}", e);

Expand Down Expand Up @@ -276,9 +244,24 @@ impl From<&Permissions> for PermissionsOptions {

/// Construct sandbox with a set of pre-defined acceptable exceptions.
#[cfg(unix)]
pub fn default_sandbox() -> SandboxResult<Birdcage> {
pub fn default_sandbox(strict: bool, min_landlock_abi: u8) -> SandboxResult<Birdcage> {
#[cfg(not(target_os = "linux"))]
let mut birdcage = Birdcage::new()?;

#[cfg(target_os = "linux")]
let abi = match min_landlock_abi {
3 => LANDLOCK_ABI::V3,
2 => LANDLOCK_ABI::V2,
_ => LANDLOCK_ABI::V1,
};
#[cfg(target_os = "linux")]
let mut birdcage = Birdcage::new_with_version(abi)?;

// Do not add any default exceptions for strict sandboxes.
if strict {
return Ok(birdcage);
}

// Permit read access to lib for dynamic linking.
add_exception(&mut birdcage, Exception::ExecuteAndRead("/usr/lib".into()))?;
add_exception(&mut birdcage, Exception::ExecuteAndRead("/usr/lib32".into()))?;
Expand Down
16 changes: 13 additions & 3 deletions cli/src/commands/sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use anyhow::{anyhow, Result};
use anyhow::{Context, Error};
#[cfg(target_os = "linux")]
use birdcage::error::Error as SandboxError;
use birdcage::{Birdcage, Exception, Sandbox};
#[cfg(not(target_os = "linux"))]
use birdcage::Birdcage;
use birdcage::{Exception, Sandbox};
use clap::ArgMatches;

use crate::commands::extensions::permissions;
Expand Down Expand Up @@ -48,13 +50,21 @@ pub async fn handle_sandbox(matches: &ArgMatches) -> CommandResult {
/// Lock down the current process.
#[cfg(unix)]
fn lock_process(matches: &ArgMatches) -> Result<()> {
let birdcage =
if matches.get_flag("strict") { Birdcage::new() } else { permissions::default_sandbox() };
let min_landlock_abi = matches.get_one("min-landlock-abi").map_or(1, |abi| *abi);
let best_effort = matches.get_flag("best-effort");
let strict = matches.get_flag("strict");

let birdcage = permissions::default_sandbox(strict, min_landlock_abi);

// Provide additional error context.
let mut birdcage = match birdcage {
Ok(birdcage) => birdcage,
#[cfg(target_os = "linux")]
Err(_) if best_effort => {
log::debug!("Landlock v{min_landlock_abi} is not supported, skipping sandbox");
return Ok(());
},
#[cfg(target_os = "linux")]
Err(err @ SandboxError::Ruleset(_)) => {
return Err(Error::from(err)).context("sandbox requires Linux kernel 5.13+");
},
Expand Down

0 comments on commit bd91108

Please sign in to comment.