#![cfg(target_os = "linux")]
use compress_tools::{uncompress_archive, Ownership};
use std::{
error::Error,
ffi::CString,
fs::{self, File},
io::Write,
os::unix::ffi::OsStrExt,
path::{Path, PathBuf},
process,
};
const SKIPPED: i32 = 77;
const BUG_REPRODUCED: i32 = 1;
const OK: i32 = 0;
#[test]
fn uncompress_archive_errors_when_target_is_full() {
let scratch = tempfile::tempdir().expect("tempdir");
let archive = build_oversize_archive(scratch.path());
let target = scratch.path().join("target");
fs::create_dir(&target).unwrap();
unsafe {
match libc::fork() {
-1 => panic!("fork failed: {}", std::io::Error::last_os_error()),
0 => run_child(&archive, &target),
pid => {
let mut status: libc::c_int = 0;
let waited = libc::waitpid(pid, &mut status, 0);
assert_eq!(
waited,
pid,
"waitpid failed: {}",
std::io::Error::last_os_error()
);
assert!(
libc::WIFEXITED(status),
"child did not exit normally (raw status = {status})"
);
match libc::WEXITSTATUS(status) {
OK => {}
SKIPPED => {
eprintln!(
"skipping: unprivileged user namespaces or tmpfs \
mount not available in this environment"
);
}
BUG_REPRODUCED => panic!(
"uncompress_archive returned Ok despite the target \
partition being full — see issue #142"
),
other => panic!("child exited with unexpected code {other}"),
}
}
}
}
}
unsafe fn run_child(archive: &Path, target: &Path) -> ! {
let uid = libc::getuid();
let gid = libc::getgid();
if libc::unshare(libc::CLONE_NEWUSER | libc::CLONE_NEWNS) != 0 {
eprintln!("child: unshare failed: {}", std::io::Error::last_os_error());
process::exit(SKIPPED);
}
if !write_proc("/proc/self/uid_map", &format!("0 {uid} 1"))
|| !write_proc("/proc/self/setgroups", "deny")
|| !write_proc("/proc/self/gid_map", &format!("0 {gid} 1"))
{
process::exit(SKIPPED);
}
let slash = CString::new("/").unwrap();
let empty = CString::new("").unwrap();
libc::mount(
empty.as_ptr(),
slash.as_ptr(),
std::ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
std::ptr::null(),
);
let source = CString::new("tmpfs").unwrap();
let fstype = CString::new("tmpfs").unwrap();
let options = CString::new("size=4K").unwrap();
let target_c = CString::new(target.as_os_str().as_bytes()).unwrap();
if libc::mount(
source.as_ptr(),
target_c.as_ptr(),
fstype.as_ptr(),
0,
options.as_ptr() as *const _,
) != 0
{
eprintln!(
"child: mount tmpfs failed: {}",
std::io::Error::last_os_error()
);
process::exit(SKIPPED);
}
let mut src = match File::open(archive) {
Ok(f) => f,
Err(e) => {
eprintln!("child: open archive failed: {e}");
process::exit(SKIPPED);
}
};
let result = uncompress_archive(&mut src, target, Ownership::Ignore);
eprintln!("child: uncompress_archive result = {result:?}");
let errno = result.as_ref().err().and_then(|e| {
e.source()
.and_then(|s| s.downcast_ref::<std::io::Error>())
.and_then(std::io::Error::raw_os_error)
});
process::exit(if errno == Some(libc::ENOSPC) {
OK
} else {
BUG_REPRODUCED
});
}
unsafe fn write_proc(path: &str, content: &str) -> bool {
match File::options().write(true).open(path) {
Ok(mut f) => match f.write_all(content.as_bytes()) {
Ok(()) => true,
Err(e) => {
eprintln!("child: write to {path} failed: {e}");
false
}
},
Err(e) => {
eprintln!("child: open {path} failed: {e}");
false
}
}
}
fn build_oversize_archive(dir: &Path) -> PathBuf {
let src = dir.join("payload");
fs::create_dir_all(&src).unwrap();
fs::write(src.join("a.bin"), vec![0xaa_u8; 8192]).unwrap();
fs::write(src.join("b.bin"), vec![0xbb_u8; 8192]).unwrap();
let archive = dir.join("oversize.tar");
let status = process::Command::new("tar")
.arg("-cf")
.arg(&archive)
.arg("-C")
.arg(&src)
.arg(".")
.status()
.expect("tar command must be available on Linux test hosts");
assert!(status.success(), "tar invocation failed");
archive
}