Skip to main content

alien_core/
sandbox_identity.rs

1//! Resolve an OCI image's USER without running code from the image.
2
3use alien_error::{AlienError, Context, IntoAlienError};
4
5use crate::error::{ErrorData, Result};
6
7/// Numeric identity used for sandbox commands, never uid zero.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct ExecIdentity {
10    /// User id.
11    pub uid: u32,
12    /// Primary group id; it need not equal the user id.
13    pub gid: u32,
14}
15
16/// Account data from the same image whose OCI USER is being resolved.
17pub struct ImageUser<'a> {
18    /// OCI config.User, empty when absent.
19    pub user: &'a str,
20    /// Image's /etc/passwd, empty when absent.
21    pub passwd: &'a str,
22    /// Image's /etc/group, empty when absent.
23    pub group: &'a str,
24}
25
26impl ImageUser<'_> {
27    /// Preserve a non-root image USER. Root/absent USER uses the backend default, or an
28    /// explicitly declared command uid. A declaration must agree with a non-root image user.
29    /// Unknown names and invalid ids fail before an image is built or a session starts.
30    pub fn exec_identity(
31        &self,
32        default_uid: u32,
33        command_uid: Option<u32>,
34    ) -> Result<ExecIdentity> {
35        let invalid = |reason: String| ErrorData::SandboxImageUserInvalid {
36            user: self.user.to_string(),
37            reason,
38        };
39        let parse_id = |value: &str| -> Result<u32> {
40            let id = value
41                .parse::<u32>()
42                .into_alien_error()
43                .context(invalid(format!("'{value}' is not a numeric uid/gid")))?;
44            if id == u32::MAX {
45                return Err(AlienError::new(invalid(
46                    "uid/gid -1 is reserved".to_string(),
47                )));
48            }
49            Ok(id)
50        };
51        let (user, group) = self
52            .user
53            .split_once(':')
54            .map_or((self.user, None), |(u, g)| (u, Some(g)));
55        let numeric_uid = if user.is_empty() {
56            Some(0)
57        } else if user.bytes().all(|c| c.is_ascii_digit()) {
58            Some(parse_id(user)?)
59        } else {
60            None
61        };
62        let account = self.passwd.lines().find_map(|line| {
63            let fields: Vec<_> = line.split(':').collect();
64            (fields.len() >= 4
65                && numeric_uid.map_or_else(
66                    || fields[0] == user,
67                    |uid| fields[2].parse::<u32>() == Ok(uid),
68                ))
69            .then_some(fields)
70        });
71        let uid = match numeric_uid {
72            Some(uid) => uid,
73            None => parse_id(
74                account.as_ref().ok_or_else(|| {
75                    AlienError::new(invalid(format!("user '{user}' is absent from /etc/passwd")))
76                })?[2],
77            )?,
78        };
79        if uid == 0 {
80            let uid = command_uid.unwrap_or(default_uid);
81            if uid == 0 || uid == u32::MAX {
82                return Err(AlienError::new(invalid(
83                    "command uid must be non-root and cannot be -1".to_string(),
84                )));
85            }
86            return Ok(ExecIdentity { uid, gid: uid });
87        }
88        if command_uid.is_some_and(|declared| declared != uid) {
89            return Err(AlienError::new(invalid(format!(
90                "privilegedSupervisor.commandUid {} disagrees with image USER uid {uid}",
91                command_uid.unwrap_or_default()
92            ))));
93        }
94        let gid = match group {
95            Some(group) if !group.is_empty() && group.bytes().all(|c| c.is_ascii_digit()) => {
96                parse_id(group)?
97            }
98            Some(group) => {
99                let entry = self
100                    .group
101                    .lines()
102                    .find_map(|line| {
103                        let fields: Vec<_> = line.split(':').collect();
104                        (fields.len() >= 3 && fields[0] == group).then_some(fields)
105                    })
106                    .ok_or_else(|| {
107                        AlienError::new(invalid(format!(
108                            "group '{group}' is absent from /etc/group"
109                        )))
110                    })?;
111                parse_id(entry[2])?
112            }
113            None => match account {
114                Some(account) => parse_id(account[3])?,
115                // OCI/Docker uses group zero for a numeric user absent from passwd.
116                None => 0,
117            },
118        };
119        if gid == 0 {
120            return Err(AlienError::new(invalid(
121                "the root group is not permitted for sandbox commands; declare USER with a non-root primary group".to_string(),
122            )));
123        }
124        Ok(ExecIdentity { uid, gid })
125    }
126}