From df084730f52f6f8daf8d88fe701e18135dde852a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Vask=C3=B3?= <1771332+vlaci@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:23:05 +0200 Subject: [PATCH] feat: add landlock based access restriction functionality --- Cargo.lock | 78 +++++++++++++++++++++++++++++--- Cargo.toml | 5 +++ src/lib.rs | 4 ++ src/sandbox.rs | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 src/sandbox.rs diff --git a/Cargo.lock b/Cargo.lock index 8d36026..057a475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + [[package]] name = "atty" version = "0.2.14" @@ -169,6 +175,26 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "getrandom" version = "0.2.9" @@ -234,6 +260,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "landlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520baa32708c4e957d2fc3a186bc5bd8d26637c33137f399ddfc202adb240068" +dependencies = [ + "enumflags2", + "libc", + "thiserror", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -258,12 +295,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "memchr" @@ -414,6 +448,17 @@ dependencies = [ "pyo3-build-config", ] +[[package]] +name = "pyo3-log" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c8b57fe71fb5dcf38970ebedc2b1531cf1c14b1b9b4c560a182a57e115575c" +dependencies = [ + "arc-swap", + "log", + "pyo3", +] + [[package]] name = "pyo3-macros" version = "0.18.3" @@ -624,6 +669,26 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -640,7 +705,10 @@ version = "0.1.1" dependencies = [ "approx", "criterion", + "landlock", + "log", "pyo3", + "pyo3-log", "rand", ] diff --git a/Cargo.toml b/Cargo.toml index 0390c58..c82c9ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,12 @@ crate-type = [ ] [dependencies] +log = "0.4.18" pyo3 = "0.18.3" +pyo3-log = "0.8.1" + +[target.'cfg(target_os = "linux")'.dependencies] +landlock = "0.2.0" [dev-dependencies] approx = "0.5.0" diff --git a/src/lib.rs b/src/lib.rs index 14f0ef4..d9e70f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod math_tools; +pub mod sandbox; use pyo3::prelude::*; @@ -6,6 +7,9 @@ use pyo3::prelude::*; #[pymodule] fn _native(py: Python, m: &PyModule) -> PyResult<()> { math_tools::init_module(py, m)?; + sandbox::init_module(py, m)?; + + pyo3_log::init(); Ok(()) } diff --git a/src/sandbox.rs b/src/sandbox.rs new file mode 100644 index 0000000..c177455 --- /dev/null +++ b/src/sandbox.rs @@ -0,0 +1,120 @@ +#[cfg(target_os = "linux")] +use landlock::{ + path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, ABI, +}; +use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyTuple}; + +use log; + +#[derive(Clone, Debug)] +pub enum FSAccess { + Read(String), + ReadWrite(String), +} + +#[cfg(target_os = "linux")] +impl FSAccess { + fn read(&self) -> Option<&str> { + if let Self::Read(path) = self { + Some(path) + } else { + None + } + } + fn read_write(&self) -> Option<&str> { + if let Self::ReadWrite(path) = self { + Some(path) + } else { + None + } + } +} + +pub fn restrict_access(access_rules: &[FSAccess]) -> Result<(), Box> { + #[cfg(target_os = "linux")] + { + let abi = ABI::V1; + + let read_only: Vec<&str> = access_rules.iter().filter_map(FSAccess::read).collect(); + + let read_write: Vec<&str> = access_rules + .iter() + .filter_map(FSAccess::read_write) + .collect(); + + let status = Ruleset::new() + .handle_access(AccessFs::from_all(abi))? + .create()? + .add_rules(path_beneath_rules(read_only, AccessFs::from_read(abi)))? + .add_rules(path_beneath_rules(read_write, AccessFs::from_all(abi)))? + .restrict_self()?; + + log::info!( + "Activated FS access restrictions; rules={:?}, status={:?}", + access_rules, + status.ruleset + ); + } + + #[cfg(not(target_os = "linux"))] + log::warn!("Sandboxing FS access is unavailable on this system"); + + Ok(()) +} + +/// Enforces access restrictions +#[pyfunction(name = "restrict_access", signature=(*rules))] +fn py_restrict_access(_py: Python, rules: &PyTuple) -> PyResult<()> { + restrict_access( + &rules + .iter() + .map(|r| Ok(r.extract::()?.access)) + .collect::>>()?, + ) + .map_err(|err| SandboxError::new_err(err.to_string())) +} + +create_exception!(unblob_native.sandbox, SandboxError, PyException); + +#[pyclass(name = "FSAccess", module = "unblob_native.sandbox")] +#[derive(Clone)] +struct PyFSAccess { + access: FSAccess, +} + +impl PyFSAccess { + fn new(access: FSAccess) -> Self { + Self { access } + } +} + +#[pymethods] +impl PyFSAccess { + #[staticmethod] + fn read(dir: String) -> Self { + Self::new(FSAccess::Read(dir)) + } + + #[staticmethod] + fn read_write(dir: String) -> Self { + Self::new(FSAccess::ReadWrite(dir)) + } +} + +pub fn init_module(py: Python, root_module: &PyModule) -> PyResult<()> { + let module = PyModule::new(py, "sandbox")?; + module.add_function(wrap_pyfunction!(py_restrict_access, module)?)?; + module.add_class::()?; + + root_module.add_submodule(module)?; + + let sys = PyModule::import(py, "sys")?; + let modules = sys.getattr("modules")?; + modules.call_method( + "__setitem__", + ("unblob_native.sandbox".to_string(), module), + None, + )?; + + Ok(()) +}