Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,8 @@ name = "test-signalfd"
path = "test/test_signalfd.rs"
harness = false
test = true

[[test]]
name = "test-mount"
path = "test/test_mount.rs"
harness = false
15 changes: 15 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,21 @@ impl NixPath for PathBuf {
}
}

/// Treats `None` as an empty string.
impl<'a, NP: ?Sized + NixPath> NixPath for Option<&'a NP> {
fn len(&self) -> usize {
self.map_or(0, NixPath::len)
}

fn with_nix_path<T, F>(&self, f: F) -> Result<T> where F: FnOnce(&CStr) -> T {
if let Some(nix_path) = *self {
nix_path.with_nix_path(f)
} else {
unsafe { CStr::from_ptr("\0".as_ptr() as *const _).with_nix_path(f) }
}
}
}

#[inline]
pub fn from_ffi(res: libc::c_int) -> Result<()> {
if res != 0 {
Expand Down
21 changes: 7 additions & 14 deletions src/mount.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,25 @@ bitflags!(
);

mod ffi {
use libc::{c_char, c_int};
use libc::{c_char, c_int, c_ulong, c_void};

extern {
/*
* TODO: Bring back
pub fn mount(
source: *const c_char,
target: *const c_char,
fstype: *const c_char,
flags: c_ulong,
data: *const c_void) -> c_int;
*/

pub fn umount(target: *const c_char) -> c_int;

pub fn umount2(target: *const c_char, flags: c_int) -> c_int;
}
}

/*
* TODO: Bring this back with a test
*
pub fn mount<P1: ?Sized + NixPath, P2: ?Sized + NixPath, P3: ?Sized + NixPath, P4: ?Sized + NixPath>(
source: Option<&P1>,
target: P2,
target: &P2,
fstype: Option<&P3>,
flags: MsFlags,
data: Option<&P4>) -> Result<()> {
Expand All @@ -85,20 +79,19 @@ pub fn mount<P1: ?Sized + NixPath, P2: ?Sized + NixPath, P3: ?Sized + NixPath, P
fstype.with_nix_path(|fstype| {
data.with_nix_path(|data| {
unsafe {
ffi::mount(source.as_ext_str(),
target.as_ext_str(),
fstype,
ffi::mount(source.as_ptr(),
target.as_ptr(),
fstype.as_ptr(),
flags.bits,
data as *const libc::c_void)
data.as_ptr() as *const libc::c_void)
}
})
})
})
})))));

return from_ffi(res);
from_ffi(res)
}
*/

pub fn umount<P: ?Sized + NixPath>(target: &P) -> Result<()> {
let res = try!(target.with_nix_path(|cstr| {
Expand Down
222 changes: 222 additions & 0 deletions test/test_mount.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
// Impelmentation note: to allow unprivileged users to run it, this test makes
// use of user and mount namespaces. On systems that allow unprivileged user
// namespaces (Linux >= 3.8 compiled with CONFIG_USER_NS), the test should run
// without root.

extern crate libc;
extern crate nix;
extern crate tempdir;

#[cfg(target_os = "linux")]
mod test_mount {
use std::fs::{self, File};
use std::io::{Read, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::fs::PermissionsExt;
use std::process::Command;

use libc::{self, EACCES, EROFS};

use nix::mount::{mount, umount, MsFlags, MS_BIND, MS_RDONLY, MS_NOEXEC};
use nix::sched::{unshare, CLONE_NEWNS, CLONE_NEWUSER};
use nix::sys::stat::{self, S_IRWXU, S_IRWXG, S_IRWXO, S_IXUSR, S_IXGRP, S_IXOTH};

use tempdir::TempDir;

static SCRIPT_CONTENTS: &'static [u8] = b"#!/bin/sh
exit 23";

const EXPECTED_STATUS: i32 = 23;

const NONE: Option<&'static [u8]> = None;
pub fn test_mount_tmpfs_without_flags_allows_rwx() {
let tempdir = TempDir::new("nix-test_mount")
.unwrap_or_else(|e| panic!("tempdir failed: {}", e));

mount(NONE,
tempdir.path(),
Some(b"tmpfs".as_ref()),
MsFlags::empty(),
NONE)
.unwrap_or_else(|e| panic!("mount failed: {}", e));

let test_path = tempdir.path().join("test");

// Verify write.
fs::OpenOptions::new()
.create(true)
.write(true)
.mode((S_IRWXU | S_IRWXG | S_IRWXO).bits())
.open(&test_path)
.and_then(|mut f| f.write(SCRIPT_CONTENTS))
.unwrap_or_else(|e| panic!("write failed: {}", e));

// Verify read.
let mut buf = Vec::new();
File::open(&test_path)
.and_then(|mut f| f.read_to_end(&mut buf))
.unwrap_or_else(|e| panic!("read failed: {}", e));
assert_eq!(buf, SCRIPT_CONTENTS);

// Verify execute.
assert_eq!(EXPECTED_STATUS,
Command::new(&test_path)
.status()
.unwrap_or_else(|e| panic!("exec failed: {}", e))
.code()
.unwrap_or_else(|| panic!("child killed by signal")));

umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
}

pub fn test_mount_rdonly_disallows_write() {
let tempdir = TempDir::new("nix-test_mount")
.unwrap_or_else(|e| panic!("tempdir failed: {}", e));

mount(NONE,
tempdir.path(),
Some(b"tmpfs".as_ref()),
MS_RDONLY,
NONE)
.unwrap_or_else(|e| panic!("mount failed: {}", e));

// EROFS: Read-only file system
assert_eq!(EROFS as i32,
File::create(tempdir.path().join("test")).unwrap_err().raw_os_error().unwrap());

umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
}

pub fn test_mount_noexec_disallows_exec() {
let tempdir = TempDir::new("nix-test_mount")
.unwrap_or_else(|e| panic!("tempdir failed: {}", e));

mount(NONE,
tempdir.path(),
Some(b"tmpfs".as_ref()),
MS_NOEXEC,
NONE)
.unwrap_or_else(|e| panic!("mount failed: {}", e));

let test_path = tempdir.path().join("test");

fs::OpenOptions::new()
.create(true)
.write(true)
.mode((S_IRWXU | S_IRWXG | S_IRWXO).bits())
.open(&test_path)
.and_then(|mut f| f.write(SCRIPT_CONTENTS))
.unwrap_or_else(|e| panic!("write failed: {}", e));

// Verify that we cannot execute despite a+x permissions being set.
let mode = stat::Mode::from_bits_truncate(fs::metadata(&test_path)
.map(|md| md.permissions().mode())
.unwrap_or_else(|e| {
panic!("metadata failed: {}", e)
}));

assert!(mode.contains(S_IXUSR | S_IXGRP | S_IXOTH),
"{:?} did not have execute permissions",
&test_path);

// EACCES: Permission denied
assert_eq!(EACCES as i32,
Command::new(&test_path).status().unwrap_err().raw_os_error().unwrap());

umount(tempdir.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
}

pub fn test_mount_bind() {
use std::env;
if env::var("CI").is_ok() && env::var("TRAVIS").is_ok() {
print!("Travis does not allow bind mounts, skipping.");
return;
}

let tempdir = TempDir::new("nix-test_mount")
.unwrap_or_else(|e| panic!("tempdir failed: {}", e));
let file_name = "test";

{
let mount_point = TempDir::new("nix-test_mount")
.unwrap_or_else(|e| panic!("tempdir failed: {}", e));

mount(Some(tempdir.path()),
mount_point.path(),
NONE,
MS_BIND,
NONE)
.unwrap_or_else(|e| panic!("mount failed: {}", e));

fs::OpenOptions::new()
.create(true)
.write(true)
.mode((S_IRWXU | S_IRWXG | S_IRWXO).bits())
.open(mount_point.path().join(file_name))
.and_then(|mut f| f.write(SCRIPT_CONTENTS))
.unwrap_or_else(|e| panic!("write failed: {}", e));

umount(mount_point.path()).unwrap_or_else(|e| panic!("umount failed: {}", e));
}

// Verify the file written in the mount shows up in source directory, even
// after unmounting.

let mut buf = Vec::new();
File::open(tempdir.path().join(file_name))
.and_then(|mut f| f.read_to_end(&mut buf))
.unwrap_or_else(|e| panic!("read failed: {}", e));
assert_eq!(buf, SCRIPT_CONTENTS);
}

pub fn setup_namespaces() {
// Hold on to the uid in the parent namespace.
let uid = unsafe { libc::getuid() };

unshare(CLONE_NEWNS | CLONE_NEWUSER).unwrap_or_else(|e| {
panic!("unshare failed: {}. Are unprivileged user namespaces available?",
e)
});

// Map user as uid 1000.
fs::OpenOptions::new()
.write(true)
.open("/proc/self/uid_map")
.and_then(|mut f| f.write(format!("1000 {} 1\n", uid).as_bytes()))
.unwrap_or_else(|e| panic!("could not write uid map: {}", e));
}
}


// Test runner

/// Mimic normal test output (hackishly).
macro_rules! run_tests {
( $($test_fn:ident),* ) => {{
print!("\n");

$(
print!("test test_mount::{} ... ", stringify!($test_fn));
$test_fn();
print!("ok\n");
)*

print!("\n");
}}
}

#[cfg(target_os = "linux")]
fn main() {
use test_mount::{setup_namespaces, test_mount_tmpfs_without_flags_allows_rwx,
test_mount_rdonly_disallows_write, test_mount_noexec_disallows_exec,
test_mount_bind};
setup_namespaces();

run_tests!(test_mount_tmpfs_without_flags_allows_rwx,
test_mount_rdonly_disallows_write,
test_mount_noexec_disallows_exec,
test_mount_bind);
}

#[cfg(not(target_os = "linux"))]
fn main() {}