Skip to content

Commit 7d703f6

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 7d703f6

File tree

9 files changed

+758
-0
lines changed

9 files changed

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

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)