1use serde::{Deserialize, Serialize};
4
5pub const API: &str = "6.1.4";
6pub const READ_ONLY_API_5_7: &str = "5.7.0";
7pub const INFO_ROUTE: &str = "/v6.1.4/libpod/info";
8
9pub fn read_only_api(value: &str) -> bool {
10 value == API || value == READ_ONLY_API_5_7
11}
12
13pub fn info_route(api: &str) -> Result<String, &'static str> {
14 if read_only_api(api) {
15 Ok(format!("/v{api}/libpod/info"))
16 } else {
17 Err("unsupported native Podman API")
18 }
19}
20
21pub fn normalize_local_socket_path(value: &str) -> Result<String, &'static str> {
24 let path = value.strip_prefix("unix://").unwrap_or(value);
25 if !path.starts_with('/')
26 || path == "/"
27 || path.len() > 4096
28 || path.ends_with('/')
29 || path.contains("//")
30 || path
31 .chars()
32 .any(|c| c.is_control() || matches!(c, '?' | '#' | '%' | '\\') || c == '\0')
33 || path
34 .split('/')
35 .skip(1)
36 .any(|part| part.is_empty() || part == "." || part == "..")
37 {
38 return Err("local Podman socket path rejected");
39 }
40 Ok(path.to_string())
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct SocketObservation {
46 pub path: String,
47 pub device: u64,
48 pub inode: u64,
49 pub uid: u32,
50 pub mode: u32,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct RemoteSocketObservation {
57 pub path: String,
58 pub exists: bool,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct Observation {
66 pub engine: String,
67 pub api_surface: String,
68 pub selected_api: String,
69 pub server_version: String,
70 pub hostname: String,
71 pub rootless: bool,
72 pub graph_root: String,
73 pub run_root: String,
74 pub remote_socket: Option<RemoteSocketObservation>,
75 pub socket: SocketObservation,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80#[serde(deny_unknown_fields)]
81pub struct ExpectedContext {
82 pub operator_epoch: String,
83 pub principal_uid: u32,
84 pub observation: Observation,
85}
86
87fn absolute_path(value: &str) -> bool {
88 value.starts_with('/')
89 && value.len() <= 4096
90 && value
91 .split('/')
92 .skip(1)
93 .all(|part| !part.is_empty() && part != "." && part != "..")
94 && !value.chars().any(char::is_control)
95}
96
97impl Observation {
98 fn validate_shape(&self) -> Result<(), &'static str> {
99 if self.engine != "podman" || self.api_surface != "libpod" {
100 return Err("unsupported native Podman observation profile");
101 }
102 if self.hostname.is_empty()
103 || self.hostname.len() > 255
104 || self
105 .hostname
106 .chars()
107 .any(|c| c.is_control() || c.is_whitespace())
108 || !absolute_path(&self.graph_root)
109 || !absolute_path(&self.run_root)
110 || !absolute_path(&self.socket.path)
111 || self.socket.inode == 0
112 || self.socket.mode & 0o170000 != 0o140000
113 || self.socket.mode & 0o7000 != 0
114 || self.socket.mode & 0o007 != 0
115 || self.socket.mode & !0o177777 != 0
116 {
117 return Err("invalid native Podman observation");
118 }
119 if let Some(remote) = &self.remote_socket {
120 if !remote.exists || remote.path != self.socket.path {
121 return Err("Podman advertised socket differs from selected socket");
122 }
123 }
124 Ok(())
125 }
126
127 pub fn validate(&self) -> Result<(), &'static str> {
129 self.validate_shape()?;
130 if self.selected_api != API || self.server_version != API {
131 return Err("unsupported native Podman observation profile");
132 }
133 Ok(())
134 }
135
136 pub fn validate_read_only(&self) -> Result<(), &'static str> {
138 self.validate_shape()?;
139 if !read_only_api(&self.selected_api) || self.server_version != self.selected_api {
140 return Err("unsupported read-only Podman observation profile");
141 }
142 Ok(())
143 }
144}
145
146impl ExpectedContext {
147 pub fn validate(&self) -> Result<(), &'static str> {
148 self.validate_epoch_shape()?;
149 self.observation.validate()
150 }
151
152 pub fn validate_read_only(&self) -> Result<(), &'static str> {
153 self.validate_epoch_shape()?;
154 self.observation.validate_read_only()
155 }
156
157 fn validate_epoch_shape(&self) -> Result<(), &'static str> {
158 if self.operator_epoch.len() != 32
159 || !self
160 .operator_epoch
161 .bytes()
162 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
163 {
164 return Err("explicit operator epoch required");
165 }
166 Ok(())
167 }
168
169 pub fn validate_local_socket_policy(&self) -> Result<(), &'static str> {
173 self.validate()?;
174 self.validate_socket_owner()
175 }
176
177 pub fn validate_local_socket_policy_read_only(&self) -> Result<(), &'static str> {
178 self.validate_read_only()?;
179 self.validate_socket_owner()
180 }
181
182 fn validate_socket_owner(&self) -> Result<(), &'static str> {
183 let uid = self.observation.socket.uid;
184 if self.observation.rootless {
185 if self.principal_uid == 0 || uid != self.principal_uid {
186 return Err("rootless socket must belong to non-root principal");
187 }
188 } else if uid != 0 {
189 return Err("rootful socket must belong to root");
190 }
191 Ok(())
192 }
193
194 #[cfg(unix)]
198 pub fn match_current_operation(
199 &self,
200 bound: &Self,
201 before: &Observation,
202 after: &Observation,
203 ) -> Result<(), &'static str> {
204 if self.principal_uid != unsafe { libc::geteuid() } {
205 return Err("Podman configured principal differs from process euid");
206 }
207 self.match_operation(bound, before, after)
208 }
209
210 #[cfg(unix)]
211 pub fn match_current_operation_read_only(
212 &self,
213 bound: &Self,
214 before: &Observation,
215 after: &Observation,
216 ) -> Result<(), &'static str> {
217 if self.principal_uid != unsafe { libc::geteuid() } {
218 return Err("Podman configured principal differs from process euid");
219 }
220 self.match_operation_read_only(bound, before, after)
221 }
222
223 pub fn match_operation(
227 &self,
228 bound: &Self,
229 before: &Observation,
230 after: &Observation,
231 ) -> Result<(), &'static str> {
232 self.validate()?;
233 bound.validate()?;
234 before.validate()?;
235 after.validate()?;
236 if self != bound || &self.observation != before || before != after {
237 return Err("Podman operation context or observations changed");
238 }
239 Ok(())
240 }
241
242 pub fn match_operation_read_only(
243 &self,
244 bound: &Self,
245 before: &Observation,
246 after: &Observation,
247 ) -> Result<(), &'static str> {
248 self.validate_read_only()?;
249 bound.validate_read_only()?;
250 before.validate_read_only()?;
251 after.validate_read_only()?;
252 if self != bound || &self.observation != before || before != after {
253 return Err("Podman read-only operation context or observations changed");
254 }
255 Ok(())
256 }
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 fn context() -> ExpectedContext {
263 ExpectedContext {
264 operator_epoch: "a".repeat(32),
265 principal_uid: 1000,
266 observation: Observation {
267 engine: "podman".into(),
268 api_surface: "libpod".into(),
269 selected_api: API.into(),
270 server_version: API.into(),
271 hostname: "fixture".into(),
272 rootless: false,
273 graph_root: "/var/lib/containers/storage".into(),
274 run_root: "/run/containers/storage".into(),
275 remote_socket: None,
276 socket: SocketObservation {
277 path: "/run/podman/podman.sock".into(),
278 device: 1,
279 inode: 2,
280 uid: 0,
281 mode: 0o140660,
282 },
283 },
284 }
285 }
286
287 #[test]
288 fn explicit_rootful_and_rootless_contexts_match() {
289 for rootless in [false, true] {
290 let mut c = context();
291 c.observation.rootless = rootless;
292 if rootless {
293 c.observation.socket.uid = 1000;
294 }
295 c.match_operation(&c, &c.observation, &c.observation)
296 .unwrap();
297 }
298 }
299
300 #[test]
301 fn epoch_and_principal_changes_are_rejected() {
302 let c = context();
303 let mut epoch = c.clone();
304 epoch.operator_epoch = "b".repeat(32);
305 let mut principal = c.clone();
306 principal.principal_uid += 1;
307 for changed in [epoch, principal] {
308 assert!(c
309 .match_operation(&changed, &c.observation, &c.observation)
310 .is_err());
311 }
312 for value in ["".into(), "A".repeat(32), "a".repeat(31)] {
313 let mut changed = c.clone();
314 changed.operator_epoch = value;
315 assert!(changed.validate().is_err());
316 }
317 }
318
319 #[test]
320 fn every_observation_dimension_is_bound() {
321 let c = context();
322 let original = serde_json::to_value(&c.observation).unwrap();
323 for (field, value) in [
324 ("engine", serde_json::json!("docker")),
325 ("api_surface", serde_json::json!("compat")),
326 ("selected_api", serde_json::json!("1.44.0")),
327 ("server_version", serde_json::json!("6.1.5")),
328 ("hostname", serde_json::json!("other")),
329 ("rootless", serde_json::json!(true)),
330 ("graph_root", serde_json::json!("/other")),
331 ("run_root", serde_json::json!("/other")),
332 ] {
333 let mut v = original.clone();
334 v[field] = value;
335 let changed: Observation = serde_json::from_value(v).unwrap();
336 assert!(
337 c.match_operation(&c, &c.observation, &changed).is_err(),
338 "{field}"
339 );
340 assert!(
341 c.match_operation(&c, &changed, &changed).is_err(),
342 "{field}"
343 );
344 }
345 for (field, value) in [
346 ("device", 3),
347 ("inode", 3),
348 ("uid", 1000),
349 ("mode", 0o140600),
350 ] {
351 let mut v = original.clone();
352 v["socket"][field] = serde_json::json!(value);
353 let changed: Observation = serde_json::from_value(v).unwrap();
354 assert!(
355 c.match_operation(&c, &c.observation, &changed).is_err(),
356 "{field}"
357 );
358 }
359 }
360
361 #[test]
362 fn optional_remote_socket_is_exact_when_present() {
363 let mut c = context();
364 c.observation.remote_socket = Some(RemoteSocketObservation {
365 path: c.observation.socket.path.clone(),
366 exists: true,
367 });
368 c.validate().unwrap();
369 let mut changed = c.observation.clone();
370 changed.remote_socket = None;
371 assert!(c.match_operation(&c, &c.observation, &changed).is_err());
372 c.observation.remote_socket.as_mut().unwrap().exists = false;
373 assert!(c.validate().is_err());
374 }
375
376 #[test]
377 fn daemon_id_epoch_in_response_and_duplicate_fields_are_rejected() {
378 let o = context().observation;
379 for key in ["daemon_id", "ID", "operator_epoch"] {
380 let mut v = serde_json::to_value(&o).unwrap();
381 v[key] = "fabricated".into();
382 assert!(serde_json::from_value::<Observation>(v).is_err());
383 }
384 let text = serde_json::to_string(&o).unwrap();
385 let duplicate = text.replacen('{', "{\"engine\":\"podman\",", 1);
386 assert!(serde_json::from_str::<Observation>(&duplicate).is_err());
387 }
388
389 #[cfg(unix)]
390 #[test]
391 fn actual_principal_cannot_be_replaced_by_self_consistent_caller_values() {
392 let mut c = context();
393 c.principal_uid = unsafe { libc::geteuid() };
394 c.match_current_operation(&c, &c.observation, &c.observation)
395 .unwrap();
396 c.principal_uid = c.principal_uid.wrapping_add(1);
397 assert!(c
398 .match_current_operation(&c, &c.observation, &c.observation)
399 .is_err());
400 }
401
402 #[test]
403 fn different_valid_socket_path_is_not_the_same_context() {
404 let c = context();
405 let mut changed = c.observation.clone();
406 changed.socket.path = "/run/podman/other.sock".into();
407 changed.validate().unwrap();
408 assert!(c.match_operation(&c, &changed, &changed).is_err());
409 }
410
411 #[test]
412 fn unsafe_paths_and_socket_types_are_rejected() {
413 for path in [
414 "relative",
415 "/run/../socket",
416 "/run//socket",
417 "/run/socket/",
418 "/run/so\0cket",
419 ] {
420 let mut c = context();
421 c.observation.socket.path = path.into();
422 assert!(c.validate().is_err());
423 }
424 for mode in [0o100600, 0o140666, 0o144600] {
425 let mut c = context();
426 c.observation.socket.mode = mode;
427 assert!(c.validate().is_err());
428 }
429 }
430 #[test]
431 fn five_seven_is_read_only_only() {
432 let mut c = context();
433 c.observation.selected_api = READ_ONLY_API_5_7.into();
434 c.observation.server_version = READ_ONLY_API_5_7.into();
435 assert!(c.validate().is_err());
436 assert!(c
437 .match_operation(&c, &c.observation, &c.observation)
438 .is_err());
439 c.validate_read_only().unwrap();
440 c.match_operation_read_only(&c, &c.observation, &c.observation)
441 .unwrap();
442 let mut mismatch = c.clone();
443 mismatch.observation.server_version = API.into();
444 assert!(mismatch.validate_read_only().is_err());
445 let mut unknown = c.clone();
446 unknown.observation.selected_api = "5.8.0".into();
447 unknown.observation.server_version = "5.8.0".into();
448 assert!(unknown.validate_read_only().is_err());
449 }
450
451 #[test]
452 fn local_socket_uri_normalization_is_unambiguous() {
453 assert_eq!(
454 normalize_local_socket_path("unix:///run/user/1000/podman/podman.sock").unwrap(),
455 "/run/user/1000/podman/podman.sock"
456 );
457 assert_eq!(
458 normalize_local_socket_path("/run/user/1000/podman/podman.sock").unwrap(),
459 "/run/user/1000/podman/podman.sock"
460 );
461 for value in [
462 "unix://host/run/podman.sock",
463 "unix:///run/../podman.sock",
464 "unix:///run/%2e%2e/podman.sock",
465 "unix:///run/podman.sock?x=1",
466 "unix:///run/podman.sock#frag",
467 "unix:///run//podman.sock",
468 "unix:///run/podman.sock/",
469 "relative.sock",
470 ] {
471 assert!(normalize_local_socket_path(value).is_err(), "{value}");
472 }
473 }
474
475 #[test]
476 fn local_socket_owner_policy_is_explicit() {
477 let mut c = context();
478 c.principal_uid = 1000;
479 c.observation.rootless = false;
480 c.observation.socket.uid = 0;
481 assert!(c.validate_local_socket_policy().is_ok());
482 c.observation.socket.uid = 1000;
483 assert!(c.validate_local_socket_policy().is_err());
484 c.observation.rootless = true;
485 assert!(c.validate_local_socket_policy().is_ok());
486 c.observation.socket.uid = 1001;
487 assert!(c.validate_local_socket_policy().is_err());
488 c.principal_uid = 0;
489 c.observation.socket.uid = 0;
490 assert!(c.validate_local_socket_policy().is_err());
491 }
492}