Skip to content
This repository was archived by the owner on Feb 4, 2025. It is now read-only.

Commit 03889ce

Browse files
vlaciqkaiser
andcommitted
feat: add landlock based access restriction functionality
Landlock is a kernel API for unprivileged access control. We take advantage of it to limit where unblob can write to and read from on the filesystem. This is a Linux-only feature that won't be enabled on OSX. For more information, see https://docs.kernel.org/userspace-api/landlock.html We use Landlock ABI version 2 since it introduced the LANDLOCK_ACCESS_FS_REFER permission that's required to create hardlinks. Co-authored-by: Quentin Kaiser <quentin.kaiser@onekey.com>
1 parent 0c205be commit 03889ce

File tree

11 files changed

+322
-3
lines changed

11 files changed

+322
-3
lines changed

Cargo.lock

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ crate-type = [
1212
]
1313

1414
[dependencies]
15+
log = "0.4.18"
1516
pyo3 = "0.18.3"
17+
pyo3-log = "0.8.1"
18+
19+
[target.'cfg(target_os = "linux")'.dependencies]
20+
landlock = "0.2.0"
1621

1722
[dev-dependencies]
1823
approx = "0.5.0"

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ ignore = [
9393
"D203", # one-blank-line-before-class: D211 (no-blank-line-before-class) is used instead
9494
"D213", # multi-line-summary-second-line: D212 (multi-line-summary-first-line) is used instead
9595
"E501", # line-too-long: Let black handle line length violations
96+
"UP007", # non-pep604-annotation: Python 3.8 support needs legacy annotations
9697
]
9798

9899
[tool.ruff.per-file-ignores]

python/unblob_native/_native/__init__.pyi

Lines changed: 0 additions & 3 deletions
This file was deleted.
File renamed without changes.

python/unblob_native/sandbox.pyi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import os
2+
import typing
3+
4+
import typing_extensions
5+
6+
_Path: typing_extensions.TypeAlias = typing.Union[os.PathLike, str]
7+
8+
class AccessFS:
9+
@staticmethod
10+
def read(access_dir: _Path) -> AccessFS: ...
11+
@staticmethod
12+
def read_write(access_dir: _Path) -> AccessFS: ...
13+
@staticmethod
14+
def make_reg(access_dir: _Path) -> AccessFS: ...
15+
@staticmethod
16+
def make_dir(access_dir: _Path) -> AccessFS: ...
17+
18+
def restrict_access(*args: AccessFS) -> None: ...

src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
pub mod math_tools;
2+
pub mod sandbox;
23

34
use pyo3::prelude::*;
45

56
/// Performance-critical functionality
67
#[pymodule]
78
fn _native(py: Python, m: &PyModule) -> PyResult<()> {
89
math_tools::init_module(py, m)?;
10+
sandbox::init_module(py, m)?;
11+
12+
pyo3_log::init();
913

1014
Ok(())
1115
}

src/sandbox/linux.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use landlock::{
2+
path_beneath_rules, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, ABI,
3+
};
4+
use log;
5+
6+
use std::path::Path;
7+
8+
use crate::sandbox::AccessFS;
9+
10+
impl AccessFS {
11+
fn read(&self) -> Option<&Path> {
12+
if let Self::Read(path) = self {
13+
Some(path)
14+
} else {
15+
None
16+
}
17+
}
18+
19+
fn read_write(&self) -> Option<&Path> {
20+
if let Self::ReadWrite(path) = self {
21+
Some(path)
22+
} else {
23+
None
24+
}
25+
}
26+
27+
fn make_reg(&self) -> Option<&Path> {
28+
if let Self::MakeReg(path) = self {
29+
Some(path)
30+
} else {
31+
None
32+
}
33+
}
34+
35+
fn make_dir(&self) -> Option<&Path> {
36+
if let Self::MakeDir(path) = self {
37+
Some(path)
38+
} else {
39+
None
40+
}
41+
}
42+
}
43+
44+
pub fn restrict_access(access_rules: &[AccessFS]) -> Result<(), Box<dyn std::error::Error>> {
45+
let abi = ABI::V2;
46+
47+
let read_only: Vec<&Path> = access_rules.iter().filter_map(AccessFS::read).collect();
48+
49+
let read_write: Vec<&Path> = access_rules
50+
.iter()
51+
.filter_map(AccessFS::read_write)
52+
.collect();
53+
54+
let create_file: Vec<&Path> = access_rules.iter().filter_map(AccessFS::make_reg).collect();
55+
56+
let create_directory: Vec<&Path> = access_rules.iter().filter_map(AccessFS::make_dir).collect();
57+
58+
let status = Ruleset::new()
59+
.handle_access(AccessFs::from_all(abi))?
60+
.create()?
61+
.add_rules(path_beneath_rules(read_write, AccessFs::from_all(abi)))?
62+
.add_rules(path_beneath_rules(create_file, AccessFs::MakeReg))?
63+
.add_rules(path_beneath_rules(create_directory, AccessFs::MakeDir))?
64+
.add_rules(path_beneath_rules(read_only, AccessFs::from_read(abi)))?
65+
.restrict_self()?;
66+
67+
log::info!(
68+
"Activated FS access restrictions; rules={:?}, status={:?}",
69+
access_rules,
70+
status.ruleset
71+
);
72+
73+
Ok(())
74+
}

src/sandbox/mod.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#[cfg_attr(target_os = "linux", path = "linux.rs")]
2+
#[cfg_attr(not(target_os = "linux"), path = "unsupported.rs")]
3+
mod sandbox_impl;
4+
5+
use pyo3::{create_exception, exceptions::PyException, prelude::*, types::PyTuple};
6+
use std::path::PathBuf;
7+
8+
#[derive(Clone, Debug)]
9+
pub enum AccessFS {
10+
Read(PathBuf),
11+
ReadWrite(PathBuf),
12+
MakeReg(PathBuf),
13+
MakeDir(PathBuf),
14+
}
15+
16+
/// Enforces access restrictions
17+
#[pyfunction(name = "restrict_access", signature=(*rules))]
18+
fn py_restrict_access(rules: &PyTuple) -> PyResult<()> {
19+
sandbox_impl::restrict_access(
20+
&rules
21+
.iter()
22+
.map(|r| Ok(r.extract::<PyAccessFS>()?.access))
23+
.collect::<PyResult<Vec<_>>>()?,
24+
)
25+
.map_err(|err| SandboxError::new_err(err.to_string()))
26+
}
27+
28+
create_exception!(unblob_native.sandbox, SandboxError, PyException);
29+
30+
#[pyclass(name = "AccessFS", module = "unblob_native.sandbox")]
31+
#[derive(Clone)]
32+
struct PyAccessFS {
33+
access: AccessFS,
34+
}
35+
36+
impl PyAccessFS {
37+
fn new(access: AccessFS) -> Self {
38+
Self { access }
39+
}
40+
}
41+
42+
#[pymethods]
43+
impl PyAccessFS {
44+
#[staticmethod]
45+
fn read(dir: PathBuf) -> Self {
46+
Self::new(AccessFS::Read(dir))
47+
}
48+
49+
#[staticmethod]
50+
fn read_write(dir: PathBuf) -> Self {
51+
Self::new(AccessFS::ReadWrite(dir))
52+
}
53+
54+
#[staticmethod]
55+
fn make_reg(dir: PathBuf) -> Self {
56+
Self::new(AccessFS::MakeReg(dir))
57+
}
58+
59+
#[staticmethod]
60+
fn make_dir(dir: PathBuf) -> Self {
61+
Self::new(AccessFS::MakeDir(dir))
62+
}
63+
}
64+
65+
pub fn init_module(py: Python, root_module: &PyModule) -> PyResult<()> {
66+
let module = PyModule::new(py, "sandbox")?;
67+
module.add_function(wrap_pyfunction!(py_restrict_access, module)?)?;
68+
module.add_class::<PyAccessFS>()?;
69+
70+
root_module.add_submodule(module)?;
71+
py.import("sys")?
72+
.getattr("modules")?
73+
.set_item("unblob_native.sandbox", module)?;
74+
75+
Ok(())
76+
}

src/sandbox/unsupported.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use log;
2+
3+
use crate::sandbox::AccessFS;
4+
5+
pub fn restrict_access(_access_rules: &[AccessFS]) -> Result<(), Box<dyn std::error::Error>> {
6+
log::warn!("Sandboxing FS access is unavailable on this system");
7+
8+
Ok(())
9+
}

0 commit comments

Comments
 (0)