Skip to content

Commit e6c9a8a

Browse files
committed
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.
1 parent b1a4ce0 commit e6c9a8a

File tree

9 files changed

+749
-0
lines changed

9 files changed

+749
-0
lines changed

Cargo.lock

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ feat_common_core = [
4242
"mesg",
4343
"mountpoint",
4444
"nologin",
45+
"pivot_root",
4546
"renice",
4647
"rev",
4748
"setpgid",
@@ -110,6 +111,7 @@ mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "
110111
mesg = { optional = true, version = "0.0.1", package = "uu_mesg", path = "src/uu/mesg" }
111112
mountpoint = { optional = true, version = "0.0.1", package = "uu_mountpoint", path = "src/uu/mountpoint" }
112113
nologin = { optional = true, version = "0.0.1", package = "uu_nologin", path = "src/uu/nologin" }
114+
pivot_root = { optional = true, version = "0.0.1", package = "uu_pivot_root", path = "src/uu/pivot_root" }
113115
renice = { optional = true, version = "0.0.1", package = "uu_renice", path = "src/uu/renice" }
114116
rev = { optional = true, version = "0.0.1", package = "uu_rev", path = "src/uu/rev" }
115117
setpgid = { optional = true, version = "0.0.1", package = "uu_setpgid", path = "src/uu/setpgid" }

src/uu/pivot_root/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "uu_pivot_root"
3+
version = "0.0.1"
4+
edition = "2021"
5+
description = "change the root filesystem"
6+
7+
[lib]
8+
path = "src/pivot_root.rs"
9+
10+
[[bin]]
11+
name = "pivot_root"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
clap = { workspace = true }
16+
libc = { workspace = true }
17+
uucore = { workspace = true }
18+
thiserror = { workspace = true }

src/uu/pivot_root/pivot_root.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# pivot_root
2+
3+
```
4+
pivot_root NEW_ROOT PUT_OLD
5+
```
6+
7+
Change the root filesystem.
8+
9+
Moves the root filesystem of the calling process to the directory PUT_OLD and
10+
makes NEW_ROOT the new root filesystem.
11+
12+
This command requires the CAP_SYS_ADMIN capability and is typically used during
13+
container initialization or system boot.
14+
15+
- NEW_ROOT must be a mount point
16+
- PUT_OLD must be at or underneath NEW_ROOT

src/uu/pivot_root/src/errors.rs

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
// This file is part of the uutils util-linux package.
2+
//
3+
// For the full copyright and license information, please view the LICENSE
4+
// file that was distributed with this source code.
5+
6+
use std::ffi::{NulError, OsString};
7+
8+
#[derive(Debug)]
9+
pub(crate) enum PathWhich {
10+
NewRoot,
11+
PutOld,
12+
}
13+
14+
impl std::fmt::Display for PathWhich {
15+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16+
match self {
17+
PathWhich::NewRoot => write!(f, "new_root"),
18+
PathWhich::PutOld => write!(f, "put_old"),
19+
}
20+
}
21+
}
22+
23+
#[derive(Debug, thiserror::Error)]
24+
pub enum PivotRootError {
25+
#[error("{which} path contains null byte at position {pos} (in '{path:?}')")]
26+
NulError {
27+
which: PathWhich,
28+
pos: usize,
29+
source: NulError,
30+
path: OsString,
31+
},
32+
33+
#[error("{message}")]
34+
SyscallFailed {
35+
message: String,
36+
source: std::io::Error,
37+
},
38+
39+
#[allow(dead_code)] // Only used on non-Linux platforms
40+
#[error("pivot_root is only supported on Linux")]
41+
UnsupportedPlatform,
42+
}
43+
44+
impl uucore::error::UError for PivotRootError {
45+
fn code(&self) -> i32 {
46+
1
47+
}
48+
49+
fn usage(&self) -> bool {
50+
false
51+
}
52+
}
53+
54+
/// Convert a `std::io::Error` into a `PivotRootError` immediately after a
55+
/// failed `pivot_root(2)` syscall.
56+
///
57+
/// Important: this conversion is intended to be used right at the call site of
58+
/// `pivot_root`, with the error value obtained from `std::io::Error::last_os_error()`.
59+
/// Doing so preserves the correct `errno` from the kernel and lets us attach
60+
/// helpful hints to well-known error codes (e.g., `EPERM`, `EINVAL`). Using an
61+
/// arbitrary `std::io::Error` captured earlier or created in another context
62+
/// may carry a stale or unrelated `raw_os_error`, which would yield misleading
63+
/// diagnostics. The error codes can be obtained from the `pivot_root(2)` man page,
64+
/// which acknowledges that errors from the `stat(2)` system call may also occur.
65+
impl From<std::io::Error> for PivotRootError {
66+
fn from(err: std::io::Error) -> Self {
67+
let mut msg = format!("failed to change root: {}", err);
68+
if let Some(code) = err.raw_os_error() {
69+
msg.push_str(&format!(" (errno {code})"));
70+
msg.push_str(match code {
71+
libc::EPERM => "; the calling process does not have the CAP_SYS_ADMIN capability",
72+
libc::EBUSY => "; new_root or put_old is on the current root mount",
73+
libc::EINVAL => {
74+
"; new_root is not a mount point, put_old is not at or underneath new_root, \
75+
the current root is not a mount point, the current root is on the rootfs, \
76+
or a mount point has propagation type MS_SHARED"
77+
}
78+
libc::ENOTDIR => "; new_root or put_old is not a directory",
79+
libc::EACCES => "; search permission denied for a directory in the path prefix",
80+
libc::EBADF => "; bad file descriptor",
81+
libc::EFAULT => "; new_root or put_old points outside the accessible address space",
82+
libc::ELOOP => "; too many symbolic links encountered while resolving the path",
83+
libc::ENAMETOOLONG => "; new_root or put_old path is too long",
84+
libc::ENOENT => {
85+
"; a component of new_root or put_old does not exist, \
86+
or is a dangling symbolic link"
87+
}
88+
libc::ENOMEM => "; out of kernel memory",
89+
libc::EOVERFLOW => {
90+
"; path refers to a file whose size, inode number, or number of blocks \
91+
cannot be represented"
92+
}
93+
_ => "",
94+
});
95+
}
96+
97+
PivotRootError::SyscallFailed { message: msg, source: err }
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::*;
104+
105+
#[test]
106+
fn test_nul_error_display() {
107+
// Create a NulError via CString::new
108+
let bytes = b"/tmp\0/dir";
109+
let err = std::ffi::CString::new(&bytes[..]).unwrap_err();
110+
let e = PivotRootError::NulError {
111+
which: PathWhich::NewRoot,
112+
pos: err.nul_position(),
113+
source: err,
114+
path: OsString::from("/tmp\u{0}/dir"),
115+
};
116+
let s = e.to_string();
117+
assert!(s.contains("new_root"), "{s}");
118+
assert!(s.contains("null byte"), "{s}");
119+
}
120+
121+
fn msg_for(code: i32) -> String {
122+
let err = std::io::Error::from_raw_os_error(code);
123+
let e = PivotRootError::from(err);
124+
e.to_string()
125+
}
126+
127+
#[test]
128+
fn test_syscall_failed_eperm_hint() {
129+
let s = msg_for(libc::EPERM);
130+
assert!(s.contains("failed to change root"), "{s}");
131+
assert!(s.contains("errno"), "{s}");
132+
assert!(s.contains("CAP_SYS_ADMIN"), "{s}");
133+
}
134+
135+
#[test]
136+
fn test_syscall_failed_ebusy_hint() {
137+
let s = msg_for(libc::EBUSY);
138+
assert!(s.contains("failed to change root"), "{s}");
139+
assert!(s.contains("on the current root mount"), "{s}");
140+
}
141+
142+
#[test]
143+
fn test_syscall_failed_einval_hint() {
144+
let s = msg_for(libc::EINVAL);
145+
assert!(s.contains("failed to change root"), "{s}");
146+
assert!(s.contains("not a mount point"), "{s}");
147+
assert!(s.contains("MS_SHARED"), "{s}");
148+
}
149+
150+
#[test]
151+
fn test_syscall_failed_enotdir_hint() {
152+
let s = msg_for(libc::ENOTDIR);
153+
assert!(s.contains("failed to change root"), "{s}");
154+
assert!(s.contains("not a directory"), "{s}");
155+
}
156+
157+
#[test]
158+
fn test_syscall_failed_eacces_hint() {
159+
let s = msg_for(libc::EACCES);
160+
assert!(s.contains("failed to change root"), "{s}");
161+
assert!(s.contains("permission denied"), "{s}");
162+
}
163+
164+
#[test]
165+
fn test_syscall_failed_ebadf_hint() {
166+
let s = msg_for(libc::EBADF);
167+
assert!(s.contains("failed to change root"), "{s}");
168+
assert!(s.contains("bad file descriptor"), "{s}");
169+
}
170+
171+
#[test]
172+
fn test_syscall_failed_efault_hint() {
173+
let s = msg_for(libc::EFAULT);
174+
assert!(s.contains("failed to change root"), "{s}");
175+
assert!(s.contains("accessible address space"), "{s}");
176+
}
177+
178+
#[test]
179+
fn test_syscall_failed_eloop_hint() {
180+
let s = msg_for(libc::ELOOP);
181+
assert!(s.contains("failed to change root"), "{s}");
182+
assert!(s.contains("symbolic links"), "{s}");
183+
}
184+
185+
#[test]
186+
fn test_syscall_failed_enametoolong_hint() {
187+
let s = msg_for(libc::ENAMETOOLONG);
188+
assert!(s.contains("failed to change root"), "{s}");
189+
assert!(s.contains("path is too long"), "{s}");
190+
}
191+
192+
#[test]
193+
fn test_syscall_failed_enoent_hint() {
194+
let s = msg_for(libc::ENOENT);
195+
assert!(s.contains("failed to change root"), "{s}");
196+
assert!(s.contains("does not exist"), "{s}");
197+
assert!(s.contains("dangling symbolic link"), "{s}");
198+
}
199+
200+
#[test]
201+
fn test_syscall_failed_enomem_hint() {
202+
let s = msg_for(libc::ENOMEM);
203+
assert!(s.contains("failed to change root"), "{s}");
204+
assert!(s.contains("out of kernel memory"), "{s}");
205+
}
206+
207+
#[test]
208+
fn test_syscall_failed_eoverflow_hint() {
209+
let s = msg_for(libc::EOVERFLOW);
210+
assert!(s.contains("failed to change root"), "{s}");
211+
assert!(s.contains("cannot be represented"), "{s}");
212+
}
213+
214+
#[test]
215+
fn test_unsupported_platform_display() {
216+
let s = PivotRootError::UnsupportedPlatform.to_string();
217+
assert!(s.contains("only supported on Linux"), "{s}");
218+
}
219+
}

src/uu/pivot_root/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uucore::bin!(uu_pivot_root);

0 commit comments

Comments
 (0)