1use crate::error::{NucleusError, Result};
2use std::fs;
3use tracing::{debug, info};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct IdMapping {
10 pub container_id: u32,
12 pub host_id: u32,
14 pub count: u32,
16}
17
18impl IdMapping {
19 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 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 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 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 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 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 pub fn rootless() -> Self {
76 let uid = nix::unistd::getuid().as_raw();
77 Self::new(0, uid, 1)
78 }
79
80 fn format(&self) -> String {
82 format!("{} {} {}\n", self.container_id, self.host_id, self.count)
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct UserNamespaceConfig {
89 pub uid_mappings: Vec<IdMapping>,
91 pub gid_mappings: Vec<IdMapping>,
93}
94
95impl UserNamespaceConfig {
96 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 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 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
139pub struct UserNamespaceMapper {
143 config: UserNamespaceConfig,
144}
145
146impl UserNamespaceMapper {
147 pub fn new(config: UserNamespaceConfig) -> Self {
148 Self { config }
149 }
150
151 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 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 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 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 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 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 }
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 }