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 image identity. Exec identities must additionally exclude uid and gid 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 identity = self.image_identity()?;
36        let invalid = |reason: String| ErrorData::SandboxImageUserInvalid {
37            user: self.user.to_string(),
38            reason,
39        };
40        if identity.uid == 0 {
41            let uid = command_uid.unwrap_or(default_uid);
42            if uid == 0 || uid == u32::MAX {
43                return Err(AlienError::new(invalid(
44                    "command uid must be non-root and cannot be -1".to_string(),
45                )));
46            }
47            return Ok(ExecIdentity { uid, gid: uid });
48        }
49        if command_uid.is_some_and(|declared| declared != identity.uid) {
50            return Err(AlienError::new(invalid(format!(
51                "privilegedSupervisor.commandUid {} disagrees with image USER uid {}",
52                command_uid.unwrap_or_default(),
53                identity.uid
54            ))));
55        }
56        if identity.gid == 0 {
57            return Err(AlienError::new(invalid(
58                "the root group is not permitted for sandbox commands; declare USER with a non-root primary group".to_string(),
59            )));
60        }
61        Ok(identity)
62    }
63
64    /// Resolve OCI USER for the image boot process, including root and an absent USER.
65    /// Numeric users without passwd entries use group zero, as Docker does.
66    pub fn image_identity(&self) -> Result<ExecIdentity> {
67        let invalid = |reason: String| ErrorData::SandboxImageUserInvalid {
68            user: self.user.to_string(),
69            reason,
70        };
71        let parse_id = |value: &str| -> Result<u32> {
72            let id = value
73                .parse::<u32>()
74                .into_alien_error()
75                .context(invalid(format!("'{value}' is not a numeric uid/gid")))?;
76            if id == u32::MAX {
77                return Err(AlienError::new(invalid(
78                    "uid/gid -1 is reserved".to_string(),
79                )));
80            }
81            Ok(id)
82        };
83        let (user, group) = self
84            .user
85            .split_once(':')
86            .map_or((self.user, None), |(u, g)| (u, Some(g)));
87        let numeric_uid = if user.is_empty() {
88            Some(0)
89        } else if user.bytes().all(|c| c.is_ascii_digit()) {
90            Some(parse_id(user)?)
91        } else {
92            None
93        };
94        let account = self.passwd.lines().find_map(|line| {
95            let fields: Vec<_> = line.split(':').collect();
96            (fields.len() >= 4
97                && numeric_uid.map_or_else(
98                    || fields[0] == user,
99                    |uid| fields[2].parse::<u32>() == Ok(uid),
100                ))
101            .then_some(fields)
102        });
103        let uid = match numeric_uid {
104            Some(uid) => uid,
105            None => parse_id(
106                account.as_ref().ok_or_else(|| {
107                    AlienError::new(invalid(format!("user '{user}' is absent from /etc/passwd")))
108                })?[2],
109            )?,
110        };
111        let gid = match group {
112            Some(group) if !group.is_empty() && group.bytes().all(|c| c.is_ascii_digit()) => {
113                parse_id(group)?
114            }
115            Some(group) => {
116                let entry = self
117                    .group
118                    .lines()
119                    .find_map(|line| {
120                        let fields: Vec<_> = line.split(':').collect();
121                        (fields.len() >= 3 && fields[0] == group).then_some(fields)
122                    })
123                    .ok_or_else(|| {
124                        AlienError::new(invalid(format!(
125                            "group '{group}' is absent from /etc/group"
126                        )))
127                    })?;
128                parse_id(entry[2])?
129            }
130            None => match account {
131                Some(account) => parse_id(account[3])?,
132                // OCI/Docker uses group zero for a numeric user absent from passwd.
133                None => 0,
134            },
135        };
136        Ok(ExecIdentity { uid, gid })
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn image_root_is_distinct_from_the_non_root_exec_fallback() {
146        for user in ["", "0", "0:0", "root", "supervisor:workers"] {
147            let image = ImageUser {
148                user,
149                passwd: "root:x:0:0::/:/bin/sh\nsupervisor:x:0:2000::/:/bin/sh\n",
150                group: "workers:x:2000:\n",
151            };
152            assert_eq!(image.image_identity().unwrap().uid, 0);
153            assert_eq!(
154                image.exec_identity(60000, None).unwrap(),
155                ExecIdentity {
156                    uid: 60000,
157                    gid: 60000
158                }
159            );
160            assert_eq!(
161                image.exec_identity(1000, Some(60001)).unwrap(),
162                ExecIdentity {
163                    uid: 60001,
164                    gid: 60001
165                }
166            );
167        }
168    }
169
170    #[test]
171    fn image_and_exec_preserve_non_root_users_and_named_primary_groups() {
172        for user in ["worker", "worker:workers", "2001:2000", "2001"] {
173            let image = ImageUser {
174                user,
175                passwd: "worker:x:2001:2000::/:/bin/sh\n",
176                group: "workers:x:2000:\n",
177            };
178            let expected = ExecIdentity {
179                uid: 2001,
180                gid: 2000,
181            };
182            assert_eq!(image.image_identity().unwrap(), expected);
183            assert_eq!(image.exec_identity(60000, None).unwrap(), expected);
184            assert!(image.exec_identity(60000, Some(2002)).is_err());
185        }
186    }
187
188    #[test]
189    fn numeric_image_user_can_have_root_group_but_exec_cannot() {
190        let image = ImageUser {
191            user: "2001",
192            passwd: "",
193            group: "",
194        };
195        assert_eq!(
196            image.image_identity().unwrap(),
197            ExecIdentity { uid: 2001, gid: 0 }
198        );
199        assert!(image.exec_identity(60000, None).is_err());
200    }
201
202    #[test]
203    fn invalid_image_users_fail_before_startup() {
204        for user in [
205            "missing",
206            "0:missing",
207            "4294967295",
208            "1:4294967295",
209            "1:2:3",
210        ] {
211            let image = ImageUser {
212                user,
213                passwd: "",
214                group: "",
215            };
216            assert_eq!(
217                image.image_identity().unwrap_err().code,
218                "SANDBOX_IMAGE_USER_INVALID"
219            );
220        }
221    }
222
223    #[test]
224    fn released_command_metadata_keeps_its_legacy_identity() {
225        let image: crate::sandbox_image::SandboxImageCommand = serde_json::from_str(
226            r#"{"command":["/bin/sleep","infinity"],"env":{},"workingDirectory":"/"}"#,
227        )
228        .unwrap();
229        assert_eq!(image.user, None);
230        let root: crate::sandbox_image::SandboxImageCommand = serde_json::from_str(
231            r#"{"user":"","command":["/bin/sleep","infinity"],"env":{},"workingDirectory":"/"}"#,
232        )
233        .unwrap();
234        assert_eq!(root.user.as_deref(), Some(""));
235    }
236}