Compare commits

..

4 commits

Author SHA1 Message Date
atagen
93e0dfc34e make ldd / where features optional 2025-11-03 16:41:38 +11:00
atagen
e9fe5a3471 comment main rountine actions 2025-11-03 16:24:15 +11:00
atagen
39ecba0170 expand permissions system, allow escapes 2025-11-03 15:44:09 +11:00
atagen
36d52304cb ensure write is actually read inclusive 2025-11-02 23:44:55 +11:00
6 changed files with 64 additions and 174 deletions

View file

@ -2,27 +2,21 @@
name = "yoke"
version = "0.1.0"
authors = [ "atagen" ]
description = "CLI sandboxing tool (similar to landrun or bwrap)"
description = "CLI sandboxing tool similar to landrun or bwrap"
repository = "https://git.atagen.co/atagen/yoke"
license = "GPL-3.0-or-later"
edition = "2024"
[features]
default = [ "abi-6", "cli" ]
cli = ["dep:elb-dl", "dep:which"]
nix = []
abi-1 = []
abi-2 = []
abi-3 = []
abi-4 = []
abi-5 = []
abi-6 = []
default = []
ldd = ["dep:elb-dl"]
which = ["dep:which"]
[profile.release]
strip = true
opt-level = "s"
codegen-units = 1
lto = true
[dependencies]
anyhow = "1.0.100"

View file

@ -26,18 +26,26 @@
});
packages = forAllSystems (pkgs: {
default = self.packages.${pkgs.system}.yoke;
yoke = pkgs.rustPlatform.callPackage ./nix/package.nix {
features = [
"cli"
];
};
yoke-lite = pkgs.rustPlatform.callPackage ./nix/package.nix { };
yoke =
let
details = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package;
lib = pkgs.lib;
in
pkgs.rustPlatform.buildRustPackage (finalAttrs: {
pname = details.name;
inherit (details) version;
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
RUSTFLAGS = "-C prefer-dynamic=yes";
meta = {
description = details.description;
homepage = details.repository;
license = lib.licenses.gpl3Plus;
maintainers = [ lib.maintainers.atagen ];
mainProgram = details.name;
};
});
});
nixosModules.default =
{ pkgs, ... }:
{
imports = [ ./nix/module.nix ];
programs.yoke.package = self.packages.${pkgs.system}.yoke;
};
};
}

View file

@ -1,45 +0,0 @@
{
lib,
rustPlatform,
features ? [ "cli" ],
abi ? 6,
...
}:
let
details = (builtins.fromTOML (builtins.readFile ../Cargo.toml)).package;
in
rustPlatform.buildRustPackage (finalAttrs: {
pname = details.name;
inherit (details) version;
src = lib.cleanSourceWith {
src = lib.cleanSource ../.;
filter =
path_t: type:
let
path = toString path_t;
baseName = baseNameOf path;
parentDir = baseNameOf (dirOf path);
matchesSuffix = lib.any (suffix: lib.hasSuffix suffix baseName) [
".rs"
".toml"
];
isCargoFile = baseName == "Cargo.lock";
isCargoConf = parentDir == ".cargo" && baseName == "config";
in
type == "directory" || matchesSuffix || isCargoFile || isCargoConf;
};
cargoLock.lockFile = ../Cargo.lock;
buildNoDefaultFeatures = true;
buildFeatures = features ++ [
"nix"
"abi-${toString abi}"
];
meta = {
description = details.description;
homepage = details.repository;
license = lib.licenses.gpl3Plus;
maintainers = [ lib.maintainers.atagen ];
mainProgram = details.name;
};
})

View file

@ -7,26 +7,13 @@ use landlock::{
RulesetCreatedAttr, Scope, make_bitflags, path_beneath_rules,
};
#[cfg(feature = "cli")]
#[cfg(feature = "ldd")]
use elb_dl::{DependencyTree, DynamicLoader, glibc};
#[cfg(feature = "cli")]
#[cfg(feature = "ldd")]
use std::{collections::VecDeque, path::PathBuf, str::FromStr};
use std::{fs::File, io::Read, os::fd::FromRawFd};
fn main() -> Result<()> {
let args = std::env::args().skip(1);
let mut opts = parse::parse_args(args)?;
if opts.fd_args {
let mut fd = unsafe { File::from_raw_fd(3) };
let mut raw_args = String::new();
fd.read_to_string(&mut raw_args)?;
let args = raw_args
.split_ascii_whitespace()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let fd_opts = parse::parse_args(args.into_iter())?;
opts.merge(fd_opts);
}
let opts = parse::parse_args()?;
if opts.exec.is_empty() {
// print help
eprintln!(
@ -63,9 +50,6 @@ rules
--no-fs | -nf
--no-tcp | -nt
accept additional rules from fd 3:
--fd-args | -fd
access specifiers
------------
@ -93,26 +77,16 @@ examples
// set up scope of our intial ruleset
let mut preempt = Ruleset::default();
// TODO FIXME set up for lesser ABI versions
preempt = preempt.set_compatibility(landlock::CompatLevel::HardRequirement);
#[cfg(feature = "abi-2")]
let abi = ABI::V2;
#[cfg(feature = "abi-3")]
let abi = ABI::V3;
#[cfg(feature = "abi-4")]
let abi = ABI::V4;
#[cfg(feature = "abi-5")]
let abi = ABI::V5;
#[cfg(feature = "abi-6")]
let abi = ABI::V6;
// disallow signals to other processes
if !opts.signals && abi >= ABI::V6 {
if !opts.signals {
preempt = preempt.scope(Scope::Signal).context("scoping signals")?;
}
// disallow connections to unix domain sockets
if !opts.sockets && abi >= ABI::V6 {
if !opts.sockets {
preempt = preempt
.scope(Scope::AbstractUnixSocket)
.context("scoping sockets")?;
@ -121,14 +95,14 @@ examples
// lock down fs access
preempt = if !opts.unsandbox.fs {
preempt
.handle_access(AccessFs::from_all(abi))
.handle_access(AccessFs::from_all(ABI::V6))
.context("handling fs access")?
} else {
preempt
};
// lock down tcp access
preempt = if !(opts.unsandbox.tcp || abi < ABI::V3) {
preempt = if !opts.unsandbox.tcp {
preempt
.handle_access(AccessNet::BindTcp)
.context("handling tcp bind access")?
@ -148,9 +122,8 @@ examples
access.insert(make_bitflags!(AccessFs::{ReadFile | ReadDir}));
}
if perms.write {
let mut flags = make_bitflags!(
AccessFs::{
WriteFile
access.insert(make_bitflags!(
AccessFs::{ WriteFile
| RemoveDir
| RemoveFile
| MakeChar
@ -160,33 +133,19 @@ examples
| MakeFifo
| MakeBlock
| MakeSym
| Refer
| Truncate
}
);
match abi {
ABI::V2 => {
flags.insert(AccessFs::Refer);
}
_ if abi >= ABI::V3 => {
flags.insert(AccessFs::Refer | AccessFs::Truncate);
}
_ => (),
};
access.insert(flags);
));
}
if perms.execute {
access.insert(AccessFs::Execute);
}
if perms.ioctl {
if abi >= ABI::V6 {
access.insert(AccessFs::IoctlDev);
} else {
return Err(anyhow!(
"ioctl is only available on Landlock ABI 6 or higher"
));
}
access.insert(AccessFs::IoctlDev);
}
if access == BitFlags::empty() {
return Err(anyhow!("invalid/empty filesystem permissions requested"));
return Err(anyhow!("invalid filesystem permissions requested"));
}
ruleset = ruleset
.add_rules(path_beneath_rules(paths, access))
@ -194,54 +153,44 @@ examples
}
// allow each tcp action specified, grouped by access specifier
if abi >= ABI::V3 {
for (dir, ports) in opts.tcp {
let mut access = BitFlags::empty();
if dir.inbound {
access.insert(make_bitflags!(AccessNet::BindTcp));
}
if dir.outbound {
access.insert(make_bitflags!(AccessNet::ConnectTcp))
}
for port in ports {
ruleset = ruleset
.add_rule(NetPort::new(port, access))
.context("adding tcp rule")?;
}
for (dir, ports) in opts.tcp {
let mut access = BitFlags::empty();
if dir.inbound {
access.insert(make_bitflags!(AccessNet::BindTcp));
}
if dir.outbound {
access.insert(make_bitflags!(AccessNet::ConnectTcp))
}
for port in ports {
ruleset = ruleset
.add_rule(NetPort::new(port, access))
.context("adding tcp rule")?;
}
} else if !opts.tcp.is_empty() {
return Err(anyhow!(
"tcp controls are only supported with Landlock ABI 3 or higher"
));
}
// locate our executable
#[cfg(feature = "cli")]
#[cfg(feature = "which")]
let fullpath = which::which(&opts.exec[0]).context("finding executable")?;
#[cfg(not(feature = "cli"))]
#[cfg(not(feature = "which"))]
let fullpath = &opts.exec[0];
// add executeable as read+execute
if !opts.unsandbox.fs {
ruleset = ruleset.add_rules(path_beneath_rules(
std::slice::from_ref(&fullpath),
AccessFs::from_read(abi),
AccessFs::from_read(ABI::V6),
))?;
}
// if requested, trace dependencies and add as read+execute
#[cfg(feature = "cli")]
#[cfg(feature = "ldd")]
if opts.ldd {
#[cfg(not(feature = "nix"))]
let loader = DynamicLoader::options()
.search_dirs(glibc::get_search_dirs(PathBuf::from_str("/")?)?)
.new_loader();
#[cfg(feature = "nix")]
let loader = DynamicLoader::options()
.search_dirs(glibc::get_hard_coded_search_dirs(None)?)
.search_dirs(glibc::get_search_dirs(
PathBuf::from_str("/").context("finding root")?,
)?)
.new_loader();
let mut tree = DependencyTree::new();
let mut queue = VecDeque::new();
queue.push_back(fullpath.clone());
@ -255,7 +204,7 @@ examples
acc.extend(deps);
acc
}),
AccessFs::from_read(abi),
AccessFs::from_read(ABI::V6),
))
.context("tracking dependencies")?;
}

View file

@ -94,7 +94,7 @@ fn tcp_parse(pairs: &[String]) -> Result<HashMap<Direction, Vec<u16>>> {
Ok(rules)
}
pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
pub fn parse_args() -> Result<Yoke> {
let mut yoke = Yoke::default();
let mut collector = Vec::new();
let mut cur_arg = Unset;
@ -109,7 +109,7 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
}
Ok(())
}
for mut arg in args {
for mut arg in std::env::args().skip(1) {
arg.make_ascii_lowercase();
match arg.as_str() {
"--fs" | "-f" => {
@ -146,7 +146,7 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
yoke.retain_env = true;
}
#[cfg(feature = "cli")]
#[cfg(feature = "ldd")]
"--ldd" | "-l" => {
collect_args(&mut yoke, &collector, &cur_arg)?;
collector.clear();
@ -166,19 +166,13 @@ pub fn parse_args(args: impl Iterator<Item = String>) -> Result<Yoke> {
cur_arg = Unset;
yoke.unsandbox.tcp = true;
}
"--fd-args" | "-fd" => {
collect_args(&mut yoke, &collector, &cur_arg)?;
collector.clear();
cur_arg = Unset;
yoke.fd_args = true;
}
"--" => {
collect_args(&mut yoke, &collector, &cur_arg)?;
collector.clear();
cur_arg = Exec;
}
_ if cur_arg != Unset => {
collector.push(arg.to_string());
collector.push(arg.clone());
}
a => {
return Err(anyhow!("invalid argument: {}", a));

View file

@ -1,4 +1,3 @@
use anyhow::Result;
use std::{collections::HashMap, path::PathBuf};
#[derive(Debug, Default)]
@ -17,7 +16,6 @@ pub struct Yoke {
pub sockets: bool,
pub unsandbox: Unsandbox,
pub ldd: bool,
pub fd_args: bool,
pub exec: Vec<String>,
}
@ -34,11 +32,3 @@ pub struct Direction {
pub inbound: bool,
pub outbound: bool,
}
impl Yoke {
pub fn merge(&mut self, other: Yoke) {
self.fs.extend(other.fs);
self.tcp.extend(other.tcp);
self.env.extend(other.env);
}
}