From 64d1f4069a070ca8893390a92d20ca1029d99626 Mon Sep 17 00:00:00 2001 From: Joseph Jon Booker Date: Sun, 14 Dec 2025 23:00:42 -0600 Subject: [PATCH] feat(pivot_root): add pivot_root utility Implement the pivot_root(2) syscall wrapper for changing the root filesystem. This utility is commonly used during container initialization and system boot. Features: - Linux/Android support via direct syscall - Graceful error on unsupported platforms - Detailed error messages with errno-specific hints - Non-UTF-8 path support via OsString The implementation delegates all path validation to the kernel, only checking for embedded null bytes which are invalid for C strings. --- Cargo.lock | 11 + Cargo.toml | 2 + src/uu/pivot_root/Cargo.toml | 18 ++ src/uu/pivot_root/pivot_root.md | 16 ++ src/uu/pivot_root/src/errors.rs | 223 +++++++++++++++++++ src/uu/pivot_root/src/main.rs | 1 + src/uu/pivot_root/src/pivot_root.rs | 327 ++++++++++++++++++++++++++++ tests/by-util/test_pivot_root.rs | 156 +++++++++++++ tests/tests.rs | 4 + 9 files changed, 758 insertions(+) create mode 100644 src/uu/pivot_root/Cargo.toml create mode 100644 src/uu/pivot_root/pivot_root.md create mode 100644 src/uu/pivot_root/src/errors.rs create mode 100644 src/uu/pivot_root/src/main.rs create mode 100644 src/uu/pivot_root/src/pivot_root.rs create mode 100644 tests/by-util/test_pivot_root.rs diff --git a/Cargo.lock b/Cargo.lock index 6f709e2..8d71e62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1369,6 +1369,7 @@ dependencies = [ "uu_mesg", "uu_mountpoint", "uu_nologin", + "uu_pivot_root", "uu_renice", "uu_rev", "uu_setpgid", @@ -1541,6 +1542,16 @@ dependencies = [ "uucore 0.2.2", ] +[[package]] +name = "uu_pivot_root" +version = "0.0.1" +dependencies = [ + "clap", + "libc", + "thiserror", + "uucore 0.2.2", +] + [[package]] name = "uu_renice" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index e2efeee..486a5c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ feat_common_core = [ "mesg", "mountpoint", "nologin", + "pivot_root", "renice", "rev", "setpgid", @@ -110,6 +111,7 @@ mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = " mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" } mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" } nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = "src/uu/nologin" } +pivot_root = { optional = true, version = "0.0.1", package = "uu_pivot_root", path = "src/uu/pivot_root" } renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" } rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" } setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" } diff --git a/src/uu/pivot_root/Cargo.toml b/src/uu/pivot_root/Cargo.toml new file mode 100644 index 0000000..6bfa037 --- /dev/null +++ b/src/uu/pivot_root/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uu_pivot_root" +version = "0.0.1" +edition = "2021" +description = "change the root filesystem" + +[lib] +path = "src/pivot_root.rs" + +[[bin]] +name = "pivot_root" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +libc = { workspace = true } +uucore = { workspace = true } +thiserror = { workspace = true } diff --git a/src/uu/pivot_root/pivot_root.md b/src/uu/pivot_root/pivot_root.md new file mode 100644 index 0000000..ef0726f --- /dev/null +++ b/src/uu/pivot_root/pivot_root.md @@ -0,0 +1,16 @@ +# pivot_root + +``` +pivot_root NEW_ROOT PUT_OLD +``` + +Change the root filesystem. + +Moves the root filesystem of the calling process to the directory PUT_OLD and +makes NEW_ROOT the new root filesystem. + +This command requires the CAP_SYS_ADMIN capability and is typically used during +container initialization or system boot. + +- NEW_ROOT must be a mount point +- PUT_OLD must be at or underneath NEW_ROOT \ No newline at end of file diff --git a/src/uu/pivot_root/src/errors.rs b/src/uu/pivot_root/src/errors.rs new file mode 100644 index 0000000..dfcbe15 --- /dev/null +++ b/src/uu/pivot_root/src/errors.rs @@ -0,0 +1,223 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{NulError, OsString}; + +#[derive(Debug)] +#[allow(dead_code)] // Never constructed on non-Linux platforms +pub(crate) enum PathWhich { + NewRoot, + PutOld, +} + +impl std::fmt::Display for PathWhich { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PathWhich::NewRoot => write!(f, "new_root"), + PathWhich::PutOld => write!(f, "put_old"), + } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PivotRootError { + #[error("{which} path contains null byte at position {pos} (in '{path:?}')")] + NulError { + which: PathWhich, + pos: usize, + source: NulError, + path: OsString, + }, + + #[error("{message}")] + SyscallFailed { + message: String, + source: std::io::Error, + }, + + #[allow(dead_code)] // Only used on non-Linux platforms + #[error("pivot_root is only supported on Linux")] + UnsupportedPlatform, +} + +impl uucore::error::UError for PivotRootError { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} + +/// Convert a `std::io::Error` into a `PivotRootError` immediately after a +/// failed `pivot_root(2)` syscall. +/// +/// Important: this conversion is intended to be used right at the call site of +/// `pivot_root`, with the error value obtained from `std::io::Error::last_os_error()`. +/// Doing so preserves the correct `errno` from the kernel and lets us attach +/// helpful hints to well-known error codes (e.g., `EPERM`, `EINVAL`). Using an +/// arbitrary `std::io::Error` captured earlier or created in another context +/// may carry a stale or unrelated `raw_os_error`, which would yield misleading +/// diagnostics. The error codes can be obtained from the `pivot_root(2)` man page, +/// which acknowledges that errors from the `stat(2)` system call may also occur. +impl From for PivotRootError { + fn from(err: std::io::Error) -> Self { + let mut msg = format!("failed to change root: {}", err); + if let Some(code) = err.raw_os_error() { + msg.push_str(&format!(" (errno {code})")); + msg.push_str(match code { + libc::EPERM => "; the calling process does not have the CAP_SYS_ADMIN capability", + libc::EBUSY => "; new_root or put_old is on the current root mount", + libc::EINVAL => { + "; new_root is not a mount point, put_old is not at or underneath new_root, \ + the current root is not a mount point, the current root is on the rootfs, \ + or a mount point has propagation type MS_SHARED" + } + libc::ENOTDIR => "; new_root or put_old is not a directory", + libc::EACCES => "; search permission denied for a directory in the path prefix", + libc::EBADF => "; bad file descriptor", + libc::EFAULT => "; new_root or put_old points outside the accessible address space", + libc::ELOOP => "; too many symbolic links encountered while resolving the path", + libc::ENAMETOOLONG => "; new_root or put_old path is too long", + libc::ENOENT => { + "; a component of new_root or put_old does not exist, \ + or is a dangling symbolic link" + } + libc::ENOMEM => "; out of kernel memory", + libc::EOVERFLOW => { + "; path refers to a file whose size, inode number, or number of blocks \ + cannot be represented" + } + _ => "", + }); + } + + PivotRootError::SyscallFailed { + message: msg, + source: err, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_nul_error_display() { + // Create a NulError via CString::new + let bytes = b"/tmp\0/dir"; + let err = std::ffi::CString::new(&bytes[..]).unwrap_err(); + let e = PivotRootError::NulError { + which: PathWhich::NewRoot, + pos: err.nul_position(), + source: err, + path: OsString::from("/tmp\u{0}/dir"), + }; + let s = e.to_string(); + assert!(s.contains("new_root"), "{s}"); + assert!(s.contains("null byte"), "{s}"); + } + + fn msg_for(code: i32) -> String { + let err = std::io::Error::from_raw_os_error(code); + let e = PivotRootError::from(err); + e.to_string() + } + + #[test] + fn test_syscall_failed_eperm_hint() { + let s = msg_for(libc::EPERM); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("errno"), "{s}"); + assert!(s.contains("CAP_SYS_ADMIN"), "{s}"); + } + + #[test] + fn test_syscall_failed_ebusy_hint() { + let s = msg_for(libc::EBUSY); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("on the current root mount"), "{s}"); + } + + #[test] + fn test_syscall_failed_einval_hint() { + let s = msg_for(libc::EINVAL); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("not a mount point"), "{s}"); + assert!(s.contains("MS_SHARED"), "{s}"); + } + + #[test] + fn test_syscall_failed_enotdir_hint() { + let s = msg_for(libc::ENOTDIR); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("not a directory"), "{s}"); + } + + #[test] + fn test_syscall_failed_eacces_hint() { + let s = msg_for(libc::EACCES); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("permission denied"), "{s}"); + } + + #[test] + fn test_syscall_failed_ebadf_hint() { + let s = msg_for(libc::EBADF); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("bad file descriptor"), "{s}"); + } + + #[test] + fn test_syscall_failed_efault_hint() { + let s = msg_for(libc::EFAULT); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("accessible address space"), "{s}"); + } + + #[test] + fn test_syscall_failed_eloop_hint() { + let s = msg_for(libc::ELOOP); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("symbolic links"), "{s}"); + } + + #[test] + fn test_syscall_failed_enametoolong_hint() { + let s = msg_for(libc::ENAMETOOLONG); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("path is too long"), "{s}"); + } + + #[test] + fn test_syscall_failed_enoent_hint() { + let s = msg_for(libc::ENOENT); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("does not exist"), "{s}"); + assert!(s.contains("dangling symbolic link"), "{s}"); + } + + #[test] + fn test_syscall_failed_enomem_hint() { + let s = msg_for(libc::ENOMEM); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("out of kernel memory"), "{s}"); + } + + #[test] + fn test_syscall_failed_eoverflow_hint() { + let s = msg_for(libc::EOVERFLOW); + assert!(s.contains("failed to change root"), "{s}"); + assert!(s.contains("cannot be represented"), "{s}"); + } + + #[test] + fn test_unsupported_platform_display() { + let s = PivotRootError::UnsupportedPlatform.to_string(); + assert!(s.contains("only supported on Linux"), "{s}"); + } +} diff --git a/src/uu/pivot_root/src/main.rs b/src/uu/pivot_root/src/main.rs new file mode 100644 index 0000000..6126e57 --- /dev/null +++ b/src/uu/pivot_root/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_pivot_root); diff --git a/src/uu/pivot_root/src/pivot_root.rs b/src/uu/pivot_root/src/pivot_root.rs new file mode 100644 index 0000000..8bedcbb --- /dev/null +++ b/src/uu/pivot_root/src/pivot_root.rs @@ -0,0 +1,327 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use clap::{crate_version, Arg, ArgAction, Command}; +use std::ffi::{OsStr, OsString}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +mod errors; +use crate::errors::PivotRootError; + +const ABOUT: &str = help_about!("pivot_root.md"); +const USAGE: &str = help_usage!("pivot_root.md"); + +mod options { + pub const NEW_ROOT: &str = "new_root"; + pub const PUT_OLD: &str = "put_old"; +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; + + let new_root = matches + .get_one::(options::NEW_ROOT) + .expect("required argument"); + let put_old = matches + .get_one::(options::PUT_OLD) + .expect("required argument"); + + pivot_root_syscall(new_root, put_old)?; + + Ok(()) +} + +/// Thin wrapper around the `pivot_root(2)` system call. +/// +/// This function performs the `pivot_root` syscall directly via `libc::syscall`. +/// It does **not** perform any validation of the paths beyond checking for +/// embedded null bytes (which would be invalid for C strings passed to the kernel). +/// +/// The kernel itself performs all semantic validation. See `pivot_root(2)` and +/// `stat(2)` man pages for full details on these errors. +#[cfg(any(target_os = "linux", target_os = "android"))] +fn pivot_root_syscall(new_root: &OsStr, put_old: &OsStr) -> Result<(), PivotRootError> { + use crate::errors::PathWhich; + use std::ffi::CString; + use std::io; + use std::os::unix::ffi::OsStrExt; + + let new_root_cstr = + CString::new(new_root.as_bytes()).map_err(|e| PivotRootError::NulError { + which: PathWhich::NewRoot, + pos: e.nul_position(), + source: e, + path: new_root.to_os_string(), + })?; + let put_old_cstr = CString::new(put_old.as_bytes()).map_err(|e| PivotRootError::NulError { + which: PathWhich::PutOld, + pos: e.nul_position(), + source: e, + path: put_old.to_os_string(), + })?; + + let result = unsafe { + libc::syscall( + libc::SYS_pivot_root, + new_root_cstr.as_ptr(), + put_old_cstr.as_ptr(), + ) + }; + + match result { + 0 => Ok(()), + _ => Err(io::Error::last_os_error().into()), + } +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn pivot_root_syscall(_new_root: &OsStr, _put_old: &OsStr) -> Result<(), PivotRootError> { + Err(PivotRootError::UnsupportedPlatform) +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg( + Arg::new(options::NEW_ROOT) + .value_name("NEW_ROOT") + .help("New root file system") + .required(true) + .index(1) + .value_parser(clap::value_parser!(OsString)) + .value_hint(clap::ValueHint::DirPath) + .action(ArgAction::Set), + ) + .arg( + Arg::new(options::PUT_OLD) + .value_name("PUT_OLD") + .help("Directory to move the current root to") + .required(true) + .index(2) + .value_parser(clap::value_parser!(OsString)) + .value_hint(clap::ValueHint::DirPath) + .action(ArgAction::Set), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use uucore::error::UError; + + #[test] + #[cfg(unix)] + fn test_null_byte_in_new_root() { + // Create a path with an embedded null byte using raw bytes + // It's not expected that a null byte could be passed in via + // the command line, but perhaps if pivot_root_syscall becomes + // used outside of this crate. + use std::os::unix::ffi::OsStrExt; + let bytes = b"/tmp\0/test"; + let new_root = OsStr::from_bytes(bytes); + let put_old = OsStr::new("/old"); + + let result = pivot_root_syscall(new_root, put_old); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.code(), 1); + assert!(err.to_string().contains("null byte")); + } + + #[test] + #[cfg(unix)] + fn test_null_byte_in_put_old() { + use std::os::unix::ffi::OsStrExt; + let new_root = OsStr::new("/tmp"); + let bytes = b"/old\0/test"; + let put_old = OsStr::from_bytes(bytes); + + let result = pivot_root_syscall(new_root, put_old); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.code(), 1); + assert!(err.to_string().contains("null byte")); + } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_non_existent_paths() { + // This test verifies that non-existent paths produce a proper syscall error, not a panic + let new_root = OsStr::new("/non_existent_new_root_12345"); + let put_old = OsStr::new("/non_existent_put_old_12345"); + + let result = pivot_root_syscall(new_root, put_old); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.code(), 1); + let s = err.to_string(); + // On most systems without sufficient privileges, EPERM is expected. + // We only assert that we got a proper error message from the syscall path. + assert!(s.contains("failed to change root"), "{s}"); + } + + #[test] + #[cfg(not(any(target_os = "linux", target_os = "android")))] + fn test_unsupported_platform() { + let new_root = OsStr::new("/tmp"); + let put_old = OsStr::new("/old"); + + let result = pivot_root_syscall(new_root, put_old); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.code(), 1); + assert!(err.to_string().contains("only supported on Linux")); + } + + #[test] + fn test_uu_app_has_correct_args() { + let app = uu_app(); + let matches = app.try_get_matches_from(vec!["pivot_root", "/new", "/old"]); + assert!(matches.is_ok()); + + let matches = matches.unwrap(); + assert!(matches.contains_id(options::NEW_ROOT)); + assert!(matches.contains_id(options::PUT_OLD)); + + let new_root = matches.get_one::(options::NEW_ROOT); + let put_old = matches.get_one::(options::PUT_OLD); + + assert!(new_root.is_some()); + assert!(put_old.is_some()); + assert_eq!(new_root.unwrap(), "/new"); + assert_eq!(put_old.unwrap(), "/old"); + } + + #[test] + fn test_uu_app_missing_args() { + let app = uu_app(); + let result = app.try_get_matches_from(vec!["pivot_root"]); + assert!(result.is_err()); + let err = result.unwrap_err(); + let s = err.to_string(); + assert!(s.to_lowercase().contains("required"), "{s}"); + } + + #[test] + fn test_uu_app_one_arg() { + let app = uu_app(); + let result = app.try_get_matches_from(vec!["pivot_root", "/new"]); + assert!(result.is_err()); + let err = result.unwrap_err(); + let s = err.to_string(); + assert!(s.to_lowercase().contains("required"), "{s}"); + } + + #[test] + fn test_uu_app_help() { + let app = uu_app(); + let result = app.try_get_matches_from(vec!["pivot_root", "--help"]); + // --help causes an exit with DisplayHelp error + assert!(result.is_err()); + let s = result.unwrap_err().to_string(); + assert!(s.contains("pivot_root"), "{s}"); + assert!(s.contains("Usage") || s.contains("USAGE"), "{s}"); + } + + #[test] + fn test_uu_app_version() { + let app = uu_app(); + let result = app.try_get_matches_from(vec!["pivot_root", "--version"]); + // --version causes an exit with DisplayVersion error + assert!(result.is_err()); + let s = result.unwrap_err().to_string(); + assert!(s.contains("pivot_root"), "{s}"); + } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_valid_path_construction() { + // Test that we can create paths from valid inputs without panicking + let test_cases = vec![ + ("/tmp", "/old"), + ("/new/root/path", "/put/old/path"), + ("/", "/old"), + ]; + + for (new_root, put_old) in test_cases { + let new_root_os = OsString::from(new_root); + let put_old_os = OsString::from(put_old); + + // This shouldn't panic, even though it will fail with permission/ENOENT errors + let result = pivot_root_syscall(&new_root_os, &put_old_os); + + // We expect an error (no permission or path doesn't exist), + // but it should be a proper error, not a panic + assert!(result.is_err()); + let s = result.unwrap_err().to_string(); + assert!(s.contains("failed to change root"), "{s}"); + } + } + + #[test] + fn test_uu_app_accepts_paths_with_special_chars() { + // Test that clap accepts paths with special characters and non-UTF-8 (on Unix) + let test_cases = vec![ + ("pivot_root", "/new-root", "/put_old"), + ("pivot_root", "/new root", "/put old"), + ("pivot_root", "/new@root#123", "/put$old%456"), + ]; + + for args in test_cases { + let app = uu_app(); + let result = app.try_get_matches_from(vec![args.0, args.1, args.2]); + assert!(result.is_ok(), "Failed to parse args: {:?}", args); + } + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let non_utf8_cases: Vec<(&[u8], &[u8])> = vec![ + (b"/new-\xFFroot", b"/put-old"), + (b"/new-root", b"/put-\x80old"), + ]; + + for (new_root_bytes, put_old_bytes) in non_utf8_cases { + let app = uu_app(); + + let mut args: Vec = Vec::new(); + args.push(OsString::from("pivot_root")); + args.push(OsStr::from_bytes(new_root_bytes).to_os_string()); + args.push(OsStr::from_bytes(put_old_bytes).to_os_string()); + + let result = app.try_get_matches_from(args); + assert!(result.is_ok(), "Failed to parse non-UTF-8 args"); + } + } + } + + #[test] + fn test_uu_app_too_many_args() { + let app = uu_app(); + let result = app.try_get_matches_from(vec!["pivot_root", "/new", "/old", "/extra"]); + assert!(result.is_err(), "Should reject extra arguments"); + let s = result.unwrap_err().to_string(); + assert!( + s.contains("unexpected") || s.contains("Found argument"), + "{s}" + ); + } + + #[test] + #[cfg(any(target_os = "linux", target_os = "android"))] + fn test_empty_paths() { + // Test empty path handling + let new_root = OsStr::new(""); + let put_old = OsStr::new(""); + + let result = pivot_root_syscall(new_root, put_old); + assert!(result.is_err()); + // Should get a syscall error, not a panic + } +} diff --git a/tests/by-util/test_pivot_root.rs b/tests/by-util/test_pivot_root.rs new file mode 100644 index 0000000..60ec341 --- /dev/null +++ b/tests/by-util/test_pivot_root.rs @@ -0,0 +1,156 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +#[cfg(unix)] +use std::ffi::OsStr; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +use uutests::util::{TestScenario, UCommand}; + +fn new_pivot_root_cmd() -> UCommand { + TestScenario::new("pivot_root").ucmd() +} + +#[test] +fn test_invalid_arg() { + new_pivot_root_cmd() + .arg("--definitely-invalid") + .fails() + .code_is(1); +} + +#[test] +fn test_help() { + new_pivot_root_cmd() + .arg("--help") + .succeeds() + .stdout_contains("pivot_root") + .stdout_contains("NEW_ROOT") + .stdout_contains("PUT_OLD"); +} + +#[test] +fn test_version() { + new_pivot_root_cmd().arg("--version").succeeds(); +} + +#[test] +fn test_missing_arguments() { + new_pivot_root_cmd() + .fails() + .code_is(1) + .stderr_contains("required arguments"); +} + +#[test] +fn test_missing_put_old_argument() { + new_pivot_root_cmd() + .arg("/new_root") + .fails() + .code_is(1) + .stderr_contains("required"); +} + +#[test] +fn test_too_many_arguments() { + new_pivot_root_cmd() + .arg("/new_root") + .arg("/put_old") + .arg("/extra") + .fails() + .code_is(1); +} + +// Tests that require elevated privileges (CAP_SYS_ADMIN) +// These tests are ignored by default and should be run manually with: +// cargo test pivot_root -- --ignored +// +// Prerequisites: +// - Must be run as root or with CAP_SYS_ADMIN capability +// - Requires proper filesystem setup with new root and put_old directories + +#[test] +#[ignore] +fn test_pivot_root_non_existing_paths() { + // This test requires root but tests error handling for non-existing paths + new_pivot_root_cmd() + .arg("/non_existing_new_root") + .arg("/non_existing_put_old") + .fails() + .code_is(1) + .stderr_contains("failed to change root"); +} + +#[test] +#[ignore] +fn test_pivot_root_without_privileges() { + // This test should be run without root privileges to verify proper error handling + // Note: This may not fail with the expected error if run as root + new_pivot_root_cmd() + .arg("/tmp") + .arg("/tmp") + .fails() + .code_is(1); +} + +// Tests for non-UTF8 path acceptance +// These tests verify that pivot_root properly handles paths containing non-UTF8 bytes, +// which are valid on Unix filesystems (any byte sequence except null is allowed). + +#[test] +#[cfg(unix)] +#[ignore] +fn test_non_utf8_new_root_path() { + // Test that pivot_root accepts a non-UTF8 path for new_root + // The path contains bytes 0x80-0xFF which are invalid UTF-8 + let non_utf8_bytes: &[u8] = b"/tmp/test_\x80\x81\x82_new_root"; + let new_root = OsStr::from_bytes(non_utf8_bytes); + let put_old = OsStr::new("/tmp/put_old"); + + new_pivot_root_cmd() + .arg(new_root) + .arg(put_old) + .fails() + .code_is(1) + // The command should fail due to path not existing, not due to encoding issues + .stderr_contains("failed to change root"); +} + +#[test] +#[cfg(unix)] +#[ignore] +fn test_non_utf8_put_old_path() { + // Test that pivot_root accepts a non-UTF8 path for put_old + let new_root = OsStr::new("/tmp/new_root"); + let non_utf8_bytes: &[u8] = b"/tmp/test_\xff\xfe\xfd_put_old"; + let put_old = OsStr::from_bytes(non_utf8_bytes); + + new_pivot_root_cmd() + .arg(new_root) + .arg(put_old) + .fails() + .code_is(1) + // The command should fail due to path not existing, not due to encoding issues + .stderr_contains("failed to change root"); +} + +#[test] +#[cfg(unix)] +#[ignore] +fn test_non_utf8_both_paths() { + // Test that pivot_root accepts non-UTF8 paths for both arguments + let new_root_bytes: &[u8] = b"/tmp/\xc0\xc1_new"; + let put_old_bytes: &[u8] = b"/tmp/\xe0\xe1_old"; + let new_root = OsStr::from_bytes(new_root_bytes); + let put_old = OsStr::from_bytes(put_old_bytes); + + new_pivot_root_cmd() + .arg(new_root) + .arg(put_old) + .fails() + .code_is(1) + // The command should fail due to path not existing, not due to encoding issues + .stderr_contains("failed to change root"); +} diff --git a/tests/tests.rs b/tests/tests.rs index 8f94a09..c6f2ac1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -39,6 +39,10 @@ mod test_mountpoint; #[path = "by-util/test_nologin.rs"] mod test_nologin; +#[cfg(feature = "pivot_root")] +#[path = "by-util/test_pivot_root.rs"] +mod test_pivot_root; + #[cfg(feature = "blockdev")] #[path = "by-util/test_blockdev.rs"] mod test_blockdev;