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

Commit 21db054

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 b61a213 commit 21db054

File tree

9 files changed

+315
-2
lines changed

9 files changed

+315
-2
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"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
from . import math_tools as math_tools
1+
from . import math_tools, sandbox
22

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

src/sandbox/mod.rs

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

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+
}

tests/test_sandbox.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import platform
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from unblob_native.sandbox import AccessFS, restrict_access # type: ignore
7+
8+
FILE_CONTENT = b"HELLO"
9+
10+
11+
@pytest.mark.skipif(platform.system() == "Linux", reason="Linux is supported.")
12+
def test_unsupported_platform():
13+
restrict_access(AccessFS.read("/"))
14+
15+
16+
@pytest.fixture(scope="session")
17+
def sandbox_path(tmp_path_factory: pytest.TempPathFactory) -> Path:
18+
sandbox_path = tmp_path_factory.mktemp("sandbox")
19+
20+
file_path = sandbox_path / "file.txt"
21+
dir_path = sandbox_path / "dir"
22+
link_path = sandbox_path / "link"
23+
24+
with file_path.open("wb") as f:
25+
assert f.write(FILE_CONTENT) == len(FILE_CONTENT)
26+
27+
dir_path.mkdir()
28+
link_path.symlink_to(file_path)
29+
30+
return sandbox_path
31+
32+
33+
@pytest.mark.skipif(
34+
platform.system() != "Linux" or platform.machine() != "x86_64",
35+
reason="Only supported on Linux x86-64.",
36+
)
37+
def test_read_sandboxing(sandbox_path: Path):
38+
restrict_access(
39+
AccessFS.read("/"), AccessFS.read(sandbox_path.resolve().as_posix())
40+
)
41+
42+
with pytest.raises(PermissionError):
43+
(sandbox_path / "some-dir").mkdir()
44+
45+
with pytest.raises(PermissionError):
46+
(sandbox_path / "some-file").touch()
47+
48+
with pytest.raises(PermissionError):
49+
(sandbox_path / "some-link").symlink_to("file.txt")
50+
51+
for path in sandbox_path.rglob("**/*"):
52+
if path.is_file() or path.is_symlink():
53+
with path.open("rb") as f:
54+
assert f.read() == FILE_CONTENT
55+
with pytest.raises(PermissionError):
56+
assert path.open("r+")
57+
with pytest.raises(PermissionError):
58+
assert path.unlink()
59+
elif path.is_dir():
60+
with pytest.raises(PermissionError):
61+
path.rmdir()

0 commit comments

Comments
 (0)