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
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    /// Create a mapping for root inside container to current user outside
29    pub fn rootless() -> Self {
30        let uid = nix::unistd::getuid().as_raw();
31        Self::new(0, uid, 1)
32    }
33
34    /// Format as a line for uid_map/gid_map file
35    fn format(&self) -> String {
36        format!("{} {} {}\n", self.container_id, self.host_id, self.count)
37    }
38}
39
40/// User namespace configuration
41#[derive(Debug, Clone)]
42pub struct UserNamespaceConfig {
43    /// UID mappings
44    pub uid_mappings: Vec<IdMapping>,
45    /// GID mappings
46    pub gid_mappings: Vec<IdMapping>,
47}
48
49impl UserNamespaceConfig {
50    /// Create config for rootless mode
51    ///
52    /// Maps container root (UID/GID 0) to current user
53    pub fn rootless() -> Self {
54        let uid = nix::unistd::getuid().as_raw();
55        let gid = nix::unistd::getgid().as_raw();
56
57        Self {
58            uid_mappings: vec![IdMapping::new(0, uid, 1)],
59            gid_mappings: vec![IdMapping::new(0, gid, 1)],
60        }
61    }
62
63    /// Create config with custom mappings
64    pub fn custom(uid_mappings: Vec<IdMapping>, gid_mappings: Vec<IdMapping>) -> Self {
65        Self {
66            uid_mappings,
67            gid_mappings,
68        }
69    }
70}
71
72/// User namespace mapper
73///
74/// Handles UID/GID mapping for rootless container execution
75pub struct UserNamespaceMapper {
76    config: UserNamespaceConfig,
77}
78
79impl UserNamespaceMapper {
80    pub fn new(config: UserNamespaceConfig) -> Self {
81        Self { config }
82    }
83
84    /// Setup UID/GID mappings for the current process
85    ///
86    /// This must be called after unshare(CLONE_NEWUSER) and before any other
87    /// namespace operations
88    pub fn setup_mappings(&self) -> Result<()> {
89        info!("Setting up user namespace mappings");
90
91        // Disable setgroups to allow GID mapping without CAP_SETGID
92        self.write_setgroups_deny()?;
93
94        // Write UID mappings
95        self.write_uid_map()?;
96
97        // Write GID mappings
98        self.write_gid_map()?;
99
100        info!("Successfully configured user namespace mappings");
101        Ok(())
102    }
103
104    /// Write to /proc/self/setgroups to deny setgroups(2)
105    ///
106    /// This is required for unprivileged user namespace mapping
107    fn write_setgroups_deny(&self) -> Result<()> {
108        let path = "/proc/self/setgroups";
109        debug!("Writing 'deny' to {}", path);
110
111        fs::write(path, "deny\n").map_err(|e| {
112            NucleusError::NamespaceError(format!("Failed to write to {}: {}", path, e))
113        })?;
114
115        Ok(())
116    }
117
118    /// Write UID mappings to /proc/self/uid_map
119    fn write_uid_map(&self) -> Result<()> {
120        let path = "/proc/self/uid_map";
121        let mut content = String::new();
122
123        for mapping in &self.config.uid_mappings {
124            content.push_str(&mapping.format());
125        }
126
127        debug!("Writing UID mappings to {}: {}", path, content.trim());
128
129        fs::write(path, &content).map_err(|e| {
130            NucleusError::NamespaceError(format!("Failed to write UID mappings: {}", e))
131        })?;
132
133        Ok(())
134    }
135
136    /// Write GID mappings to /proc/self/gid_map
137    fn write_gid_map(&self) -> Result<()> {
138        let path = "/proc/self/gid_map";
139        let mut content = String::new();
140
141        for mapping in &self.config.gid_mappings {
142            content.push_str(&mapping.format());
143        }
144
145        debug!("Writing GID mappings to {}: {}", path, content.trim());
146
147        fs::write(path, &content).map_err(|e| {
148            NucleusError::NamespaceError(format!("Failed to write GID mappings: {}", e))
149        })?;
150
151        Ok(())
152    }
153
154    /// Get the user namespace configuration
155    pub fn config(&self) -> &UserNamespaceConfig {
156        &self.config
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_id_mapping_format() {
166        let mapping = IdMapping::new(0, 1000, 1);
167        assert_eq!(mapping.format(), "0 1000 1\n");
168
169        let mapping = IdMapping::new(1000, 2000, 100);
170        assert_eq!(mapping.format(), "1000 2000 100\n");
171    }
172
173    #[test]
174    fn test_id_mapping_rootless() {
175        let mapping = IdMapping::rootless();
176        assert_eq!(mapping.container_id, 0);
177        assert_eq!(mapping.count, 1);
178        // host_id will be the current UID
179    }
180
181    #[test]
182    fn test_user_namespace_config_rootless() {
183        let config = UserNamespaceConfig::rootless();
184        assert_eq!(config.uid_mappings.len(), 1);
185        assert_eq!(config.gid_mappings.len(), 1);
186        assert_eq!(config.uid_mappings[0].container_id, 0);
187        assert_eq!(config.gid_mappings[0].container_id, 0);
188    }
189
190    #[test]
191    fn test_user_namespace_config_custom() {
192        let uid_mappings = vec![IdMapping::new(0, 1000, 1), IdMapping::new(1000, 2000, 100)];
193        let gid_mappings = vec![IdMapping::new(0, 1000, 1)];
194
195        let config = UserNamespaceConfig::custom(uid_mappings.clone(), gid_mappings.clone());
196        assert_eq!(config.uid_mappings, uid_mappings);
197        assert_eq!(config.gid_mappings, gid_mappings);
198    }
199
200    // Note: Testing actual mapping setup requires user namespace creation
201    // This is tested in integration tests
202}