gatehouse_runtime_protocol/lifecycle_v2/
observation.rs1use serde::{Deserialize, Serialize};
4
5pub const API: &str = "6.1.4";
6pub const INFO_ROUTE: &str = "/v6.1.4/libpod/info";
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub struct SocketObservation {
11 pub path: String,
12 pub device: u64,
13 pub inode: u64,
14 pub uid: u32,
15 pub mode: u32,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct RemoteSocketObservation {
22 pub path: String,
23 pub exists: bool,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct Observation {
31 pub engine: String,
32 pub api_surface: String,
33 pub selected_api: String,
34 pub server_version: String,
35 pub hostname: String,
36 pub rootless: bool,
37 pub graph_root: String,
38 pub run_root: String,
39 pub remote_socket: Option<RemoteSocketObservation>,
40 pub socket: SocketObservation,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct ExpectedContext {
47 pub operator_epoch: String,
48 pub principal_uid: u32,
49 pub observation: Observation,
50}
51
52fn absolute_path(value: &str) -> bool {
53 value.starts_with('/')
54 && value.len() <= 4096
55 && value
56 .split('/')
57 .skip(1)
58 .all(|part| !part.is_empty() && part != "." && part != "..")
59 && !value.chars().any(char::is_control)
60}
61
62impl Observation {
63 pub fn validate(&self) -> Result<(), &'static str> {
64 if self.engine != "podman"
65 || self.api_surface != "libpod"
66 || self.selected_api != API
67 || self.server_version != API
68 {
69 return Err("unsupported native Podman observation profile");
70 }
71 if self.hostname.is_empty()
72 || self.hostname.len() > 255
73 || self
74 .hostname
75 .chars()
76 .any(|c| c.is_control() || c.is_whitespace())
77 || !absolute_path(&self.graph_root)
78 || !absolute_path(&self.run_root)
79 || !absolute_path(&self.socket.path)
80 || self.socket.inode == 0
81 || self.socket.mode & 0o170000 != 0o140000
82 || self.socket.mode & 0o7000 != 0
83 || self.socket.mode & 0o007 != 0
84 || self.socket.mode & !0o177777 != 0
85 {
86 return Err("invalid native Podman observation");
87 }
88 if let Some(remote) = &self.remote_socket {
89 if !remote.exists || remote.path != self.socket.path {
90 return Err("Podman advertised socket differs from selected socket");
91 }
92 }
93 Ok(())
94 }
95}
96
97impl ExpectedContext {
98 pub fn validate(&self) -> Result<(), &'static str> {
99 if self.operator_epoch.len() != 32
100 || !self
101 .operator_epoch
102 .bytes()
103 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
104 {
105 return Err("explicit operator epoch required");
106 }
107 self.observation.validate()
108 }
109
110 pub fn validate_local_socket_policy(&self) -> Result<(), &'static str> {
114 self.validate()?;
115 let uid = self.observation.socket.uid;
116 if self.observation.rootless {
117 if self.principal_uid == 0 || uid != self.principal_uid {
118 return Err("rootless socket must belong to non-root principal");
119 }
120 } else if uid != 0 {
121 return Err("rootful socket must belong to root");
122 }
123 Ok(())
124 }
125
126 #[cfg(unix)]
130 pub fn match_current_operation(
131 &self,
132 bound: &Self,
133 before: &Observation,
134 after: &Observation,
135 ) -> Result<(), &'static str> {
136 if self.principal_uid != unsafe { libc::geteuid() } {
137 return Err("Podman configured principal differs from process euid");
138 }
139 self.match_operation(bound, before, after)
140 }
141
142 pub fn match_operation(
146 &self,
147 bound: &Self,
148 before: &Observation,
149 after: &Observation,
150 ) -> Result<(), &'static str> {
151 self.validate()?;
152 bound.validate()?;
153 before.validate()?;
154 after.validate()?;
155 if self != bound || &self.observation != before || before != after {
156 return Err("Podman operation context or observations changed");
157 }
158 Ok(())
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 fn context() -> ExpectedContext {
166 ExpectedContext {
167 operator_epoch: "a".repeat(32),
168 principal_uid: 1000,
169 observation: Observation {
170 engine: "podman".into(),
171 api_surface: "libpod".into(),
172 selected_api: API.into(),
173 server_version: API.into(),
174 hostname: "fixture".into(),
175 rootless: false,
176 graph_root: "/var/lib/containers/storage".into(),
177 run_root: "/run/containers/storage".into(),
178 remote_socket: None,
179 socket: SocketObservation {
180 path: "/run/podman/podman.sock".into(),
181 device: 1,
182 inode: 2,
183 uid: 0,
184 mode: 0o140660,
185 },
186 },
187 }
188 }
189
190 #[test]
191 fn explicit_rootful_and_rootless_contexts_match() {
192 for rootless in [false, true] {
193 let mut c = context();
194 c.observation.rootless = rootless;
195 if rootless {
196 c.observation.socket.uid = 1000;
197 }
198 c.match_operation(&c, &c.observation, &c.observation)
199 .unwrap();
200 }
201 }
202
203 #[test]
204 fn epoch_and_principal_changes_are_rejected() {
205 let c = context();
206 let mut epoch = c.clone();
207 epoch.operator_epoch = "b".repeat(32);
208 let mut principal = c.clone();
209 principal.principal_uid += 1;
210 for changed in [epoch, principal] {
211 assert!(c
212 .match_operation(&changed, &c.observation, &c.observation)
213 .is_err());
214 }
215 for value in ["".into(), "A".repeat(32), "a".repeat(31)] {
216 let mut changed = c.clone();
217 changed.operator_epoch = value;
218 assert!(changed.validate().is_err());
219 }
220 }
221
222 #[test]
223 fn every_observation_dimension_is_bound() {
224 let c = context();
225 let original = serde_json::to_value(&c.observation).unwrap();
226 for (field, value) in [
227 ("engine", serde_json::json!("docker")),
228 ("api_surface", serde_json::json!("compat")),
229 ("selected_api", serde_json::json!("1.44.0")),
230 ("server_version", serde_json::json!("6.1.5")),
231 ("hostname", serde_json::json!("other")),
232 ("rootless", serde_json::json!(true)),
233 ("graph_root", serde_json::json!("/other")),
234 ("run_root", serde_json::json!("/other")),
235 ] {
236 let mut v = original.clone();
237 v[field] = value;
238 let changed: Observation = serde_json::from_value(v).unwrap();
239 assert!(
240 c.match_operation(&c, &c.observation, &changed).is_err(),
241 "{field}"
242 );
243 assert!(
244 c.match_operation(&c, &changed, &changed).is_err(),
245 "{field}"
246 );
247 }
248 for (field, value) in [
249 ("device", 3),
250 ("inode", 3),
251 ("uid", 1000),
252 ("mode", 0o140600),
253 ] {
254 let mut v = original.clone();
255 v["socket"][field] = serde_json::json!(value);
256 let changed: Observation = serde_json::from_value(v).unwrap();
257 assert!(
258 c.match_operation(&c, &c.observation, &changed).is_err(),
259 "{field}"
260 );
261 }
262 }
263
264 #[test]
265 fn optional_remote_socket_is_exact_when_present() {
266 let mut c = context();
267 c.observation.remote_socket = Some(RemoteSocketObservation {
268 path: c.observation.socket.path.clone(),
269 exists: true,
270 });
271 c.validate().unwrap();
272 let mut changed = c.observation.clone();
273 changed.remote_socket = None;
274 assert!(c.match_operation(&c, &c.observation, &changed).is_err());
275 c.observation.remote_socket.as_mut().unwrap().exists = false;
276 assert!(c.validate().is_err());
277 }
278
279 #[test]
280 fn daemon_id_epoch_in_response_and_duplicate_fields_are_rejected() {
281 let o = context().observation;
282 for key in ["daemon_id", "ID", "operator_epoch"] {
283 let mut v = serde_json::to_value(&o).unwrap();
284 v[key] = "fabricated".into();
285 assert!(serde_json::from_value::<Observation>(v).is_err());
286 }
287 let text = serde_json::to_string(&o).unwrap();
288 let duplicate = text.replacen('{', "{\"engine\":\"podman\",", 1);
289 assert!(serde_json::from_str::<Observation>(&duplicate).is_err());
290 }
291
292 #[cfg(unix)]
293 #[test]
294 fn actual_principal_cannot_be_replaced_by_self_consistent_caller_values() {
295 let mut c = context();
296 c.principal_uid = unsafe { libc::geteuid() };
297 c.match_current_operation(&c, &c.observation, &c.observation)
298 .unwrap();
299 c.principal_uid = c.principal_uid.wrapping_add(1);
300 assert!(c
301 .match_current_operation(&c, &c.observation, &c.observation)
302 .is_err());
303 }
304
305 #[test]
306 fn different_valid_socket_path_is_not_the_same_context() {
307 let c = context();
308 let mut changed = c.observation.clone();
309 changed.socket.path = "/run/podman/other.sock".into();
310 changed.validate().unwrap();
311 assert!(c.match_operation(&c, &changed, &changed).is_err());
312 }
313
314 #[test]
315 fn unsafe_paths_and_socket_types_are_rejected() {
316 for path in [
317 "relative",
318 "/run/../socket",
319 "/run//socket",
320 "/run/socket/",
321 "/run/so\0cket",
322 ] {
323 let mut c = context();
324 c.observation.socket.path = path.into();
325 assert!(c.validate().is_err());
326 }
327 for mode in [0o100600, 0o140666, 0o144600] {
328 let mut c = context();
329 c.observation.socket.mode = mode;
330 assert!(c.validate().is_err());
331 }
332 }
333 #[test]
334 fn local_socket_owner_policy_is_explicit() {
335 let mut c = context();
336 c.principal_uid = 1000;
337 c.observation.rootless = false;
338 c.observation.socket.uid = 0;
339 assert!(c.validate_local_socket_policy().is_ok());
340 c.observation.socket.uid = 1000;
341 assert!(c.validate_local_socket_policy().is_err());
342 c.observation.rootless = true;
343 assert!(c.validate_local_socket_policy().is_ok());
344 c.observation.socket.uid = 1001;
345 assert!(c.validate_local_socket_policy().is_err());
346 c.principal_uid = 0;
347 c.observation.socket.uid = 0;
348 assert!(c.validate_local_socket_policy().is_err());
349 }
350}