use std::env;
use std::ffi::CString;
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::net::TcpStream;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::FileExt;
use std::process::Command;
use std::sync::{Arc, Barrier};
use std::thread;
#[repr(C)]
struct OpenHow {
flags: u64,
mode: u64,
resolve: u64,
}
fn main() {
let mut args = env::args().skip(1);
match args.next().as_deref() {
Some("noop") => {}
Some("spawn") => {
let status = Command::new("/bin/true").status().expect("spawn /bin/true");
assert!(status.success());
}
Some("file") => {
let path = args.next().expect("file path");
let _ = fs::read(path).expect("read fixture file");
}
Some("file-rw") => {
let path = args.next().expect("file path");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path)
.expect("open fixture file");
let mut byte = [0_u8; 1];
file.read_exact(&mut byte).expect("read fixture byte");
file.seek(SeekFrom::End(0)).expect("seek end");
file.write_all(b"x").expect("write fixture byte");
file.flush().expect("flush");
}
Some("relative-file") => {
let dir = args.next().expect("directory");
let name = args.next().expect("relative file name");
env::set_current_dir(dir).expect("chdir");
let _ = fs::read(name).expect("read relative fixture file");
}
Some("read-fd") => {
let fd: i32 = args.next().expect("fd").parse().expect("numeric fd");
let mut byte = [0_u8; 1];
let result = unsafe { libc::read(fd, byte.as_mut_ptr().cast(), 1) };
assert_eq!(result, 1, "read inherited fd after exec");
}
Some("dup-read") => {
let path = args.next().expect("file path");
let file = File::open(path).expect("open");
let duplicated = unsafe { libc::dup(file.as_raw_fd()) };
assert!(duplicated >= 0);
let mut duplicated = unsafe { File::from_raw_fd(duplicated) };
let mut byte = [0_u8; 1];
duplicated
.read_exact(&mut byte)
.expect("read duplicated fd");
}
Some("fork-read") => {
let path = args.next().expect("file path");
let file = File::open(path).expect("open");
let fd = file.as_raw_fd();
let child = unsafe { libc::fork() };
assert!(child >= 0);
if child == 0 {
let mut byte = [0_u8; 1];
let result = unsafe { libc::read(fd, byte.as_mut_ptr().cast(), 1) };
unsafe { libc::_exit(if result == 1 { 0 } else { 1 }) };
}
let mut status = 0;
assert_eq!(unsafe { libc::waitpid(child, &mut status, 0) }, child);
assert!(libc::WIFEXITED(status));
assert_eq!(libc::WEXITSTATUS(status), 0);
}
Some("clone-private-read") => {
let path = args.next().expect("file path");
let file = File::open(path).expect("open");
let fd = file.as_raw_fd();
unsafe {
libc::signal(libc::SIGUSR1, libc::SIG_IGN);
}
let flags = libc::SIGUSR1 as libc::c_long;
let child = unsafe { libc::syscall(libc::SYS_clone, flags, 0, 0, 0, 0) } as libc::pid_t;
assert!(child >= 0, "private clone failed");
if child == 0 {
let mut byte = [0_u8; 1];
let result = unsafe { libc::read(fd, byte.as_mut_ptr().cast(), 1) };
unsafe { libc::_exit(if result == 1 { 0 } else { 1 }) };
}
let mut status = 0;
assert_eq!(
unsafe { libc::waitpid(child, &mut status, libc::__WCLONE) },
child,
"wait private clone"
);
assert!(libc::WIFEXITED(status));
assert_eq!(libc::WEXITSTATUS(status), 0);
}
Some("thread-read") => {
let path = args.next().expect("file path");
let count: usize = args.next().expect("thread count").parse().expect("count");
let file = Arc::new(File::open(path).expect("open"));
let mut handles = Vec::new();
for _ in 0..count {
let file = Arc::clone(&file);
handles.push(thread::spawn(move || {
let mut byte = [0_u8; 1];
file.read_at(&mut byte, 0).expect("pread shared fd");
}));
}
for handle in handles {
handle.join().expect("thread");
}
}
Some("thread-exec-fd") => {
let path = args.next().expect("file path");
let fd = File::open(path)
.expect("open exec-shared file")
.into_raw_fd();
let fd_flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
assert!(fd_flags >= 0, "F_GETFD");
assert_eq!(
unsafe { libc::fcntl(fd, libc::F_SETFD, fd_flags & !libc::FD_CLOEXEC) },
0,
"clear FD_CLOEXEC"
);
let handle = thread::spawn(move || {
let exe = env::current_exe().expect("current executable");
let exe = CString::new(exe.as_os_str().as_bytes()).expect("executable cstring");
let mode = CString::new("read-fd").expect("mode cstring");
let fd_arg = CString::new(fd.to_string()).expect("fd cstring");
let argv = [
exe.as_ptr(),
mode.as_ptr(),
fd_arg.as_ptr(),
std::ptr::null(),
];
unsafe {
libc::execv(exe.as_ptr(), argv.as_ptr());
libc::_exit(127);
}
});
handle.join().expect("exec thread unexpectedly returned");
panic!("thread exec unexpectedly returned without replacing the process");
}
Some("thread-fd-reuse") => {
let old_path = args.next().expect("old file path");
let new_path = args.next().expect("new file path");
let fd = File::open(old_path).expect("open old file").into_raw_fd();
let barrier = Arc::new(Barrier::new(2));
let replace_barrier = Arc::clone(&barrier);
let replace = thread::spawn(move || {
assert_eq!(unsafe { libc::close(fd) }, 0, "close old shared fd");
let replacement = File::open(new_path).expect("open replacement file");
let replacement_fd = replacement.into_raw_fd();
if replacement_fd != fd {
assert_eq!(
unsafe { libc::dup2(replacement_fd, fd) },
fd,
"dup2 replacement fd"
);
assert_eq!(
unsafe { libc::close(replacement_fd) },
0,
"close extra replacement fd"
);
}
replace_barrier.wait();
});
let read_barrier = Arc::clone(&barrier);
let read = thread::spawn(move || {
read_barrier.wait();
let mut byte = [0_u8; 1];
let result = unsafe { libc::read(fd, byte.as_mut_ptr().cast(), 1) };
assert_eq!(result, 1, "read reused shared fd");
});
replace.join().expect("replace thread");
read.join().expect("read thread");
assert_eq!(unsafe { libc::close(fd) }, 0, "close final shared fd");
}
Some("stage2-dup2-untracked-write") => {
let victim_path = args.next().expect("victim file path");
let victim = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(victim_path)
.expect("open victim file");
let victim_fd = victim.as_raw_fd();
let mut pipe_fds = [-1_i32; 2];
assert_eq!(
unsafe { libc::pipe(pipe_fds.as_mut_ptr()) },
0,
"create untracked pipe"
);
assert_eq!(
unsafe { libc::dup2(pipe_fds[1], victim_fd) },
victim_fd,
"dup2 untracked pipe over tracked victim fd"
);
let byte = *b"x";
assert_eq!(
unsafe { libc::write(victim_fd, byte.as_ptr().cast(), byte.len()) },
1,
"write through replaced destination fd"
);
assert_eq!(
unsafe { libc::close(pipe_fds[0]) },
0,
"close pipe read end"
);
assert_eq!(
unsafe { libc::close(pipe_fds[1]) },
0,
"close pipe write end"
);
}
Some("stage2-dup3-untracked-write") => {
let victim_path = args.next().expect("victim file path");
let victim = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(victim_path)
.expect("open victim file");
let victim_fd = victim.as_raw_fd();
let mut pipe_fds = [-1_i32; 2];
assert_eq!(
unsafe { libc::pipe(pipe_fds.as_mut_ptr()) },
0,
"create untracked pipe"
);
assert_eq!(
unsafe { libc::dup3(pipe_fds[1], victim_fd, libc::O_CLOEXEC) },
victim_fd,
"dup3 untracked pipe over tracked victim fd"
);
let byte = *b"x";
assert_eq!(
unsafe { libc::write(victim_fd, byte.as_ptr().cast(), byte.len()) },
1,
"write through dup3-replaced destination fd"
);
assert_eq!(
unsafe { libc::close(pipe_fds[0]) },
0,
"close pipe read end"
);
assert_eq!(
unsafe { libc::close(pipe_fds[1]) },
0,
"close pipe write end"
);
}
Some("stage2-fcntl-dupfd-untracked-write") => {
let mut pipe_fds = [-1_i32; 2];
assert_eq!(
unsafe { libc::pipe(pipe_fds.as_mut_ptr()) },
0,
"create untracked pipe"
);
let duplicated_fd = unsafe { libc::fcntl(pipe_fds[1], libc::F_DUPFD_CLOEXEC, 64) };
assert!(duplicated_fd >= 0, "F_DUPFD_CLOEXEC must succeed");
let byte = *b"x";
assert_eq!(
unsafe { libc::write(duplicated_fd, byte.as_ptr().cast(), byte.len()) },
1,
"write through fcntl-duplicated fd"
);
assert_eq!(
unsafe { libc::close(duplicated_fd) },
0,
"close duplicated fd"
);
assert_eq!(
unsafe { libc::close(pipe_fds[0]) },
0,
"close pipe read end"
);
assert_eq!(
unsafe { libc::close(pipe_fds[1]) },
0,
"close pipe write end"
);
}
Some("stage2-dup2-same-fd-write") => {
let path = args.next().expect("file path");
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
.expect("open same-fd target");
let fd = file.as_raw_fd();
assert_eq!(unsafe { libc::dup2(fd, fd) }, fd, "dup2 same fd");
let byte = *b"x";
assert_eq!(
unsafe { libc::write(fd, byte.as_ptr().cast(), byte.len()) },
1,
"write after dup2 same-fd no-op"
);
}
Some("stage2-stderr-write") => {
let byte = *b"x";
assert_eq!(
unsafe { libc::write(libc::STDERR_FILENO, byte.as_ptr().cast(), byte.len(),) },
1,
"write one byte to inherited stderr"
);
}
Some("stage2-shared-untracked-fd-write") => {
let worker = std::thread::spawn(|| {
let mut pipe_fds = [-1_i32; 2];
assert_eq!(
unsafe { libc::pipe(pipe_fds.as_mut_ptr()) },
0,
"create worker-local untracked pipe"
);
let byte = *b"x";
assert_eq!(
unsafe { libc::write(pipe_fds[1], byte.as_ptr().cast(), byte.len()) },
1,
"write through untracked fd while CLONE_FILES table is shared"
);
assert_eq!(
unsafe { libc::close(pipe_fds[0]) },
0,
"close pipe read end"
);
assert_eq!(
unsafe { libc::close(pipe_fds[1]) },
0,
"close pipe write end"
);
});
worker.join().expect("shared-fd worker");
}
Some("stage2-hardlink-after-open-write") => {
let target_path = args.next().expect("target path");
let alias_path = args.next().expect("alias path");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&target_path)
.expect("open target before hardlink creation");
fs::hard_link(&target_path, &alias_path).expect("create hardlink after open");
file.seek(SeekFrom::End(0)).expect("seek target end");
file.write_all(b"x").expect("write after hardlink creation");
file.flush().expect("flush write after hardlink creation");
}
Some("stage2-hardlink-remove-before-write") => {
let target_path = args.next().expect("target path");
let alias_path = args.next().expect("alias path");
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(&target_path)
.expect("open target before hardlink removal");
fs::remove_file(&alias_path).expect("remove hardlink before write");
file.seek(SeekFrom::End(0)).expect("seek target end");
file.write_all(b"x").expect("write after hardlink removal");
file.flush().expect("flush write after hardlink removal");
}
Some("stage2-symlink-truncate") => {
let link_path = args.next().expect("symlink path");
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(link_path)
.expect("open symlink target with truncate");
drop(file);
}
Some("burst") => {
let dir = args.next().expect("directory");
let count: usize = args.next().expect("count").parse().expect("count");
fs::create_dir_all(&dir).expect("create burst dir");
for index in 0..count {
let path = format!("{dir}/event-{index}.txt");
fs::write(&path, b"x").expect("burst write");
let _ = fs::read(&path).expect("burst read");
}
}
Some("openat2") => {
let path = args.next().expect("file path");
let path = CString::new(path).expect("cstring");
let how = OpenHow {
flags: libc::O_RDONLY as u64,
mode: 0,
resolve: 0,
};
let fd = unsafe {
libc::syscall(
libc::SYS_openat2,
libc::AT_FDCWD,
path.as_ptr(),
&how as *const OpenHow,
std::mem::size_of::<OpenHow>(),
)
} as i32;
assert!(
fd >= 0,
"openat2 failed: {}",
std::io::Error::last_os_error()
);
unsafe {
libc::close(fd);
}
}
Some("fault-path") => {
let result = unsafe {
libc::syscall(
libc::SYS_openat,
libc::AT_FDCWD,
1_usize as *const libc::c_char,
libc::O_RDONLY,
0,
)
};
assert_eq!(result, -1);
}
Some("network") => {
let address = args.next().expect("socket address");
let _stream = TcpStream::connect(address).expect("connect fixture listener");
}
other => panic!("unknown fixture mode: {other:?}"),
}
}