use serde::{Deserialize, Serialize};
pub const API: &str = "6.1.4";
pub const INFO_ROUTE: &str = "/v6.1.4/libpod/info";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SocketObservation {
pub path: String,
pub device: u64,
pub inode: u64,
pub uid: u32,
pub mode: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RemoteSocketObservation {
pub path: String,
pub exists: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Observation {
pub engine: String,
pub api_surface: String,
pub selected_api: String,
pub server_version: String,
pub hostname: String,
pub rootless: bool,
pub graph_root: String,
pub run_root: String,
pub remote_socket: Option<RemoteSocketObservation>,
pub socket: SocketObservation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExpectedContext {
pub operator_epoch: String,
pub principal_uid: u32,
pub observation: Observation,
}
fn absolute_path(value: &str) -> bool {
value.starts_with('/')
&& value.len() <= 4096
&& value
.split('/')
.skip(1)
.all(|part| !part.is_empty() && part != "." && part != "..")
&& !value.chars().any(char::is_control)
}
impl Observation {
pub fn validate(&self) -> Result<(), &'static str> {
if self.engine != "podman"
|| self.api_surface != "libpod"
|| self.selected_api != API
|| self.server_version != API
{
return Err("unsupported native Podman observation profile");
}
if self.hostname.is_empty()
|| self.hostname.len() > 255
|| self
.hostname
.chars()
.any(|c| c.is_control() || c.is_whitespace())
|| !absolute_path(&self.graph_root)
|| !absolute_path(&self.run_root)
|| !absolute_path(&self.socket.path)
|| self.socket.inode == 0
|| self.socket.mode & 0o170000 != 0o140000
|| self.socket.mode & 0o7000 != 0
|| self.socket.mode & 0o007 != 0
|| self.socket.mode & !0o177777 != 0
{
return Err("invalid native Podman observation");
}
if let Some(remote) = &self.remote_socket {
if !remote.exists || remote.path != self.socket.path {
return Err("Podman advertised socket differs from selected socket");
}
}
Ok(())
}
}
impl ExpectedContext {
pub fn validate(&self) -> Result<(), &'static str> {
if self.operator_epoch.len() != 32
|| !self
.operator_epoch
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err("explicit operator epoch required");
}
self.observation.validate()
}
pub fn validate_local_socket_policy(&self) -> Result<(), &'static str> {
self.validate()?;
let uid = self.observation.socket.uid;
if self.observation.rootless {
if self.principal_uid == 0 || uid != self.principal_uid {
return Err("rootless socket must belong to non-root principal");
}
} else if uid != 0 {
return Err("rootful socket must belong to root");
}
Ok(())
}
#[cfg(unix)]
pub fn match_current_operation(
&self,
bound: &Self,
before: &Observation,
after: &Observation,
) -> Result<(), &'static str> {
if self.principal_uid != unsafe { libc::geteuid() } {
return Err("Podman configured principal differs from process euid");
}
self.match_operation(bound, before, after)
}
pub fn match_operation(
&self,
bound: &Self,
before: &Observation,
after: &Observation,
) -> Result<(), &'static str> {
self.validate()?;
bound.validate()?;
before.validate()?;
after.validate()?;
if self != bound || &self.observation != before || before != after {
return Err("Podman operation context or observations changed");
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn context() -> ExpectedContext {
ExpectedContext {
operator_epoch: "a".repeat(32),
principal_uid: 1000,
observation: Observation {
engine: "podman".into(),
api_surface: "libpod".into(),
selected_api: API.into(),
server_version: API.into(),
hostname: "fixture".into(),
rootless: false,
graph_root: "/var/lib/containers/storage".into(),
run_root: "/run/containers/storage".into(),
remote_socket: None,
socket: SocketObservation {
path: "/run/podman/podman.sock".into(),
device: 1,
inode: 2,
uid: 0,
mode: 0o140660,
},
},
}
}
#[test]
fn explicit_rootful_and_rootless_contexts_match() {
for rootless in [false, true] {
let mut c = context();
c.observation.rootless = rootless;
if rootless {
c.observation.socket.uid = 1000;
}
c.match_operation(&c, &c.observation, &c.observation)
.unwrap();
}
}
#[test]
fn epoch_and_principal_changes_are_rejected() {
let c = context();
let mut epoch = c.clone();
epoch.operator_epoch = "b".repeat(32);
let mut principal = c.clone();
principal.principal_uid += 1;
for changed in [epoch, principal] {
assert!(c
.match_operation(&changed, &c.observation, &c.observation)
.is_err());
}
for value in ["".into(), "A".repeat(32), "a".repeat(31)] {
let mut changed = c.clone();
changed.operator_epoch = value;
assert!(changed.validate().is_err());
}
}
#[test]
fn every_observation_dimension_is_bound() {
let c = context();
let original = serde_json::to_value(&c.observation).unwrap();
for (field, value) in [
("engine", serde_json::json!("docker")),
("api_surface", serde_json::json!("compat")),
("selected_api", serde_json::json!("1.44.0")),
("server_version", serde_json::json!("6.1.5")),
("hostname", serde_json::json!("other")),
("rootless", serde_json::json!(true)),
("graph_root", serde_json::json!("/other")),
("run_root", serde_json::json!("/other")),
] {
let mut v = original.clone();
v[field] = value;
let changed: Observation = serde_json::from_value(v).unwrap();
assert!(
c.match_operation(&c, &c.observation, &changed).is_err(),
"{field}"
);
assert!(
c.match_operation(&c, &changed, &changed).is_err(),
"{field}"
);
}
for (field, value) in [
("device", 3),
("inode", 3),
("uid", 1000),
("mode", 0o140600),
] {
let mut v = original.clone();
v["socket"][field] = serde_json::json!(value);
let changed: Observation = serde_json::from_value(v).unwrap();
assert!(
c.match_operation(&c, &c.observation, &changed).is_err(),
"{field}"
);
}
}
#[test]
fn optional_remote_socket_is_exact_when_present() {
let mut c = context();
c.observation.remote_socket = Some(RemoteSocketObservation {
path: c.observation.socket.path.clone(),
exists: true,
});
c.validate().unwrap();
let mut changed = c.observation.clone();
changed.remote_socket = None;
assert!(c.match_operation(&c, &c.observation, &changed).is_err());
c.observation.remote_socket.as_mut().unwrap().exists = false;
assert!(c.validate().is_err());
}
#[test]
fn daemon_id_epoch_in_response_and_duplicate_fields_are_rejected() {
let o = context().observation;
for key in ["daemon_id", "ID", "operator_epoch"] {
let mut v = serde_json::to_value(&o).unwrap();
v[key] = "fabricated".into();
assert!(serde_json::from_value::<Observation>(v).is_err());
}
let text = serde_json::to_string(&o).unwrap();
let duplicate = text.replacen('{', "{\"engine\":\"podman\",", 1);
assert!(serde_json::from_str::<Observation>(&duplicate).is_err());
}
#[cfg(unix)]
#[test]
fn actual_principal_cannot_be_replaced_by_self_consistent_caller_values() {
let mut c = context();
c.principal_uid = unsafe { libc::geteuid() };
c.match_current_operation(&c, &c.observation, &c.observation)
.unwrap();
c.principal_uid = c.principal_uid.wrapping_add(1);
assert!(c
.match_current_operation(&c, &c.observation, &c.observation)
.is_err());
}
#[test]
fn different_valid_socket_path_is_not_the_same_context() {
let c = context();
let mut changed = c.observation.clone();
changed.socket.path = "/run/podman/other.sock".into();
changed.validate().unwrap();
assert!(c.match_operation(&c, &changed, &changed).is_err());
}
#[test]
fn unsafe_paths_and_socket_types_are_rejected() {
for path in [
"relative",
"/run/../socket",
"/run//socket",
"/run/socket/",
"/run/so\0cket",
] {
let mut c = context();
c.observation.socket.path = path.into();
assert!(c.validate().is_err());
}
for mode in [0o100600, 0o140666, 0o144600] {
let mut c = context();
c.observation.socket.mode = mode;
assert!(c.validate().is_err());
}
}
#[test]
fn local_socket_owner_policy_is_explicit() {
let mut c = context();
c.principal_uid = 1000;
c.observation.rootless = false;
c.observation.socket.uid = 0;
assert!(c.validate_local_socket_policy().is_ok());
c.observation.socket.uid = 1000;
assert!(c.validate_local_socket_policy().is_err());
c.observation.rootless = true;
assert!(c.validate_local_socket_policy().is_ok());
c.observation.socket.uid = 1001;
assert!(c.validate_local_socket_policy().is_err());
c.principal_uid = 0;
c.observation.socket.uid = 0;
assert!(c.validate_local_socket_policy().is_err());
}
}