Skip to main content

nucleus/isolation/
usermap.rs

1use crate::error::{NucleusError, Result};
2use std::fs;
3use tracing::{debug, info};
4
5/// UID/GID mapping configuration for user namespaces
6///
7/// Maps a range of UIDs/GIDs inside the container to a range outside
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct IdMapping {
10    /// ID inside the container
11    pub container_id: u32,
12    /// ID outside the container (on the host)
13    pub host_id: u32,
14    /// Number of IDs to map
15    pub count: u32,
16}
17
18impl IdMapping {
19    /// Create a new ID mapping with validation
20    pub fn new(container_id: u32, host_id: u32, count: u32) -> Self {
21        Self {
22            container_id,
23            host_id,
24            count,
25        }
26    }
27
28    /// Validate the mapping for safety.
29    ///
30    /// Rejects zero count, overflow in ID ranges, and excessively large
31    /// mappings that could map the entire host UID/GID space.
32    pub fn validate(&self, allow_host_root: bool) -> crate::error::Result<()> {
33        if self.count == 0 {
34            return Err(NucleusError::ConfigError(
35                "ID mapping count must be non-zero".to_string(),
36            ));
37        }
38
39        // Cap at 65536 to prevent overly broad mappings
40        if self.count > 65_536 {
41            return Err(NucleusError::ConfigError(format!(
42                "ID mapping count {} exceeds maximum 65536",
43                self.count
44            )));
45        }
46
47        // Check for overflow in container_id + count
48        if self.container_id.checked_add(self.count).is_none() {
49            return Err(NucleusError::ConfigError(format!(
50                "ID mapping overflow: container_id {} + count {} exceeds u32",
51                self.container_id, self.count
52            )));
53        }
54
55        // Check for overflow in host_id + count
56        if self.host_id.checked_add(self.count).is_none() {
57            return Err(NucleusError::ConfigError(format!(
58                "ID mapping overflow: host_id {} + count {} exceeds u32",
59                self.host_id, self.count
60            )));
61        }
62
63        // Reject mapping host UID 0 unless explicitly allowed (e.g., root-remapped mode)
64        if !allow_host_root && self.host_id == 0 && self.count > 0 {
65            return Err(NucleusError::ConfigError(
66                "ID mapping includes host UID/GID 0; use root-remapped mode if intentional"
67                    .to_string(),
68            ));
69        }
70
71        Ok(())
72    }
73
74    /// Create a mapping for root inside container to current user outside
75    pub fn rootless() -> Self {
76        let uid = nix::unistd::getuid().as_raw();
77        Self::new(0, uid, 1)
78    }
79
80    /// Format as a line for uid_map/gid_map file
81    fn format(&self) -> String {
82        format!("{} {} {}\n", self.container_id, self.host_id, self.count)
83    }
84}
85
86/// User namespace configuration
87#[derive(Debug, Clone)]
88pub struct UserNamespaceConfig {
89    /// UID mappings
90    pub uid_mappings: Vec<IdMapping>,
91    /// GID mappings
92    pub gid_mappings: Vec<IdMapping>,
93}
94
95impl UserNamespaceConfig {
96    /// Create config for rootless mode
97    ///
98    /// Maps container root (UID/GID 0) to current user
99    pub fn rootless() -> Self {
100        let uid = nix::unistd::getuid().as_raw();
101        let gid = nix::unistd::getgid().as_raw();
102
103        Self {
104            uid_mappings: vec![IdMapping::new(0, uid, 1)],
105            gid_mappings: vec![IdMapping::new(0, gid, 1)],
106        }
107    }
108
109    /// Create config for root-remapped mode
110    ///
111    /// When running as host root, maps container UID 0 to a high unprivileged
112    /// UID range so a container escape does not yield real host root.
113    pub fn root_remapped() -> Self {
114        Self {
115            uid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
116            gid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
117        }
118    }
119
120    /// Create config with custom mappings (validated)
121    pub fn custom(
122        uid_mappings: Vec<IdMapping>,
123        gid_mappings: Vec<IdMapping>,
124    ) -> crate::error::Result<Self> {
125        let allow_host_root = nix::unistd::Uid::effective().is_root();
126        for mapping in &uid_mappings {
127            mapping.validate(allow_host_root)?;
128        }
129        for mapping in &gid_mappings {
130            mapping.validate(allow_host_root)?;
131        }
132        Ok(Self {
133            uid_mappings,
134            gid_mappings,
135        })
136    }
137}
138
139/// User namespace mapper
140///
141/// Handles UID/GID mapping for rootless container execution
142pub struct UserNamespaceMapper {
143    config: UserNamespaceConfig,
144}
145
146impl UserNamespaceMapper {
147    pub fn new(config: UserNamespaceConfig) -> Self {
148        Self { config }
149    }
150
151    /// Setup UID/GID mappings for the current process
152    ///
153    /// This must be called after unshare(CLONE_NEWUSER) and before any other
154    /// namespace operations
155    pub fn setup_mappings(&self) -> Result<()> {
156        if !self.can_self_map_current_process() {
157            return Err(NucleusError::NamespaceError(
158                "This user namespace mapping must be written from a process outside the new \
159                 user namespace; use write_mappings_for_pid() from the parent after fork"
160                    .to_string(),
161            ));
162        }
163
164        self.write_mappings_for_pid(std::process::id())
165    }
166
167    /// Write UID/GID mappings for the given process from an external writer.
168    ///
169    /// For privileged multi-ID mappings, Linux requires a task outside the new
170    /// user namespace to write `/proc/<pid>/{uid,gid}_map`.
171    pub fn write_mappings_for_pid(&self, pid: u32) -> Result<()> {
172        info!("Setting up user namespace mappings for pid {}", pid);
173
174        if self.should_deny_setgroups() {
175            self.write_setgroups_deny(pid)?;
176        }
177
178        self.write_uid_map(pid)?;
179        self.write_gid_map(pid)?;
180
181        info!(
182            "Successfully configured user namespace mappings for pid {}",
183            pid
184        );
185        Ok(())
186    }
187
188    fn can_self_map_current_process(&self) -> bool {
189        let uid = nix::unistd::getuid().as_raw();
190        let gid = nix::unistd::getgid().as_raw();
191
192        self.config.uid_mappings.len() == 1
193            && self.config.gid_mappings.len() == 1
194            && self.config.uid_mappings[0] == IdMapping::new(0, uid, 1)
195            && self.config.gid_mappings[0] == IdMapping::new(0, gid, 1)
196    }
197
198    fn should_deny_setgroups(&self) -> bool {
199        self.config.gid_mappings.len() == 1 && self.config.gid_mappings[0].count == 1
200    }
201
202    /// Write to /proc/<pid>/setgroups to deny setgroups(2)
203    ///
204    /// This is required for the unprivileged single-ID gid_map case.
205    fn write_setgroups_deny(&self, pid: u32) -> Result<()> {
206        let path = format!("/proc/{}/setgroups", pid);
207        debug!("Writing 'deny' to {}", path);
208
209        fs::write(&path, "deny\n").map_err(|e| {
210            NucleusError::NamespaceError(format!("Failed to write to {}: {}", path, e))
211        })?;
212
213        Ok(())
214    }
215
216    /// Write UID mappings to /proc/<pid>/uid_map
217    fn write_uid_map(&self, pid: u32) -> Result<()> {
218        let path = format!("/proc/{}/uid_map", pid);
219        let mut content = String::new();
220
221        for mapping in &self.config.uid_mappings {
222            content.push_str(&mapping.format());
223        }
224
225        debug!("Writing UID mappings to {}: {}", path, content.trim());
226
227        fs::write(&path, &content).map_err(|e| {
228            NucleusError::NamespaceError(format!("Failed to write UID mappings: {}", e))
229        })?;
230
231        Ok(())
232    }
233
234    /// Write GID mappings to /proc/<pid>/gid_map
235    fn write_gid_map(&self, pid: u32) -> Result<()> {
236        let path = format!("/proc/{}/gid_map", pid);
237        let mut content = String::new();
238
239        for mapping in &self.config.gid_mappings {
240            content.push_str(&mapping.format());
241        }
242
243        debug!("Writing GID mappings to {}: {}", path, content.trim());
244
245        fs::write(&path, &content).map_err(|e| {
246            NucleusError::NamespaceError(format!("Failed to write GID mappings: {}", e))
247        })?;
248
249        Ok(())
250    }
251
252    /// Get the user namespace configuration
253    pub fn config(&self) -> &UserNamespaceConfig {
254        &self.config
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    #[test]
263    fn test_id_mapping_format() {
264        let mapping = IdMapping::new(0, 1000, 1);
265        assert_eq!(mapping.format(), "0 1000 1\n");
266
267        let mapping = IdMapping::new(1000, 2000, 100);
268        assert_eq!(mapping.format(), "1000 2000 100\n");
269    }
270
271    #[test]
272    fn test_id_mapping_rootless() {
273        let mapping = IdMapping::rootless();
274        assert_eq!(mapping.container_id, 0);
275        assert_eq!(mapping.count, 1);
276        // host_id will be the current UID
277    }
278
279    #[test]
280    fn test_user_namespace_config_rootless() {
281        let config = UserNamespaceConfig::rootless();
282        assert_eq!(config.uid_mappings.len(), 1);
283        assert_eq!(config.gid_mappings.len(), 1);
284        assert_eq!(config.uid_mappings[0].container_id, 0);
285        assert_eq!(config.gid_mappings[0].container_id, 0);
286    }
287
288    #[test]
289    fn test_user_namespace_config_custom() {
290        let uid_mappings = vec![IdMapping::new(0, 1000, 1), IdMapping::new(1000, 2000, 100)];
291        let gid_mappings = vec![IdMapping::new(0, 1000, 1)];
292
293        let config =
294            UserNamespaceConfig::custom(uid_mappings.clone(), gid_mappings.clone()).unwrap();
295        assert_eq!(config.uid_mappings, uid_mappings);
296        assert_eq!(config.gid_mappings, gid_mappings);
297    }
298
299    #[test]
300    fn test_rootless_mapping_can_self_map_current_process() {
301        let mapper = UserNamespaceMapper::new(UserNamespaceConfig::rootless());
302        assert!(mapper.can_self_map_current_process());
303        assert!(mapper.should_deny_setgroups());
304    }
305
306    #[test]
307    fn test_root_remapped_requires_external_writer() {
308        let mapper = UserNamespaceMapper::new(UserNamespaceConfig::root_remapped());
309        assert!(!mapper.can_self_map_current_process());
310        assert!(!mapper.should_deny_setgroups());
311        assert!(mapper.setup_mappings().is_err());
312    }
313
314    // Note: Testing actual mapping setup requires user namespace creation
315    // This is tested in integration tests
316}