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        info!("Setting up user namespace mappings");
157
158        // Disable setgroups to allow GID mapping without CAP_SETGID
159        self.write_setgroups_deny()?;
160
161        // Write UID mappings
162        self.write_uid_map()?;
163
164        // Write GID mappings
165        self.write_gid_map()?;
166
167        info!("Successfully configured user namespace mappings");
168        Ok(())
169    }
170
171    /// Write to /proc/self/setgroups to deny setgroups(2)
172    ///
173    /// This is required for unprivileged user namespace mapping
174    fn write_setgroups_deny(&self) -> Result<()> {
175        let path = "/proc/self/setgroups";
176        debug!("Writing 'deny' to {}", path);
177
178        fs::write(path, "deny\n").map_err(|e| {
179            NucleusError::NamespaceError(format!("Failed to write to {}: {}", path, e))
180        })?;
181
182        Ok(())
183    }
184
185    /// Write UID mappings to /proc/self/uid_map
186    fn write_uid_map(&self) -> Result<()> {
187        let path = "/proc/self/uid_map";
188        let mut content = String::new();
189
190        for mapping in &self.config.uid_mappings {
191            content.push_str(&mapping.format());
192        }
193
194        debug!("Writing UID mappings to {}: {}", path, content.trim());
195
196        fs::write(path, &content).map_err(|e| {
197            NucleusError::NamespaceError(format!("Failed to write UID mappings: {}", e))
198        })?;
199
200        Ok(())
201    }
202
203    /// Write GID mappings to /proc/self/gid_map
204    fn write_gid_map(&self) -> Result<()> {
205        let path = "/proc/self/gid_map";
206        let mut content = String::new();
207
208        for mapping in &self.config.gid_mappings {
209            content.push_str(&mapping.format());
210        }
211
212        debug!("Writing GID mappings to {}: {}", path, content.trim());
213
214        fs::write(path, &content).map_err(|e| {
215            NucleusError::NamespaceError(format!("Failed to write GID mappings: {}", e))
216        })?;
217
218        Ok(())
219    }
220
221    /// Get the user namespace configuration
222    pub fn config(&self) -> &UserNamespaceConfig {
223        &self.config
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_id_mapping_format() {
233        let mapping = IdMapping::new(0, 1000, 1);
234        assert_eq!(mapping.format(), "0 1000 1\n");
235
236        let mapping = IdMapping::new(1000, 2000, 100);
237        assert_eq!(mapping.format(), "1000 2000 100\n");
238    }
239
240    #[test]
241    fn test_id_mapping_rootless() {
242        let mapping = IdMapping::rootless();
243        assert_eq!(mapping.container_id, 0);
244        assert_eq!(mapping.count, 1);
245        // host_id will be the current UID
246    }
247
248    #[test]
249    fn test_user_namespace_config_rootless() {
250        let config = UserNamespaceConfig::rootless();
251        assert_eq!(config.uid_mappings.len(), 1);
252        assert_eq!(config.gid_mappings.len(), 1);
253        assert_eq!(config.uid_mappings[0].container_id, 0);
254        assert_eq!(config.gid_mappings[0].container_id, 0);
255    }
256
257    #[test]
258    fn test_user_namespace_config_custom() {
259        let uid_mappings = vec![IdMapping::new(0, 1000, 1), IdMapping::new(1000, 2000, 100)];
260        let gid_mappings = vec![IdMapping::new(0, 1000, 1)];
261
262        let config =
263            UserNamespaceConfig::custom(uid_mappings.clone(), gid_mappings.clone()).unwrap();
264        assert_eq!(config.uid_mappings, uid_mappings);
265        assert_eq!(config.gid_mappings, gid_mappings);
266    }
267
268    // Note: Testing actual mapping setup requires user namespace creation
269    // This is tested in integration tests
270}