#![allow(clippy::disallowed_methods)]
#![allow(clippy::disallowed_types)]
use std::{
env,
error::Error,
ffi::OsStr,
fmt,
fs::{canonicalize, File},
io::Write,
net::{Ipv6Addr, SocketAddrV6, TcpListener},
os::fd::RawFd,
path::{Path, PathBuf},
process::{Child, Command, ExitStatus, Output, Stdio},
sync::LazyLock,
time::Duration,
};
use memchr::memmem;
use nix::{
errno::Errno,
sys::{
resource::{Resource, RLIM_INFINITY},
utsname::uname,
},
unistd::{isatty, Gid, Uid},
};
use syd::{config::*, confine::confine_rlimit};
#[derive(Debug)]
pub struct TestError(pub String);
pub type TestResult = Result<(), TestError>;
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<E: Error> From<E> for TestError {
fn from(err: E) -> Self {
TestError(err.to_string())
}
}
#[macro_export]
macro_rules! say {
($($arg:tt)*) => {{
let msg = format!($($arg)*);
let pre = "\r\x1b[K";
if *$crate::util::SYD_TEST_TTY {
eprint!("{pre}{msg}");
} else {
eprintln!("{msg}");
}
}};
}
#[macro_export]
macro_rules! assert {
($cond:expr) => {
if !$cond {
return Err(TestError(format!("Assertion failed: {}", stringify!($cond))));
}
};
($cond:expr, $($arg:tt)*) => {
if !$cond {
return Err(TestError(format!("Assertion failed: {}: {}", stringify!($cond), format_args!($($arg)*))));
}
};
}
#[macro_export]
macro_rules! assert_eq {
($left:expr, $right:expr) => {
if $left != $right {
return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
}
};
($left:expr, $right:expr, $($arg:tt)*) => {
if $left != $right {
return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
}
};
}
#[macro_export]
macro_rules! assert_ne {
($left:expr, $right:expr) => {
if $left == $right {
return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
}
};
($left:expr, $right:expr, $($arg:tt)*) => {
if $left == $right {
return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
}
};
}
#[macro_export]
macro_rules! fixup {
($cond:expr) => {
if $cond {
return Err(TestError(format!("Known issue fixed in {}:{}", file!(), line!())));
} else {
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
}
};
($cond:expr, $($arg:tt)*) => {
if $cond {
return Err(TestError(format!("Known issue fixed in {}:{}: {}", file!(), line!(), format_args!($($arg)*))));
} else {
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
}
};
}
#[macro_export]
macro_rules! ignore {
($cond:expr) => {
if $cond {
eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
} else {
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
}
};
($cond:expr, $($arg:tt)*) => {
if $cond {
eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
} else {
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
}
};
}
#[macro_export]
macro_rules! assert_status_aborted {
($status:expr) => {{
const XABRT: i32 = 128 + libc::SIGABRT;
const XBUS: i32 = 128 + libc::SIGBUS;
const XILL: i32 = 128 + libc::SIGILL;
const XSEGV: i32 = 128 + libc::SIGSEGV;
assert_status_code_matches!($status, XABRT | XBUS | XILL | XSEGV);
}};
}
#[macro_export]
macro_rules! assert_status_terminated {
($status:expr) => {{
const XTERM: i32 = 128 + libc::SIGTERM;
assert_status_code_matches!($status, XTERM);
}};
}
#[macro_export]
macro_rules! assert_status_panicked {
($status:expr) => {{
assert_status_code!($status, 101);
}};
}
#[macro_export]
macro_rules! assert_status_timeout_exceeded {
($status:expr) => {{
assert_status_code!($status, 124);
}};
}
#[macro_export]
macro_rules! assert_status_code_matches {
($status:expr, $($pattern:tt)+) => {{
let code = $status.code().unwrap_or(127);
$crate::assert!(
matches!(code, $($pattern)+),
"code: {code} status: {:?}",
$status
);
}};
}
#[macro_export]
macro_rules! assert_status_code {
($status:expr, $expected:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert_eq!(code, $expected, "code:{code} status:{:?}", $status);
};
}
#[macro_export]
macro_rules! assert_status_ok {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert!($status.success(), "code:{code} status:{:?}", $status);
};
}
#[macro_export]
macro_rules! assert_status_not_ok {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert!(!$status.success(), "code:{code} status:{:?}", $status);
};
}
#[macro_export]
macro_rules! assert_status_bad_file {
($status:expr) => {
$crate::assert_status_code!($status, libc::EBADF);
};
}
#[macro_export]
macro_rules! assert_status_bad_message {
($status:expr) => {
$crate::assert_status_code!($status, libc::EBADMSG);
};
}
#[macro_export]
macro_rules! assert_status_access_denied {
($status:expr) => {
$crate::assert_status_code!($status, libc::EACCES);
};
}
#[macro_export]
macro_rules! assert_status_permission_denied {
($status:expr) => {
$crate::assert_status_code!($status, libc::EPERM);
};
}
#[macro_export]
macro_rules! assert_status_network_unreachable {
($status:expr) => {
$crate::assert_status_code!($status, libc::ENETUNREACH);
};
}
#[macro_export]
macro_rules! assert_status_no_such_process {
($status:expr) => {
$crate::assert_status_code!($status, libc::ESRCH);
};
}
#[macro_export]
macro_rules! assert_status_broken_pipe {
($status:expr) => {
$crate::assert_status_code!($status, libc::EPIPE);
};
}
#[macro_export]
macro_rules! assert_status_busy {
($status:expr) => {
$crate::assert_status_code!($status, libc::EBUSY);
};
}
#[macro_export]
macro_rules! assert_status_connection_refused {
($status:expr) => {
$crate::assert_status_code!($status, libc::ECONNREFUSED);
};
}
#[macro_export]
macro_rules! assert_status_not_connected {
($status:expr) => {
$crate::assert_status_code!($status, libc::ENOTCONN);
};
}
#[macro_export]
macro_rules! assert_status_faulted {
($status:expr) => {
$crate::assert_status_code!($status, libc::EFAULT);
};
}
#[macro_export]
macro_rules! assert_status_hidden {
($status:expr) => {
$crate::assert_status_code!($status, libc::ENOENT);
};
}
#[macro_export]
macro_rules! assert_status_notdir {
($status:expr) => {
$crate::assert_status_code!($status, libc::ENOTDIR);
};
}
#[macro_export]
macro_rules! assert_status_illegal_sequence {
($status:expr) => {
$crate::assert_status_code!($status, libc::EILSEQ);
};
}
#[macro_export]
macro_rules! assert_status_invalid {
($status:expr) => {
$crate::assert_status_code!($status, libc::EINVAL);
};
}
#[macro_export]
macro_rules! assert_status_interrupted {
($status:expr) => {
$crate::assert_status_code!($status, libc::EINTR);
};
}
#[macro_export]
macro_rules! assert_status_loop {
($status:expr) => {
$crate::assert_status_code!($status, libc::ELOOP);
};
}
#[macro_export]
macro_rules! assert_status_unimplemented {
($status:expr) => {
$crate::assert_status_code!($status, libc::ENOSYS);
};
}
#[macro_export]
macro_rules! assert_status_killed {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert_eq!(
code,
128 + libc::SIGKILL,
"code:{code} status:{:?}",
$status
);
};
}
#[macro_export]
macro_rules! assert_status_not_killed {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert_ne!(
code,
128 + libc::SIGKILL,
"code:{code} status:{:?}",
$status
);
};
}
#[macro_export]
macro_rules! assert_status_sigsys {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert_eq!(code, 128 + libc::SIGSYS, "code:{code} status:{:?}", $status);
};
}
#[macro_export]
macro_rules! assert_status_signaled {
($status:expr, $signal:expr) => {
let signal = $status.signal().unwrap_or(127);
$crate::assert_eq!(signal, $signal, "signal:{signal} status:{:?}", $status);
};
}
#[macro_export]
macro_rules! assert_status_not_supported {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert!(
matches!(
code,
libc::EAFNOSUPPORT | libc::ENOTSUP | libc::EPROTONOSUPPORT
),
"code:{code} status:{:?}",
$status
);
};
}
#[macro_export]
macro_rules! assert_status_operation_not_supported {
($status:expr) => {
let code = $status.code().unwrap_or(127);
$crate::assert!(
matches!(code, libc::EOPNOTSUPP),
"code:{code} status:{:?}",
$status
);
};
}
#[macro_export]
macro_rules! skip_unless_gnu {
($($program:expr),* $(,)?) => {{
let mut all_gnu = true;
$(
if !is_program_gnu($program) {
eprintln!("Test requires {} to be GNU, skipping!", $program);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
all_gnu = false;
}
)*
if !all_gnu {
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_available {
($($program:expr),* $(,)?) => {{
let mut all_available = true;
$(
if !is_program_available($program) {
eprintln!("Test requires {}, skipping!", $program);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
all_available = false;
}
)*
if !all_available {
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_trusted {
() => {{
if !cfg!(feature = "trusted") {
eprintln!("trusted feature not enabled, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_if_32bin {
() => {{
if cfg!(target_pointer_width = "32") {
eprintln!("32-bit target detected, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_if_32bin_64host {
() => {{
if check_32bin_64host() {
eprintln!("Binary/Host mismatch, cannot run test, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_if_mips {
() => {{
if cfg!(any(
target_arch = "mips",
target_arch = "mips32r6",
target_arch = "mips64",
target_arch = "mips64r6",
)) {
eprintln!("Test does not work on MIPS, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_if_root {
() => {{
if Uid::effective().is_root() {
eprintln!("Test requires to run as non-root user, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("Thanks for not running this test as root!");
}
}};
}
#[macro_export]
macro_rules! skip_if_fs {
($($fs:expr),* $(,)?) => {{
let cwd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
let current_fs = syd::compat::FsType::get(cwd).unwrap();
$(
if let Ok(fs_types) = syd::compat::FsType::from_name($fs) {
if fs_types.contains(¤t_fs) {
eprintln!("Test does not run correctly on {}, skipping!", $fs);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}
)*
eprintln!("Filesystem check passed, continuing...");
}};
}
#[macro_export]
macro_rules! skip_unless_iproute2 {
() => {{
if !check_iproute2() {
eprintln!("ip is not iproute2, cannot run test, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_ipv6 {
() => {{
if !check_ipv6() {
eprintln!("IPv6 is not supported, cannot run test, skipping!");
env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_linux {
($need_major:expr, $need_minor:expr) => {{
let (maj, min) = *syd::config::KERNEL_VERSION;
let too_old = maj < $need_major || (maj == $need_major && min < $need_minor);
if too_old {
eprintln!(
"Test requires Linux kernel >= {}.{} (current: {maj}.{min}), skipping!",
$need_major, $need_minor,
);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!(
"Linux kernel {maj}.{min} satisfies >= {}.{}, proceeding!",
$need_major, $need_minor,
);
}
}};
}
#[macro_export]
macro_rules! skip_unless_stdin_is_a_tty {
() => {{
if !nix::unistd::isatty(std::io::stdin()).unwrap_or(false) {
eprintln!("Test requires STDIN to be a TTY, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("STDIN is a TTY, proceeding with test...");
}
}};
}
#[macro_export]
macro_rules! skip_unless_stdout_is_a_tty {
() => {{
if !nix::unistd::isatty(std::io::stdout()).unwrap_or(false) {
eprintln!("Test requires STDOUT to be a TTY, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("STDOUT is a TTY, proceeding with test...");
}
}};
}
#[macro_export]
macro_rules! skip_unless_kernel_crypto_is_supported {
() => {{
let key = syd::hash::Key::random().unwrap();
let key_id = match syd::hash::add_key(
"user",
"SYD-3-CRYPT-TEST",
key.as_ref(),
syd::hash::KEY_SPEC_USER_KEYRING,
) {
Ok(key_id) => key_id,
Err(nix::errno::Errno::EAFNOSUPPORT | nix::errno::Errno::ENOSYS) => {
eprintln!("Test requires Linux keyrings(7) API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(nix::errno::Errno::EPERM) => {
eprintln!("Are you in a container without keyrings access?");
eprintln!("Test requires Linux keyrings(7) API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(nix::errno::Errno::EACCES) => {
eprintln!("Is your session keyring attached to your user keyring?");
eprintln!("Test requires Linux keyrings(7) API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(errno) => {
return Err(TestError(format!(
"Failed to test for Linux keyrings(7) API: {errno}"
)));
}
};
match syd::hash::aes_ctr_setup(key_id) {
Ok(fd) => drop(fd),
Err(nix::errno::Errno::EAFNOSUPPORT | Errno::ENOPROTOOPT) => {
eprintln!("Test requires Linux Kernel Cryptography API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(nix::errno::Errno::EACCES) => {
eprintln!("Is your session keyring attached to your user keyring?");
eprintln!("Test requires Linux keyrings(7) API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(errno) => {
return Err(TestError(format!(
"Failed to test for Linux Kernel Cryptography API: {errno}"
)));
}
}
match syd::hash::hmac_sha256_setup(key_id) {
Ok(fd) => drop(fd),
Err(nix::errno::Errno::EAFNOSUPPORT | Errno::ENOPROTOOPT) => {
eprintln!("Test requires Linux Kernel Cryptography API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(nix::errno::Errno::EACCES) => {
eprintln!("Is your session keyring attached to your user keyring?");
eprintln!("Test requires Linux keyrings(7) API, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(errno) => {
return Err(TestError(format!(
"Failed to test for Linux Kernel Cryptography API: {errno}"
)));
}
}
}};
}
#[macro_export]
macro_rules! skip_unless_xattrs_are_supported {
() => {{
let fd = match nix::fcntl::open(
"xattr.test",
nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CREAT | nix::fcntl::OFlag::O_EXCL,
nix::sys::stat::Mode::from_bits_truncate(0o600),
) {
Ok(fd) => fd,
Err(errno) => return Err(TestError(format!("Failed to open xattr.test: {errno}"))),
};
match syd::xattr::fsetxattr(&fd, "user.syd.crypt.api", b"3", libc::XATTR_CREATE) {
Ok(_) => {
let _ = nix::unistd::close(fd);
}
Err(nix::errno::Errno::EOPNOTSUPP) => {
let _ = nix::unistd::close(fd);
eprintln!("Test requires extended attributes support, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(errno) => {
let _ = nix::unistd::close(fd);
return Err(TestError(format!(
"Failed to test for extended attributes support: {errno}"
)));
}
}
}};
}
#[macro_export]
macro_rules! skip_unless_unix_diag_is_supported {
() => {{
match syd::confine::check_unix_diag() {
Ok(true) => {
eprintln!("Unix socket diagnostics are supported, proceeding with test...");
}
Ok(false) => {
eprintln!("Test requires UNIX socket diagnostics support, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
Err(errno) => {
return Err(TestError(format!(
"Failed to test for UNIX socket diagnostics: {errno}"
)));
}
}
}};
}
#[macro_export]
macro_rules! skip_unless_unshare {
( $( $ns:expr ),* $(,)? ) => {{
let namespaces = [$($ns),*].join(",");
if namespaces.is_empty() {
panic!("No namespace given!");
}
eprintln!("[*] Checking support for namespaces `{namespaces}'...");
let code = syd()
.p("off")
.m(&format!("unshare/{namespaces}:1"))
.arg("/dev/nulx")
.status()
.expect("execute syd")
.code()
.unwrap_or(127);
if code != Errno::ENOENT as i32 {
eprintln!("[!] Syd exited with {code}!");
eprintln!("[!] Test requires Linux namespaces `{namespaces}', skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("[*] Namespaces `{namespaces}' are supported, proceeding with test.");
}
}};
}
#[macro_export]
macro_rules! skip_unless_coredumps {
() => {{
if let Err(errno) = enable_coredumps() {
eprintln!("Failed to enable coredumps: {errno}!");
eprintln!("Skipping test!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_exists {
($path:expr) => {{
if !std::path::Path::new($path).exists() {
eprintln!("Test requires the path \"{}\" to exist, skipping!", $path);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_bitness {
($bitness:expr) => {{
if !cfg!(target_pointer_width = $bitness) {
eprintln!("Test requires a {}-bit syd, skipping!", $bitness);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_ptrace_set_syscall_info {
() => {{
if !*syd::config::HAVE_PTRACE_SET_SYSCALL_INFO {
eprintln!("Test requires PTRACE_SET_SYSCALL_INFO (Linux >= 6.14), skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_feature {
($feature:expr) => {{
if !cfg!(feature = $feature) {
eprintln!(
"Test requires syd built with {} feature, skipping!",
$feature
);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_cap {
($cap:expr) => {{
use std::str::FromStr;
let cap = syd::caps::Capability::from_str(&syd::caps::to_canonical($cap)).expect("cap2str");
if !syd::caps::has_cap(None, syd::caps::CapSet::Permitted, cap).expect("syd::caps::has_cap")
{
eprintln!("Test requires {cap} capability, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("Capability {cap} set, proceeding with test.");
}
}};
}
#[macro_export]
macro_rules! skip_if_landlock_abi_supported {
($abi:expr) => {{
if syd::landlock::ABI::new_current() >= syd::landlock::ABI::from($abi) {
eprintln!(
"Test is for Landlock ABI versions older than {}, skipping!",
$abi
);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!(
"Landlock ABI {} is NOT supported, proceeding with test...",
$abi
);
}
}};
}
#[macro_export]
macro_rules! skip_unless_landlock_abi_supported {
($abi:expr) => {{
if syd::landlock::ABI::new_current() < syd::landlock::ABI::from($abi) {
eprintln!("Test requires Landlock ABI version {}, skipping!", $abi);
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!(
"Landlock ABI {} is supported, proceeding with test...",
$abi
);
}
}};
}
#[macro_export]
macro_rules! skip_if_cross_memory_attach_is_not_enabled {
() => {
if !*syd::config::HAVE_CROSS_MEMORY_ATTACH {
eprintln!("skipping: kernel not configured with CONFIG_CROSS_MEMORY_ATTACH");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
};
}
#[macro_export]
macro_rules! skip_if_strace {
() => {{
if std::env::var("SYD_TEST_STRACE").is_ok() && std::env::var("SYD_TEST_FORCE").is_err() {
eprintln!("Test does not work correctly under strace, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_strace_can_inject {
() => {{
if !check_strace_inject() {
eprintln!("strace can't inject syscalls, cannot run test, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
#[macro_export]
macro_rules! skip_unless_pty {
() => {{
use std::process::Command;
skip_unless_available!("python");
let status = Command::new("python")
.arg("-c")
.arg("import os; os.openpty()")
.status()
.expect("execute python");
if status.code().unwrap_or(127) != 0 {
eprintln!("Test requires PTY access, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
} else {
eprintln!("PTY access is available, proceeding with test...");
}
}};
}
#[macro_export]
macro_rules! skip_unless_at_execve_check_is_supported {
() => {{
if !*HAVE_AT_EXECVE_CHECK {
eprintln!("execveat(2) flag AT_EXECVE_CHECK is not supported, skipping!");
std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
return Ok(());
}
}};
}
pub static SYD: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd").unwrap_or("syd".to_string()));
pub static SYD_AUX: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-aux").unwrap_or("syd-aux".to_string()));
pub static SYD_BIT: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-bit").unwrap_or("syd-bit".to_string()));
pub static SYD_CAP: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-cap").unwrap_or("syd-cap".to_string()));
pub static SYD_DNS: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-dns").unwrap_or("syd-dns".to_string()));
#[allow(dead_code)]
pub static SYD_ENV: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-env").unwrap_or("syd-env".to_string()));
pub static SYD_EXEC: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-exec").unwrap_or("syd-exec".to_string()));
pub static SYD_AES: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-aes").unwrap_or("syd-aes".to_string()));
pub static _SYD_KEY: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-key").unwrap_or("syd-key".to_string()));
pub static SYD_ELF: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-elf").unwrap_or("syd-elf".to_string()));
pub static SYD_CPU: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-cpu").unwrap_or("syd-cpu".to_string()));
pub static SYD_HEX: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-hex").unwrap_or("syd-hex".to_string()));
pub static SYD_INFO: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-info").unwrap_or("syd-info".to_string()));
pub static SYD_LOCK: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-lock").unwrap_or("syd-lock".to_string()));
pub static SYD_MDWE: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-mdwe").unwrap_or("syd-mdwe".to_string()));
pub static SYD_OFD: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-ofd").unwrap_or("syd-ofd".to_string()));
pub static SYD_PAUSE: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-pause").unwrap_or("syd-pause".to_string()));
pub static SYD_PDS: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-pds").unwrap_or("syd-pds".to_string()));
pub static SYD_SEC: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-sec").unwrap_or("syd-sec".to_string()));
pub static SYD_SIZE: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-size").unwrap_or("syd-size".to_string()));
pub static SYD_DO: LazyLock<String> =
LazyLock::new(|| match env::var("CARGO_BIN_EXE_syd-test-do") {
Ok(var) => Path::new(&var)
.canonicalize()
.expect("CARGO_BIN_EXE_syd-test-do")
.to_string_lossy()
.into_owned(),
Err(_) => which(if env::var("SYD_TEST_32").is_ok() {
"syd-test-do32"
} else {
"syd-test-do"
})
.expect("syd-test-do"),
});
pub static SYD_UTS: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-uts").unwrap_or("syd-uts".to_string()));
pub static SYD_X: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-x").unwrap_or("syd-x".to_string()));
#[cfg(feature = "oci")]
pub static SYD_OCI: LazyLock<String> =
LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-oci").unwrap_or("syd-oci".to_string()));
pub static CI_BUILD: LazyLock<bool> = LazyLock::new(|| env::var("JOB_ID").ok().is_some());
pub static GL_BUILD: LazyLock<bool> = LazyLock::new(|| env::var("CI_PROJECT_ID").ok().is_some());
#[allow(dead_code)]
pub static SYD_TEST_TTY: LazyLock<bool> =
LazyLock::new(|| isatty(std::io::stderr()).unwrap_or(false));
pub struct Syd {
cmd: Command,
is_quiet: bool,
}
impl Syd {
pub fn new(cmd: &str) -> Self {
let mut cmd = Command::new(cmd);
cmd.stdin(Stdio::null());
Syd {
cmd,
is_quiet: false,
}
}
pub fn c<S: ToString>(&mut self, arg: S) -> &mut Self {
self.cmd.arg(format!("-c{}", arg.to_string()));
self
}
pub fn m<S: ToString>(&mut self, arg: S) -> &mut Self {
self.cmd.arg(format!("-m{}", arg.to_string()));
self
}
pub fn p<S: ToString>(&mut self, arg: S) -> &mut Self {
self.cmd.arg(format!("-p{}", arg.to_string()));
self
}
#[expect(non_snake_case)]
pub fn P<S: ToString>(&mut self, arg: S) -> &mut Self {
self.cmd.arg(format!("-P{}", arg.to_string()));
self
}
pub fn current_dir<S: ToString>(&mut self, arg: S) -> &mut Self {
self.cmd.current_dir(arg.to_string());
self
}
pub fn log(&mut self, value: &str) -> &mut Self {
self.cmd
.env(ENV_LOG, env::var(ENV_LOG).unwrap_or(value.to_string()));
self
}
pub fn log_fd(&mut self, fd: RawFd) -> &mut Self {
self.cmd.env(ENV_LOG_FD, fd.to_string());
self
}
pub fn do_<I, S, V>(&mut self, value: V, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.cmd.args(["--", &SYD_DO]);
self.cmd.args(args);
self.do__(value)
}
pub fn do__<V>(&mut self, value: V) -> &mut Self
where
V: AsRef<OsStr>,
{
self.cmd.env("SYD_TEST_DO", value);
self
}
pub fn argv<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
self.cmd.arg("--");
self.cmd.args(args);
self
}
pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
self.cmd.arg(arg);
self
}
pub fn args<I, S>(&mut self, args: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<std::ffi::OsStr>,
{
self.cmd.args(args);
self
}
pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
self.cmd.stdin(cfg);
self
}
pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
self.cmd.stdout(cfg);
self
}
pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
self.cmd.stderr(cfg);
self
}
#[allow(dead_code)]
pub fn quiet(&mut self) -> &mut Self {
self.stderr(Stdio::null());
self.stdout(Stdio::null());
self.is_quiet = true;
self
}
pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: AsRef<OsStr>,
V: AsRef<OsStr>,
{
self.cmd.env(key, value);
self
}
pub fn env_remove<K>(&mut self, key: K) -> &mut Self
where
K: AsRef<OsStr>,
{
self.cmd.env_remove(key);
self
}
pub fn spawn(&mut self) -> std::io::Result<Child> {
if !self.is_quiet {
eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
}
self.cmd.spawn()
}
pub fn status(&mut self) -> std::io::Result<ExitStatus> {
if !self.is_quiet {
eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
}
self.cmd.status()
}
pub fn output(&mut self) -> std::io::Result<Output> {
if !self.is_quiet {
eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
}
self.cmd.stderr(Stdio::inherit());
self.cmd.output()
}
}
pub fn syd() -> Syd {
static USE_PERF: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_PERF").is_some());
static USE_TRACE: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_TRACE").is_some());
static USE_STRACE: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_STRACE").is_some());
static USE_VALGRIND: LazyLock<bool> =
LazyLock::new(|| env::var_os("SYD_TEST_VALGRIND").is_some());
static USE_HELGRIND: LazyLock<bool> =
LazyLock::new(|| env::var_os("SYD_TEST_HELGRIND").is_some());
let mut cmd = Syd::new("timeout");
if check_timeout_foreground() {
cmd.arg("-v");
cmd.arg("--foreground");
cmd.arg("--preserve-status");
}
cmd.arg("-sKILL");
cmd.arg(env::var("SYD_TEST_TIMEOUT").unwrap_or("15m".to_string()));
if *USE_PERF {
cmd.arg("perf");
cmd.arg("record");
cmd.arg("-F99");
cmd.arg("--call-graph=dwarf");
cmd.arg("-o/tmp/syd-perf.data"); cmd.arg("--");
} else if *USE_STRACE {
cmd.arg("strace");
cmd.arg("-yyY");
if env::var_os("SYD_TEST_STRACE_NOFORK").is_none() {
cmd.arg("-f");
}
if env::var_os("SYD_TEST_STRACE_VERBOSE").is_none() {
cmd.arg("-s256");
cmd.arg("-e!read,readv,write,writev");
} else {
cmd.arg("-s4096");
}
let arg = env::var_os("SYD_TEST_STRACE").unwrap();
if !arg.is_empty() {
cmd.arg(arg);
}
cmd.arg("--");
} else if *USE_VALGRIND {
cmd.arg("valgrind");
cmd.arg("--leak-check=yes");
cmd.arg("--track-origins=yes");
cmd.arg("--track-fds=yes");
cmd.arg("--trace-children=no");
cmd.arg("--");
} else if *USE_HELGRIND {
cmd.arg("valgrind");
cmd.arg("--tool=helgrind");
cmd.arg("--");
}
cmd.arg(&*SYD);
cmd.arg("-q");
cmd.m("trace/allow_unsafe_dumpable:1");
cmd.env(ENV_LOG, env::var(ENV_LOG).unwrap_or("warn".to_string()));
if env::var_os(ENV_FORCE_TTY).is_none() {
cmd.env(ENV_QUIET_TTY, "YesPlease");
}
cmd.m("trace/allow_unsafe_exec_nopie:1"); cmd.m("trace/allow_unsafe_prlimit:1"); if *USE_TRACE || (*USE_STRACE && env::var_os("SYD_TEST_STRACE_NOFORK").is_none()) {
cmd.m("trace/allow_unsafe_ptrace:1"); }
cmd
}
pub fn get_user_uid(user: &str) -> Uid {
let out = Command::new("id")
.arg("-u")
.arg(user)
.output()
.unwrap()
.stdout;
let out = String::from_utf8_lossy(&out);
let out = out.trim();
Uid::from(out.parse::<libc::uid_t>().unwrap())
}
pub fn get_user_gid(user: &str) -> Gid {
let out = Command::new("id")
.arg("-g")
.arg(user)
.output()
.unwrap()
.stdout;
let out = String::from_utf8_lossy(&out);
let out = out.trim();
Gid::from(out.parse::<libc::gid_t>().unwrap())
}
pub fn is_program_gnu(command: &str) -> bool {
if check_32bin_64host() {
eprintln!("Binary/Host mismatch, cannot use program {command}!");
return false;
}
let out = Command::new(command)
.arg("--version")
.output()
.unwrap()
.stdout;
memmem::find(&out, b"GNU").is_some()
}
pub fn is_program_available(command: &str) -> bool {
if check_32bin_64host() {
eprintln!("Binary/Host mismatch, cannot use program {command}!");
return false;
}
Command::new("which")
.stdout(Stdio::null())
.arg(command)
.status()
.map(|status| status.success())
.unwrap_or(false)
}
pub fn which(command: &str) -> Result<String, Errno> {
let out = Command::new("which")
.arg(command)
.output()
.expect("execute which")
.stdout;
if out.is_empty() {
return Err(Errno::ENOENT);
}
let bin = String::from_utf8_lossy(&out);
let bin = bin.trim();
Ok(canonicalize(bin)
.map_err(|_| Errno::last())?
.to_string_lossy()
.into_owned())
}
pub fn check_ipv6() -> bool {
let test_sock = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0);
if let Err(error) = TcpListener::bind(test_sock) {
eprintln!("IPv6 is not available on this system. Skipping test: {error}");
false
} else {
true
}
}
pub fn check_nested_routines() -> bool {
let c_code = r#"
#include <stdlib.h>
int main(int argc, char *argv[])
{
int x = atoi(argv[1]);
int nested()
{
return x * x;
};
// Use pointer indirection so compiler cannot unnest.
int (*fp)() = nested;
// Yolo!
return fp();
}
"#;
if let Ok(mut file) = File::create("nested.c") {
if file.write_all(c_code.as_bytes()).is_err() {
return false;
}
} else {
return false;
}
let compile_status = Command::new("cc")
.arg("-Wall")
.arg("-Wextra")
.arg("-zexecstack") .arg("nested.c")
.arg("-o")
.arg("nested")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
compile_status
.map(|status| status.success())
.unwrap_or(false)
}
pub fn check_self_modifying_mp() -> bool {
if !cfg!(target_arch = "x86_64") {
return false;
}
let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
void foo(void);
int change_page_permissions_of_address(void *addr);
int main(void) {
void *foo_addr = (void*)foo;
// Change the permissions of the page that contains foo() to read,
// write, and execute. This assumes that foo() is fully contained
// by a single page.
if(change_page_permissions_of_address(foo_addr) == -1) {
int save_errno = errno;
fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
return save_errno;
}
// Call the unmodified foo()
puts("Calling foo...");
foo();
// Change the immediate value in the addl instruction in foo() to 42
unsigned char *instruction = (unsigned char*)foo_addr + 18;
*instruction = 0x2A;
// Call the modified foo()
puts("Calling foo...");
foo();
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
int change_page_permissions_of_address(void *addr) {
// Move the pointer to the page boundary
int page_size = getpagesize();
addr -= (unsigned long)addr % page_size;
if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
return -1;
}
return 0;
}
"#;
if let Ok(mut file) = File::create("selfmod.c") {
if file.write_all(c_code.as_bytes()).is_err() {
return false;
}
} else {
return false;
}
let compile_status = Command::new("cc")
.arg("-std=c99")
.arg("-D_BSD_SOURCE")
.arg("-Wall")
.arg("selfmod.c")
.arg("-o")
.arg("selfmod")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
compile_status
.map(|status| status.success())
.unwrap_or(false)
}
pub fn check_self_modifying_xs() -> bool {
if !cfg!(target_arch = "x86_64") {
return false;
}
let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>
void foo(void);
int change_page_permissions_of_address(void *addr);
int main(void) {
void *foo_addr = (void*)foo;
// Change the permissions of the page that contains foo() to read,
// write, and execute. This assumes that foo() is fully contained
// by a single page.
if(change_page_permissions_of_address(foo_addr) == -1) {
int save_errno = errno;
fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
return save_errno;
}
// Call the unmodified foo()
puts("Calling foo...");
foo();
// Change the immediate value in the addl instruction in foo() to 42
unsigned char *instruction = (unsigned char*)foo_addr + 18;
*instruction = 0x2A;
// Call the modified foo()
puts("Calling foo...");
foo();
return 0;
}
void foo(void) {
int i=0;
i++;
printf("i: %d\n", i);
}
int change_page_permissions_of_address(void *addr) {
// Move the pointer to the page boundary
int page_size = getpagesize();
addr -= (unsigned long)addr % page_size;
if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
return -1;
}
return 0;
}
"#;
if let Ok(mut file) = File::create("selfmod.c") {
if file.write_all(c_code.as_bytes()).is_err() {
return false;
}
} else {
return false;
}
let compile_status = Command::new("cc")
.arg("-std=c99")
.arg("-D_BSD_SOURCE")
.arg("-zexecstack")
.arg("-Wall")
.arg("selfmod.c")
.arg("-o")
.arg("selfmod")
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status();
compile_status
.map(|status| status.success())
.unwrap_or(false)
}
pub fn format_duration(d: Duration) -> String {
let total_seconds = d.as_secs();
let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = total_seconds % 60;
format!("{hours}h {minutes}m {seconds}s")
}
pub fn check_32bin_64host() -> bool {
#[cfg(target_pointer_width = "32")]
let is_binary_32bit = true;
#[cfg(not(target_pointer_width = "32"))]
let is_binary_32bit = false;
let arch = uname()
.expect("uname")
.machine()
.to_string_lossy()
.to_string();
if is_binary_32bit && arch.contains("64") {
eprintln!("32->64: Running 32bit on {arch}!");
true
} else {
false
}
}
pub fn check_strace_inject() -> bool {
let status = Command::new("strace")
.arg("-qq")
.arg("-e")
.arg("trace=getpid")
.arg("-e")
.arg("inject=getpid:retval=2525")
.arg("--")
.arg("sh")
.arg("-c")
.arg("test $$ -eq 2525")
.status();
match status {
Ok(status) => status.success(),
Err(_) => false,
}
}
pub fn enable_coredumps() -> Result<(), Errno> {
confine_rlimit(Resource::RLIMIT_CORE, Some(RLIM_INFINITY))
}
pub fn check_timeout_foreground() -> bool {
Command::new("timeout")
.arg("--foreground")
.arg("-sKILL")
.arg("60s")
.arg("true")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
}
pub fn check_iproute2() -> bool {
Command::new("sh")
.arg("-cex")
.arg("ip -V | grep -iq iproute2")
.status()
.map(|status| status.success())
.unwrap_or(false)
}
pub fn current_dir(base: bool) -> std::io::Result<PathBuf> {
let current_dir = env::current_dir()?;
if base {
let basename = current_dir
.file_name()
.ok_or(std::io::Error::other("Failed to get the basename"))
.map(PathBuf::from)?;
Ok(basename)
} else {
current_dir.canonicalize()
}
}
pub fn shuffle_vec<T>(vec: &mut [T]) {
let len = vec.len();
for i in 0..len {
#[cfg(not(target_os = "android"))]
let r = unsafe { libc::rand() } as usize;
#[cfg(target_os = "android")]
let r = syd::rng::randint(usize::MIN..=usize::MAX).unwrap();
let j = r % (len - i) + i;
vec.swap(i, j); }
}