Skip to main content

linuxutils_system/
unshare.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use rustix::{
7    mount::{self, MountFlags, MountPropagationFlags},
8    process,
9    thread::UnshareFlags,
10};
11use std::{
12    env,
13    ffi::CString,
14    fs, io,
15    os::unix::process::CommandExt,
16    process::{Command, ExitCode},
17};
18
19#[derive(Parser)]
20#[command(name = "unshare", about = "Run a program in new namespaces")]
21pub struct Args {
22    /// Unshare mount namespace
23    #[arg(short = 'm', long)]
24    mount: bool,
25
26    /// Unshare UTS namespace (hostname/domainname)
27    #[arg(short = 'u', long)]
28    uts: bool,
29
30    /// Unshare IPC namespace
31    #[arg(short = 'i', long)]
32    ipc: bool,
33
34    /// Unshare network namespace
35    #[arg(short = 'n', long)]
36    net: bool,
37
38    /// Unshare PID namespace
39    #[arg(short = 'p', long)]
40    pid: bool,
41
42    /// Unshare user namespace
43    #[arg(short = 'U', long)]
44    user: bool,
45
46    /// Unshare cgroup namespace
47    #[arg(short = 'C', long)]
48    cgroup: bool,
49
50    /// Unshare time namespace
51    #[arg(short = 'T', long)]
52    time: bool,
53
54    /// Fork before executing the program
55    #[arg(short = 'f', long)]
56    fork: bool,
57
58    /// Map current user to root in user namespace (implies --user)
59    #[arg(short = 'r', long = "map-root-user")]
60    map_root_user: bool,
61
62    /// Map current user to same UID/GID in user namespace (implies --user)
63    #[arg(short = 'c', long = "map-current-user")]
64    map_current_user: bool,
65
66    /// Mount /proc filesystem (implies --mount)
67    #[arg(long = "mount-proc", num_args = 0..=1, default_missing_value = "/proc")]
68    mount_proc: Option<String>,
69
70    /// Set mount propagation (private, shared, slave, unchanged)
71    #[arg(long, default_value = "private")]
72    propagation: String,
73
74    /// Set root directory
75    #[arg(short = 'R', long = "root")]
76    root_dir: Option<String>,
77
78    /// Set working directory
79    #[arg(short = 'w', long = "wd")]
80    work_dir: Option<String>,
81
82    /// Set UID in entered namespace
83    #[arg(short = 'S', long = "setuid")]
84    set_uid: Option<u32>,
85
86    /// Set GID in entered namespace
87    #[arg(short = 'G', long = "setgid")]
88    set_gid: Option<u32>,
89
90    /// Allow or deny setgroups in user namespace (allow, deny)
91    #[arg(long = "setgroups")]
92    setgroups: Option<String>,
93
94    /// Program and arguments to run
95    #[arg(trailing_var_arg = true)]
96    command: Vec<String>,
97}
98
99fn build_unshare_flags(args: &Args) -> UnshareFlags {
100    let mut flags = UnshareFlags::empty();
101    if args.mount || args.mount_proc.is_some() {
102        flags |= UnshareFlags::NEWNS;
103    }
104    if args.uts {
105        flags |= UnshareFlags::NEWUTS;
106    }
107    if args.ipc {
108        flags |= UnshareFlags::NEWIPC;
109    }
110    if args.net {
111        flags |= UnshareFlags::NEWNET;
112    }
113    if args.pid {
114        flags |= UnshareFlags::NEWPID;
115    }
116    if args.user || args.map_root_user || args.map_current_user {
117        flags |= UnshareFlags::NEWUSER;
118    }
119    if args.cgroup {
120        flags |= UnshareFlags::NEWCGROUP;
121    }
122    if args.time {
123        flags |= UnshareFlags::NEWTIME;
124    }
125    flags
126}
127
128fn map_user(
129    target_uid: u32,
130    target_gid: u32,
131    real_uid: u32,
132    real_gid: u32,
133    deny_setgroups: bool,
134) -> io::Result<()> {
135    if deny_setgroups {
136        fs::write("/proc/self/setgroups", "deny")?;
137    }
138
139    fs::write("/proc/self/uid_map", format!("{target_uid} {real_uid} 1\n"))?;
140    fs::write("/proc/self/gid_map", format!("{target_gid} {real_gid} 1\n"))?;
141
142    Ok(())
143}
144
145fn set_propagation(prop: &str) -> io::Result<()> {
146    let flags = match prop {
147        "private" => {
148            MountPropagationFlags::PRIVATE | MountPropagationFlags::REC
149        }
150        "shared" => MountPropagationFlags::SHARED | MountPropagationFlags::REC,
151        "slave" => {
152            MountPropagationFlags::DOWNSTREAM | MountPropagationFlags::REC
153        }
154        "unchanged" => return Ok(()),
155        other => {
156            return Err(io::Error::new(
157                io::ErrorKind::InvalidInput,
158                format!("unknown propagation type: {other}"),
159            ));
160        }
161    };
162    mount::mount_change("/", flags).map_err(io::Error::from)
163}
164
165fn do_mount_proc(target: &str) -> io::Result<()> {
166    let target_c = CString::new(target)
167        .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
168    let proc_c = c"proc";
169    mount::mount(proc_c, &*target_c, proc_c, MountFlags::empty(), None)
170        .map_err(io::Error::from)
171}
172
173fn do_setuid(uid: u32) -> io::Result<()> {
174    if unsafe { libc::setuid(uid) } != 0 {
175        Err(io::Error::last_os_error())
176    } else {
177        Ok(())
178    }
179}
180
181fn do_setgid(gid: u32) -> io::Result<()> {
182    if unsafe { libc::setgid(gid) } != 0 {
183        Err(io::Error::last_os_error())
184    } else {
185        Ok(())
186    }
187}
188
189fn get_shell() -> String {
190    env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string())
191}
192
193fn child_setup(
194    root_dir: &Option<String>,
195    work_dir: &Option<String>,
196    mount_proc: &Option<String>,
197    set_gid: Option<u32>,
198    set_uid: Option<u32>,
199) -> io::Result<()> {
200    if let Some(dir) = root_dir {
201        std::os::unix::fs::chroot(dir)?;
202        env::set_current_dir("/")?;
203    }
204    if let Some(dir) = work_dir {
205        env::set_current_dir(dir)?;
206    }
207    if let Some(target) = mount_proc {
208        do_mount_proc(target)?;
209    }
210    if let Some(gid) = set_gid {
211        do_setgid(gid)?;
212    }
213    if let Some(uid) = set_uid {
214        do_setuid(uid)?;
215    }
216    Ok(())
217}
218
219pub fn run(args: Args) -> ExitCode {
220    let flags = build_unshare_flags(&args);
221
222    if flags.is_empty() {
223        eprintln!("unshare: no namespaces specified");
224        return ExitCode::FAILURE;
225    }
226
227    // Capture real UID/GID before unshare, since after unshare(NEWUSER)
228    // getuid()/getgid() return the unmapped overflow values (65534).
229    let real_uid = process::getuid().as_raw();
230    let real_gid = process::getgid().as_raw();
231
232    // SAFETY: We're a single-threaded CLI tool at this point, and we're not
233    // using CLONE_FILES which is the main footgun for unshare in threaded programs.
234    if let Err(e) = unsafe { rustix::thread::unshare_unsafe(flags) } {
235        eprintln!("unshare: unshare failed: {}", io::Error::from(e));
236        return ExitCode::FAILURE;
237    }
238
239    // Set up user namespace mapping
240    if args.map_root_user {
241        if let Err(e) = map_user(
242            0,
243            0,
244            real_uid,
245            real_gid,
246            args.setgroups.as_deref() != Some("allow"),
247        ) {
248            eprintln!("unshare: failed to map root user: {e}");
249            return ExitCode::FAILURE;
250        }
251    } else if args.map_current_user {
252        if let Err(e) = map_user(
253            real_uid,
254            real_gid,
255            real_uid,
256            real_gid,
257            args.setgroups.as_deref() != Some("allow"),
258        ) {
259            eprintln!("unshare: failed to map current user: {e}");
260            return ExitCode::FAILURE;
261        }
262    } else if let Some(ref val) = args.setgroups
263        && flags.contains(UnshareFlags::NEWUSER)
264        && let Err(e) = fs::write("/proc/self/setgroups", val)
265    {
266        eprintln!("unshare: failed to set setgroups: {e}");
267        return ExitCode::FAILURE;
268    }
269
270    // Set mount propagation
271    if flags.contains(UnshareFlags::NEWNS)
272        && args.propagation != "unchanged"
273        && let Err(e) = set_propagation(&args.propagation)
274    {
275        eprintln!("unshare: failed to set propagation: {e}");
276        return ExitCode::FAILURE;
277    }
278
279    let program = if args.command.is_empty() {
280        get_shell()
281    } else {
282        args.command[0].clone()
283    };
284    let program_args: Vec<&str> = if args.command.len() > 1 {
285        args.command[1..].iter().map(|s| s.as_str()).collect()
286    } else {
287        vec![]
288    };
289
290    if args.fork || args.pid {
291        let root_dir = args.root_dir.clone();
292        let work_dir = args.work_dir.clone();
293        let mount_proc = args.mount_proc.clone();
294        let set_uid = args.set_uid;
295        let set_gid = args.set_gid;
296
297        let status = unsafe {
298            Command::new(&program)
299                .args(&program_args)
300                .pre_exec(move || {
301                    child_setup(
302                        &root_dir,
303                        &work_dir,
304                        &mount_proc,
305                        set_gid,
306                        set_uid,
307                    )
308                })
309                .status()
310        };
311
312        match status {
313            Ok(s) => ExitCode::from(s.code().unwrap_or(1) as u8),
314            Err(e) => {
315                eprintln!("unshare: failed to execute {program}: {e}");
316                ExitCode::FAILURE
317            }
318        }
319    } else {
320        if let Err(e) = child_setup(
321            &args.root_dir,
322            &args.work_dir,
323            &args.mount_proc,
324            args.set_gid,
325            args.set_uid,
326        ) {
327            eprintln!("unshare: setup failed: {e}");
328            return ExitCode::FAILURE;
329        }
330
331        let err = Command::new(&program).args(&program_args).exec();
332        eprintln!("unshare: failed to execute {program}: {err}");
333        ExitCode::FAILURE
334    }
335}