Open6

Rustで名前空間の検証(unshareコマンドの再実装)

ryohryoh

まずはRustのインストール
公式サイトの記載があるようにコマンドを実行するだけ。
https://www.rust-lang.org/ja/tools/install

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

あと追加でやっておくこと
Ubuntuの場合

apt install build-essential clang pkg-config libssl-dev

インストール後、Rust環境の整備

echo 'source $HOME/.cargo/env' >> ~/.bashrc
source $HOME/.cargo/env

cargo install cargo-edit
cargo install cargo-crev
ryohryoh

namespaceの確認をしたいため nix crate を使って、疑似unshareを実施

Cargo.toml

[package]
name = "nstest"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0"
nix = "0.23"
sys-mount = "1.3"

main.rs

use std::ffi::CString;

use anyhow::Result;
use nix::sched::{unshare, CloneFlags};
use nix::sys::wait::waitpid;
use nix::unistd::{execvp, fork, getpid, getuid, ForkResult};

fn main() -> Result<()> {
    // Start program
    println!("Main PID: {}, User: {}", getpid(), getuid());

    // unshare start
    unshare(
        CloneFlags::CLONE_NEWCGROUP
            | CloneFlags::CLONE_NEWIPC
            | CloneFlags::CLONE_NEWNET
            | CloneFlags::CLONE_NEWNS
            | CloneFlags::CLONE_NEWPID
            | CloneFlags::CLONE_NEWUSER
            | CloneFlags::CLONE_NEWUTS,
    )
    .expect("failed unshare");

    println!("Under unshare PID: {} User: {}", getpid(), getuid());

    // fork start (for run command)
    match unsafe { fork().expect("failed fork") } {
        ForkResult::Parent { child } => {
            println!(
                "I'm parent. PID: {} User: {} Child: {}",
                getpid(),
                getuid(),
                child
            );

            // wait child process
            waitpid(child, None).expect("failed waitpid");

            println!("exit");
        }
        ForkResult::Child => {
            println!("I'm child. PID: {} User: {}", getpid(), getuid());

            // Run command
            let path = CString::new("/bin/echo").expect("CString::new failed");
            let argv =
                ["echo", "Hello", "World"].map(|s| CString::new(s).expect("CString::new failed"));

            execvp(&path, &argv).expect("failed execv");
        }
    }

    Ok(())
}

実行結果

ryoh@dormouse:~/nstest$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/nstest`
Main PID: 343, User: 1000
Under unshare PID: 343 User: 65534
I'm parent. PID: 343 User: 65534 Child: 345
I'm child. PID: 1 User: 65534
Hello World
exit
ryohryoh

nix::mount::mount で次のようなコードが通らない

mount(None, "/", None, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None)?;
ryoh@dormouse:~/rushare$ cargo run -- -mnuipCU -- cat /proc/self/mountinfo
   Compiling rushare v0.1.0 (/home/ryoh/rushare)
error[E0283]: type annotations needed
   --> src/main.rs:123:5
    |
123 |     mount(None, "/", None, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None)?;
    |     ^^^^^ cannot infer type for type parameter `P3` declared on the function `mount`
    |
    = note: cannot satisfy `_: NixPath`
note: required by a bound in `nix::mount::mount`
   --> /home/ryoh/.cargo/registry/src/github.com-1ecc6299db9ec823/nix-0.23.1/src/mount/linux.rs:59:71
    |
59  | pub fn mount<P1: ?Sized + NixPath, P2: ?Sized + NixPath, P3: ?Sized + NixPath, P4: ?Sized + NixPath>(
    |                                                                       ^^^^^^^ required by this bound in `nix::mount::mount`
help: consider specifying the type arguments in the function call
    |
123 |     mount::<P1, P2, P3, P4>(Some("none"), "/", None, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None)?;
    |          ++++++++++++++++++

For more information about this error, try `rustc --explain E0283`.
error: could not compile `rushare` due to previous error

次のサイトより、NixPathの型を指定する必要がありそう。(Someだと型情報があるがNoneの場合は型情報がないため型推論ができない)
https://stackoverflow.com/questions/64693079/how-to-use-nixmountmount-replace-with-libcmount-in-rust

次の形に直して、ビルドが通るようになった

mount::<Path, Path, Path, Path>(None, Path::new("/"), None, MsFlags::MS_REC | MsFlags::MS_PRIVATE, None)?;
ryohryoh

一旦作成したかった機能は実装

    // To immutable
    let unshare_flags = unshare_flags;

    // command building
    let path = match opt.prog {
        // Case no value
        None => env::var("SHELL").unwrap_or("/bin/sh".to_string()),
        // Case program value exists
        Some(prog) => prog,
    };
    let path = CString::new(path).expect("CString::new error");

    let mut argv: Vec<CString> = opt.args.iter()
        .map(|s| CString::new(s.as_str()).expect("CString::new error"))
        .collect();
    argv.insert(0, path.clone());


    // unshare and run command
    unsafe { signal(signal::SIGCHLD, signal::SigHandler::SigDfl) }?;

    // unshare
    unshare(unshare_flags).with_context(|| format!("{}: unshare failed", progname))?;

    // fork
    if opt.fork {
        unsafe{ signal(signal::SIGINT, signal::SigHandler::SigIgn) }?;
        unsafe{ signal(signal::SIGTERM, signal::SigHandler::SigIgn) }?;

        match unsafe{ fork().with_context(|| format!("{}: fork failed", progname))? } {
            ForkResult::Parent { child } => {
                let status = waitpid(child, None).with_context(|| format!("{}: waitpid failed", progname))?;

                unsafe{ signal(signal::SIGINT, signal::SigHandler::SigDfl) }?;
                unsafe{ signal(signal::SIGTERM, signal::SigHandler::SigDfl) }?;

                match status {
                    WaitStatus::Exited(_pid, status) => {
                        exit(status);
                    }
                    WaitStatus::Signaled(pid, signal, _) => {
                        kill(pid, signal)?;
                    }
                    _ => {
                        eprintln!("child exit failed");
                    }
                }
                exit(EXIT_FAILURE);
            }
            ForkResult::Child => {
                println!("forked.");
            }
        }
    }

    if opt.mount {
        // change propagation
        mount::<str, str, str, str>(None, "/", None, MsFlags::MS_PRIVATE | MsFlags::MS_REC, None)
            .with_context(|| format!("{}: change propagation failed", progname))?;
    }

    if opt.mount_proc {
        // proc mount
        mount::<str, str, str, str>(Some("proc"), "/proc", Some("proc"), MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV | MsFlags::MS_NOATIME, None)
            .with_context(|| format!("{}: proc remount failed", progname))?;
    }

    // execvp
    execvp(&path, &argv).with_context(|| format!("{}: execvp failed", progname))?;