Decompose oversized Rust files by responsibility
This commit is contained in:
parent
03bfe92ca0
commit
ee9201b057
1 changed files with 18648 additions and 18513 deletions
767
crates/lazaret-core/src/configuration/confined_io.rs
Normal file
767
crates/lazaret-core/src/configuration/confined_io.rs
Normal file
|
|
@ -0,0 +1,767 @@
|
|||
fn validate_host_base(path: &Path, name: &str) -> Result<(), ConfigurationError> {
|
||||
if !path.is_absolute()
|
||||
|| path.as_os_str().is_empty()
|
||||
|| path.as_os_str().as_bytes().contains(&0)
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidHostInput,
|
||||
format!("{name} must be a NUL-free absolute path"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn config_home(request: &ConfigurationLoadRequest) -> PathBuf {
|
||||
request
|
||||
.xdg_config_home
|
||||
.as_ref()
|
||||
.filter(|path| path.is_absolute() && !path.as_os_str().is_empty())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| request.home.join(".config"))
|
||||
}
|
||||
|
||||
fn data_home(request: &ConfigurationLoadRequest) -> PathBuf {
|
||||
request
|
||||
.xdg_data_home
|
||||
.as_ref()
|
||||
.filter(|path| path.is_absolute() && !path.as_os_str().is_empty())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| request.home.join(".local/share"))
|
||||
}
|
||||
|
||||
fn expand_host_path(
|
||||
declaration: &str,
|
||||
home: &Path,
|
||||
profile: Option<&Path>,
|
||||
) -> Result<PathBuf, ConfigurationError> {
|
||||
validate_declared_host_path(declaration, profile.is_some())?;
|
||||
if let Some(suffix) = declaration.strip_prefix("~/") {
|
||||
return Ok(home.join(suffix));
|
||||
}
|
||||
if let Some(suffix) = declaration.strip_prefix("{profile_dir}/") {
|
||||
return profile.map(|root| root.join(suffix)).ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"profile-relative path is invalid in this field",
|
||||
)
|
||||
});
|
||||
}
|
||||
Ok(PathBuf::from(declaration))
|
||||
}
|
||||
|
||||
fn read_stable_configuration(plan: &ConfinedPathPlan) -> Result<Vec<u8>, ConfigurationError> {
|
||||
let path_descriptor = plan.open_authority()?;
|
||||
let before = rustix::fs::fstat(&path_descriptor).map_err(io_error)?;
|
||||
validate_configuration_stat(&before)?;
|
||||
let proc_path = format!("/proc/self/fd/{}", path_descriptor.as_raw_fd());
|
||||
let read_descriptor = rustix::fs::open(
|
||||
proc_path,
|
||||
OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NONBLOCK,
|
||||
Mode::empty(),
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
let opened = rustix::fs::fstat(&read_descriptor).map_err(io_error)?;
|
||||
if !same_file_snapshot(&before, &opened) {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration reopen selected a different object",
|
||||
));
|
||||
}
|
||||
let size = usize::try_from(before.st_size).map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration size is negative or unsupported",
|
||||
)
|
||||
})?;
|
||||
if size > MAX_CONFIGURATION_BYTES {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration exceeds 1,048,576 bytes",
|
||||
));
|
||||
}
|
||||
let mut file = File::from(read_descriptor);
|
||||
let first = read_exact_snapshot(&mut file, size)?;
|
||||
file.seek(SeekFrom::Start(0)).map_err(io_error)?;
|
||||
let second = read_exact_snapshot(&mut file, size)?;
|
||||
let after = rustix::fs::fstat(&file).map_err(io_error)?;
|
||||
if first != second || !same_file_snapshot(&before, &after) {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration changed during its single load attempt",
|
||||
));
|
||||
}
|
||||
Ok(first)
|
||||
}
|
||||
|
||||
fn validate_configuration_stat(stat: &Stat) -> Result<(), ConfigurationError> {
|
||||
let uid = rustix::process::getuid().as_raw();
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
||||
|| stat.st_nlink != 1
|
||||
|| !matches!(stat.st_uid, 0) && stat.st_uid != uid
|
||||
|| stat.st_mode & 0o022 != 0
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration is not a safe regular file",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_exact_snapshot(file: &mut File, size: usize) -> Result<Vec<u8>, ConfigurationError> {
|
||||
let mut bytes = vec![0; size];
|
||||
file.read_exact(&mut bytes).map_err(io_error)?;
|
||||
let mut trailing = [0];
|
||||
match file.read(&mut trailing) {
|
||||
Ok(0) => Ok(bytes),
|
||||
Ok(_) => Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidConfigurationSource,
|
||||
"configuration grew during read",
|
||||
)),
|
||||
Err(error) => Err(io_error(error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn same_file_snapshot(left: &Stat, right: &Stat) -> bool {
|
||||
left.st_dev == right.st_dev
|
||||
&& left.st_ino == right.st_ino
|
||||
&& left.st_size == right.st_size
|
||||
&& left.st_ctime == right.st_ctime
|
||||
&& left.st_ctime_nsec == right.st_ctime_nsec
|
||||
}
|
||||
|
||||
fn confined_existing(
|
||||
canonical: &Path,
|
||||
expected: ConfinedPathType,
|
||||
executable: bool,
|
||||
) -> Result<ConfinedPathPlan, ConfigurationError> {
|
||||
let plan = ConfinedPathPlan::for_canonical_path(canonical, expected)?;
|
||||
let descriptor = plan.open_authority()?;
|
||||
let stat = rustix::fs::fstat(&descriptor).map_err(io_error)?;
|
||||
if executable && stat.st_mode & 0o111 == 0 {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"resolved source has the wrong type or mode",
|
||||
));
|
||||
}
|
||||
Ok(plan)
|
||||
}
|
||||
|
||||
fn open_absolute_directory(path: &Path) -> Result<OwnedFd, ConfigurationError> {
|
||||
openat2(
|
||||
rustix::fs::CWD,
|
||||
path,
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
ResolveFlags::NO_MAGICLINKS | ResolveFlags::NO_SYMLINKS,
|
||||
)
|
||||
.map_err(io_error)
|
||||
}
|
||||
|
||||
fn ensure_owned_directory(path: &Path, uid: u32) -> Result<PathBuf, ConfigurationError> {
|
||||
validate_host_base(path, "Lazaret root")?;
|
||||
if path.exists() {
|
||||
let canonical = std::fs::canonicalize(path).map_err(io_error)?;
|
||||
if canonical.as_os_str().as_bytes() != path.as_os_str().as_bytes() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"Lazaret root contains a symlink",
|
||||
));
|
||||
}
|
||||
validate_owned_directory(&open_absolute_directory(&canonical)?, uid)?;
|
||||
return Ok(canonical);
|
||||
}
|
||||
|
||||
let mut existing = path;
|
||||
let mut missing = Vec::new();
|
||||
while !existing.exists() {
|
||||
let name = existing.file_name().ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"cannot find an existing ancestor for Lazaret root",
|
||||
)
|
||||
})?;
|
||||
missing.push(name.to_os_string());
|
||||
existing = existing.parent().ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"cannot find an existing ancestor for Lazaret root",
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let existing_canonical = std::fs::canonicalize(existing).map_err(io_error)?;
|
||||
if existing_canonical.as_os_str().as_bytes() != existing.as_os_str().as_bytes() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"Lazaret root ambient ancestor contains a symlink",
|
||||
));
|
||||
}
|
||||
let mut current = open_absolute_directory(&existing_canonical)?;
|
||||
let mut canonical = existing_canonical;
|
||||
for component in missing.into_iter().rev() {
|
||||
match rustix::fs::mkdirat(¤t, &component, Mode::RWXU) {
|
||||
Ok(()) | Err(rustix::io::Errno::EXIST) => {}
|
||||
Err(error) => return Err(io_error(error)),
|
||||
}
|
||||
let next = openat2(
|
||||
¤t,
|
||||
&component,
|
||||
OFlags::PATH | OFlags::DIRECTORY | OFlags::CLOEXEC,
|
||||
Mode::empty(),
|
||||
ResolveFlags::BENEATH
|
||||
| ResolveFlags::NO_MAGICLINKS
|
||||
| ResolveFlags::NO_SYMLINKS
|
||||
| ResolveFlags::NO_XDEV,
|
||||
)
|
||||
.map_err(io_error)?;
|
||||
validate_owned_directory(&next, uid)?;
|
||||
canonical.push(&component);
|
||||
current = next;
|
||||
}
|
||||
Ok(canonical)
|
||||
}
|
||||
|
||||
fn validate_owned_directory(descriptor: &OwnedFd, uid: u32) -> Result<(), ConfigurationError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(io_error)?;
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_dir()
|
||||
|| stat.st_uid != uid
|
||||
|| stat.st_mode & 0o7777 != 0o700
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidPath,
|
||||
"Lazaret-owned directory is not invoking-user-owned mode 0700",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_persistent_destinations(
|
||||
destinations: &[String],
|
||||
protected: &BTreeSet<String>,
|
||||
) -> Result<Vec<String>, ConfigurationError> {
|
||||
require_pairwise_non_ancestral(
|
||||
destinations.iter().map(String::as_str),
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
)?;
|
||||
for destination in destinations {
|
||||
validate_sandbox_path(destination)?;
|
||||
require_clear_of_protected(destination, protected)?;
|
||||
}
|
||||
Ok(destinations.to_vec())
|
||||
}
|
||||
|
||||
fn require_clear_of_protected(
|
||||
destination: &str,
|
||||
protected: &BTreeSet<String>,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
if protected
|
||||
.iter()
|
||||
.any(|protected| sandbox_paths_intersect(destination, protected))
|
||||
{
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::PathConflict,
|
||||
"configured sandbox destination intersects a protected destination",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn require_strict_child_of_one(
|
||||
destination: &str,
|
||||
parents: &[String],
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let count = parents
|
||||
.iter()
|
||||
.filter(|parent| sandbox_is_strict_descendant(destination, parent))
|
||||
.count();
|
||||
if count == 1 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidVolume,
|
||||
"volume destination is not a strict child of exactly one persistent path",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn require_pairwise_non_ancestral<'a>(
|
||||
paths: impl IntoIterator<Item = &'a str>,
|
||||
kind: ConfigurationErrorKind,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let paths = paths.into_iter().collect::<Vec<_>>();
|
||||
for (index, left) in paths.iter().enumerate() {
|
||||
for right in &paths[index + 1..] {
|
||||
if sandbox_paths_intersect(left, right) {
|
||||
return Err(ConfigurationError::new(
|
||||
kind,
|
||||
"paths are duplicate or ancestral",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sandbox_paths_intersect(left: &str, right: &str) -> bool {
|
||||
left == right
|
||||
|| left
|
||||
.strip_prefix(right)
|
||||
.is_some_and(|suffix| suffix.starts_with('/'))
|
||||
|| right
|
||||
.strip_prefix(left)
|
||||
.is_some_and(|suffix| suffix.starts_with('/'))
|
||||
}
|
||||
|
||||
fn sandbox_is_strict_descendant(child: &str, parent: &str) -> bool {
|
||||
child
|
||||
.strip_prefix(parent)
|
||||
.is_some_and(|suffix| suffix.starts_with('/'))
|
||||
}
|
||||
|
||||
fn require_pairwise_disjoint(
|
||||
paths: &[&Path],
|
||||
authorities: &[&ConfinedPathPlan],
|
||||
kind: ConfigurationErrorKind,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
if paths.len() != authorities.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
kind,
|
||||
"host path and authority sets differ",
|
||||
));
|
||||
}
|
||||
for (index, left) in paths.iter().enumerate() {
|
||||
for (right, right_authority) in paths[index + 1..].iter().zip(&authorities[index + 1..]) {
|
||||
if paths_intersect(left, right) || authorities[index].aliases(right_authority) {
|
||||
return Err(ConfigurationError::new(
|
||||
kind,
|
||||
"host authority trees overlap",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn paths_intersect(left: &Path, right: &Path) -> bool {
|
||||
left == right || left.starts_with(right) || right.starts_with(left)
|
||||
}
|
||||
|
||||
fn is_strict_descendant(child: &Path, parent: &Path) -> bool {
|
||||
child != parent && child.starts_with(parent)
|
||||
}
|
||||
|
||||
fn resolve_network(
|
||||
configuration: &TrustedHostConfigurationV1,
|
||||
selected: &ProfileConfigurationV1,
|
||||
) -> Result<(Vec<Ipv4Prefix>, Vec<Ipv4Prefix>), ConfigurationError> {
|
||||
let mut allow = BTreeSet::new();
|
||||
let mut deny = ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"]
|
||||
.into_iter()
|
||||
.map(parse_ipv4_prefix)
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
for prefix in configuration
|
||||
.network()
|
||||
.default_allow
|
||||
.iter()
|
||||
.chain(&selected.allow_egress)
|
||||
{
|
||||
allow.insert(parse_ipv4_prefix(prefix)?);
|
||||
}
|
||||
for prefix in configuration
|
||||
.network()
|
||||
.default_deny
|
||||
.iter()
|
||||
.chain(&selected.deny_egress)
|
||||
{
|
||||
deny.insert(parse_ipv4_prefix(prefix)?);
|
||||
}
|
||||
if allow.iter().any(|prefix| deny.contains(prefix)) {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"equal normalized prefix is both allowed and denied",
|
||||
));
|
||||
}
|
||||
Ok((allow.into_iter().collect(), deny.into_iter().collect()))
|
||||
}
|
||||
|
||||
const MAX_READINESS_TARGETS: usize = 32;
|
||||
fn resolve_network_readiness(
|
||||
readiness: &NetworkReadinessConfigurationV1,
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
) -> Result<ResolvedNetworkReadinessV1, ConfigurationError> {
|
||||
if readiness.denied.is_empty() || readiness.permitted.is_empty() || readiness.dns.is_empty() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness requires denied, permitted, and DNS targets",
|
||||
));
|
||||
}
|
||||
if readiness.denied.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.permitted.len() > MAX_READINESS_TARGETS
|
||||
|| readiness.dns.len() > MAX_READINESS_TARGETS
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness target count exceeds 32",
|
||||
));
|
||||
}
|
||||
|
||||
let denied = resolve_tcp_readiness_targets(&readiness.denied, allow, deny, false)?;
|
||||
let permitted = resolve_tcp_readiness_targets(&readiness.permitted, allow, deny, true)?;
|
||||
|
||||
let dns = readiness
|
||||
.dns
|
||||
.iter()
|
||||
.map(|target| {
|
||||
Ok(ResolvedDnsProbeTargetV1 {
|
||||
resolver: parse_probe_address(&target.resolver)?,
|
||||
query: normalize_dns_query(&target.query)?,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, ConfigurationError>>()?;
|
||||
if dns.len() != readiness.dns.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate DNS targets",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ResolvedNetworkReadinessV1 {
|
||||
denied: denied.into_iter().collect(),
|
||||
permitted: permitted.into_iter().collect(),
|
||||
dns: dns.into_iter().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_tcp_readiness_targets(
|
||||
targets: &[NetworkProbeTargetConfigurationV1],
|
||||
allow: &[Ipv4Prefix],
|
||||
deny: &[Ipv4Prefix],
|
||||
expected_allowed: bool,
|
||||
) -> Result<BTreeSet<ResolvedNetworkProbeTargetV1>, ConfigurationError> {
|
||||
let resolved = targets
|
||||
.iter()
|
||||
.map(|target| {
|
||||
if target.port == 0 {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target has port zero",
|
||||
));
|
||||
}
|
||||
let address = parse_probe_address(&target.address)?;
|
||||
if is_effectively_allowed(address, allow, deny) != expected_allowed {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP target disagrees with effective profile policy",
|
||||
));
|
||||
}
|
||||
Ok(ResolvedNetworkProbeTargetV1 {
|
||||
address,
|
||||
port: target.port,
|
||||
})
|
||||
})
|
||||
.collect::<Result<BTreeSet<_>, _>>()?;
|
||||
if resolved.len() != targets.len() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"profile readiness contains duplicate TCP targets",
|
||||
));
|
||||
}
|
||||
if resolved
|
||||
.iter()
|
||||
.map(|target| target.address)
|
||||
.collect::<BTreeSet<_>>()
|
||||
.len()
|
||||
!= resolved.len()
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness TCP targets repeat an observation address",
|
||||
));
|
||||
}
|
||||
Ok(resolved)
|
||||
}
|
||||
|
||||
fn parse_probe_address(value: &str) -> Result<[u8; 4], ConfigurationError> {
|
||||
let address = value.parse::<std::net::Ipv4Addr>().map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not IPv4",
|
||||
)
|
||||
})?;
|
||||
if address.is_unspecified() || address.is_multicast() || address.is_broadcast() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"readiness target address is not unicast",
|
||||
));
|
||||
}
|
||||
Ok(address.octets())
|
||||
}
|
||||
|
||||
fn normalize_dns_query(value: &str) -> Result<String, ConfigurationError> {
|
||||
let query = value.strip_suffix('.').unwrap_or(value);
|
||||
if query.is_empty() || query.len() > 253 || !query.is_ascii() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
for label in query.split('.') {
|
||||
if label.is_empty()
|
||||
|| label.len() > 63
|
||||
|| label.starts_with('-')
|
||||
|| label.ends_with('-')
|
||||
|| !label
|
||||
.bytes()
|
||||
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"DNS readiness query name is invalid",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(query.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn is_effectively_allowed(address: [u8; 4], allow: &[Ipv4Prefix], deny: &[Ipv4Prefix]) -> bool {
|
||||
let best_allow = allow
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
let best_deny = deny
|
||||
.iter()
|
||||
.filter(|prefix| prefix_contains(**prefix, address))
|
||||
.map(|prefix| prefix.prefix_length)
|
||||
.max();
|
||||
match (best_allow, best_deny) {
|
||||
(Some(allowed), Some(denied)) => allowed > denied,
|
||||
(Some(_) | None, None) => true,
|
||||
(None, Some(_)) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn prefix_contains(prefix: Ipv4Prefix, address: [u8; 4]) -> bool {
|
||||
let mask = if prefix.prefix_length == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - prefix.prefix_length)
|
||||
};
|
||||
u32::from_be_bytes(address) & mask == u32::from_be_bytes(prefix.address)
|
||||
}
|
||||
|
||||
fn parse_ipv4_prefix(value: &str) -> Result<Ipv4Prefix, ConfigurationError> {
|
||||
let (address, prefix) = value.split_once('/').ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"IPv4 prefix has no prefix length",
|
||||
)
|
||||
})?;
|
||||
if prefix.contains('/') {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"IPv4 prefix contains multiple separators",
|
||||
));
|
||||
}
|
||||
let address = address.parse::<std::net::Ipv4Addr>().map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"network prefix is not IPv4",
|
||||
)
|
||||
})?;
|
||||
let prefix_length = prefix.parse::<u8>().map_err(|_| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"IPv4 prefix length is invalid",
|
||||
)
|
||||
})?;
|
||||
if prefix_length > 32 {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidNetwork,
|
||||
"IPv4 prefix length exceeds 32",
|
||||
));
|
||||
}
|
||||
let raw = u32::from(address);
|
||||
let mask = if prefix_length == 0 {
|
||||
0
|
||||
} else {
|
||||
u32::MAX << (32 - prefix_length)
|
||||
};
|
||||
Ok(Ipv4Prefix {
|
||||
address: (raw & mask).to_be_bytes(),
|
||||
prefix_length,
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_environment_references(
|
||||
selected: &ProfileConfigurationV1,
|
||||
) -> Result<Vec<EnvironmentReference>, ConfigurationError> {
|
||||
selected
|
||||
.env
|
||||
.iter()
|
||||
.map(|(sandbox_name, declaration)| {
|
||||
let host_name = declaration.strip_prefix("env:").ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidProfile,
|
||||
"environment value is not an env: reference",
|
||||
)
|
||||
})?;
|
||||
validate_environment_name(sandbox_name)?;
|
||||
validate_environment_name(host_name)?;
|
||||
Ok(EnvironmentReference {
|
||||
sandbox_name: sandbox_name.clone(),
|
||||
host_name: host_name.to_owned(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resolve_environment_values(
|
||||
references: &[EnvironmentReference],
|
||||
environment: &BTreeMap<String, OsString>,
|
||||
) -> Result<BTreeMap<String, EnvironmentValue>, ConfigurationError> {
|
||||
references
|
||||
.iter()
|
||||
.map(|reference| {
|
||||
let value = environment.get(&reference.host_name).ok_or_else(|| {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::MissingEnvironment,
|
||||
format!("{} is unset", reference.host_name),
|
||||
)
|
||||
})?;
|
||||
if value.as_bytes().contains(&0) || value.to_str().is_none() {
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::MissingEnvironment,
|
||||
format!("{} is not NUL-free UTF-8", reference.host_name),
|
||||
));
|
||||
}
|
||||
Ok((
|
||||
reference.sandbox_name.clone(),
|
||||
EnvironmentValue(value.clone()),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn validate_executable(descriptor: &OwnedFd, uid: u32) -> Result<(), ConfigurationError> {
|
||||
let stat = rustix::fs::fstat(descriptor).map_err(io_error)?;
|
||||
let executable_by_invoking_user = if stat.st_uid == uid {
|
||||
stat.st_mode & 0o100 != 0
|
||||
} else {
|
||||
let primary_gid = rustix::process::getgid().as_raw();
|
||||
let supplementary_gids = rustix::process::getgroups().map_err(io_error)?;
|
||||
stat.st_mode & 0o001 != 0
|
||||
|| stat.st_mode & 0o010 != 0
|
||||
&& (stat.st_gid == primary_gid
|
||||
|| supplementary_gids
|
||||
.iter()
|
||||
.any(|group| group.as_raw() == stat.st_gid))
|
||||
};
|
||||
if !FileType::from_raw_mode(stat.st_mode).is_file()
|
||||
|| stat.st_nlink != 1
|
||||
|| !matches!(stat.st_uid, 0) && stat.st_uid != uid
|
||||
|| stat.st_mode & 0o022 != 0
|
||||
|| !executable_by_invoking_user
|
||||
{
|
||||
return Err(ConfigurationError::new(
|
||||
ConfigurationErrorKind::InvalidAutoApproval,
|
||||
"executable is not a safe single-link regular executable",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn encode_volume(volume: &ResolvedVolume) -> Result<Vec<u8>, ConfigurationError> {
|
||||
let mut encoded = Vec::new();
|
||||
push_bytes(&mut encoded, volume.canonical_host_source.as_bytes())?;
|
||||
push_bytes(&mut encoded, volume.container_path.as_bytes())?;
|
||||
encoded.push(match volume.kind {
|
||||
VolumeKind::File => 0,
|
||||
VolumeKind::Directory => 1,
|
||||
});
|
||||
encoded.push(match volume.access {
|
||||
VolumeAccess::ReadOnly => 0,
|
||||
VolumeAccess::ReadWrite => 1,
|
||||
});
|
||||
push_bool(&mut encoded, volume.create);
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn encode_auto_approval(rule: &ResolvedAutoApprovalRule) -> Result<Vec<u8>, ConfigurationError> {
|
||||
let mut encoded = Vec::new();
|
||||
push_bytes(&mut encoded, rule.canonical_executable.as_bytes())?;
|
||||
push_declared_list(
|
||||
&mut encoded,
|
||||
rule.args.iter().map(|argument| {
|
||||
let mut item = Vec::new();
|
||||
push_bytes(&mut item, argument.as_bytes())?;
|
||||
Ok(item)
|
||||
}),
|
||||
)?;
|
||||
push_bool(&mut encoded, rule.allow_extra_args);
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn push_bytes(target: &mut Vec<u8>, bytes: &[u8]) -> Result<(), ConfigurationError> {
|
||||
let length = u32::try_from(bytes.len()).map_err(|_| fingerprint_overflow())?;
|
||||
target.extend_from_slice(&length.to_be_bytes());
|
||||
target.extend_from_slice(bytes);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_bool(target: &mut Vec<u8>, value: bool) {
|
||||
target.push(u8::from(value));
|
||||
}
|
||||
|
||||
fn push_sorted_list(
|
||||
target: &mut Vec<u8>,
|
||||
items: impl IntoIterator<Item = Result<Vec<u8>, ConfigurationError>>,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let mut items = items.into_iter().collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_unstable();
|
||||
push_framed_items(target, &items)
|
||||
}
|
||||
|
||||
fn push_declared_list(
|
||||
target: &mut Vec<u8>,
|
||||
items: impl IntoIterator<Item = Result<Vec<u8>, ConfigurationError>>,
|
||||
) -> Result<(), ConfigurationError> {
|
||||
let items = items.into_iter().collect::<Result<Vec<_>, _>>()?;
|
||||
push_framed_items(target, &items)
|
||||
}
|
||||
|
||||
fn push_framed_items(target: &mut Vec<u8>, items: &[Vec<u8>]) -> Result<(), ConfigurationError> {
|
||||
let count = u32::try_from(items.len()).map_err(|_| fingerprint_overflow())?;
|
||||
target.extend_from_slice(&count.to_be_bytes());
|
||||
for item in items {
|
||||
push_bytes(target, item)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fingerprint_overflow() -> ConfigurationError {
|
||||
ConfigurationError::new(
|
||||
ConfigurationErrorKind::FingerprintOverflow,
|
||||
"field or collection does not fit v1 u32 framing",
|
||||
)
|
||||
}
|
||||
|
||||
fn host_path(path: &Path) -> HostPath {
|
||||
HostPath::new(path.as_os_str().as_bytes().to_vec())
|
||||
}
|
||||
|
||||
fn hex_lower(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
output.push(char::from(HEX[usize::from(byte >> 4)]));
|
||||
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn io_error(error: impl fmt::Display) -> ConfigurationError {
|
||||
ConfigurationError::new(ConfigurationErrorKind::Io, error.to_string())
|
||||
}
|
||||
Loading…
Reference in a new issue