Skip to content

Commit bf26d3f

Browse files
committed
move the validation logic into executor
To allow more flexibility for the executor, we move the validate logic into the executor. The validate runs in the `create` step before workloads are executed. Instead of implementing the validation in the `exec`, to maintain backward competiability, we have to introduce an extra step. The exec is too late to fail if the spec is not validated. Signed-off-by: yihuaf <[email protected]>
1 parent 8eccb32 commit bf26d3f

File tree

7 files changed

+174
-115
lines changed

7 files changed

+174
-115
lines changed

crates/libcontainer/src/process/container_init_process.rs

Lines changed: 1 addition & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -76,72 +76,6 @@ pub enum InitProcessError {
7676

7777
type Result<T> = std::result::Result<T, InitProcessError>;
7878

79-
fn get_executable_path(name: &str, path_var: &str) -> Option<PathBuf> {
80-
// if path has / in it, we have to assume absolute path, as per runc impl
81-
if name.contains('/') && PathBuf::from(name).exists() {
82-
return Some(PathBuf::from(name));
83-
}
84-
for path in path_var.split(':') {
85-
let potential_path = PathBuf::from(path).join(name);
86-
if potential_path.exists() {
87-
return Some(potential_path);
88-
}
89-
}
90-
None
91-
}
92-
93-
fn is_executable(path: &Path) -> std::result::Result<bool, std::io::Error> {
94-
use std::os::unix::fs::PermissionsExt;
95-
let metadata = path.metadata()?;
96-
let permissions = metadata.permissions();
97-
// we have to check if the path is file and the execute bit
98-
// is set. In case of directories, the execute bit is also set,
99-
// so have to check if this is a file or not
100-
Ok(metadata.is_file() && permissions.mode() & 0o001 != 0)
101-
}
102-
103-
// this checks if the binary to run actually exists and if we have
104-
// permissions to run it. Taken from
105-
// https://github.com/opencontainers/runc/blob/25c9e888686773e7e06429133578038a9abc091d/libcontainer/standard_init_linux.go#L195-L206
106-
fn verify_binary(args: &[String], envs: &[String]) -> Result<()> {
107-
let path_vars: Vec<&String> = envs.iter().filter(|&e| e.starts_with("PATH=")).collect();
108-
if path_vars.is_empty() {
109-
tracing::error!("PATH environment variable is not set");
110-
return Err(InitProcessError::InvalidExecutable(args[0].clone()));
111-
}
112-
let path_var = path_vars[0].trim_start_matches("PATH=");
113-
match get_executable_path(&args[0], path_var) {
114-
None => {
115-
tracing::error!(
116-
"executable {} for container process not found in PATH",
117-
args[0]
118-
);
119-
return Err(InitProcessError::InvalidExecutable(args[0].clone()));
120-
}
121-
Some(path) => match is_executable(&path) {
122-
Ok(true) => {
123-
tracing::debug!("found executable {:?}", path);
124-
}
125-
Ok(false) => {
126-
tracing::error!(
127-
"executable {:?} does not have the correct permission set",
128-
path
129-
);
130-
return Err(InitProcessError::InvalidExecutable(args[0].clone()));
131-
}
132-
Err(err) => {
133-
tracing::error!(
134-
"failed to check permissions for executable {:?}: {}",
135-
path,
136-
err
137-
);
138-
return Err(InitProcessError::Io(err));
139-
}
140-
},
141-
}
142-
Ok(())
143-
}
144-
14579
fn sysctl(kernel_params: &HashMap<String, String>) -> Result<()> {
14680
let sys = PathBuf::from("/proc/sys");
14781
for (kernel_param, value) in kernel_params {
@@ -637,9 +571,7 @@ pub fn container_init_process(
637571
tracing::warn!("seccomp not available, unable to set seccomp privileges!")
638572
}
639573

640-
if let Some(args) = proc.args() {
641-
verify_binary(args, &envs)?;
642-
}
574+
args.executor.validate(spec)?;
643575

644576
// Notify main process that the init process is ready to execute the
645577
// payload. Note, because we are already inside the pid namespace, the pid
@@ -1091,44 +1023,6 @@ mod tests {
10911023
assert_eq!(0, got.len());
10921024
}
10931025

1094-
#[test]
1095-
fn test_get_executable_path() {
1096-
let non_existing_abs_path = "/some/non/existent/absolute/path";
1097-
let existing_abs_path = "/usr/bin/sh";
1098-
let existing_binary = "sh";
1099-
let non_existing_binary = "non-existent";
1100-
let path_value = "/usr/bin:/bin";
1101-
1102-
assert_eq!(
1103-
get_executable_path(existing_abs_path, path_value),
1104-
Some(PathBuf::from(existing_abs_path))
1105-
);
1106-
assert_eq!(get_executable_path(non_existing_abs_path, path_value), None);
1107-
1108-
assert_eq!(
1109-
get_executable_path(existing_binary, path_value),
1110-
Some(PathBuf::from("/usr/bin/sh"))
1111-
);
1112-
1113-
assert_eq!(get_executable_path(non_existing_binary, path_value), None);
1114-
}
1115-
1116-
#[test]
1117-
fn test_is_executable() {
1118-
let tmp = tempfile::tempdir().expect("create temp directory for test");
1119-
let executable_path = PathBuf::from("/bin/sh");
1120-
let directory_path = tmp.path();
1121-
let non_executable_path = directory_path.join("non_executable_file");
1122-
let non_existent_path = PathBuf::from("/some/non/existent/path");
1123-
1124-
std::fs::File::create(non_executable_path.as_path()).unwrap();
1125-
1126-
assert!(is_executable(&non_existent_path).is_err());
1127-
assert!(is_executable(&executable_path).unwrap());
1128-
assert!(!is_executable(&non_executable_path).unwrap());
1129-
assert!(!is_executable(directory_path).unwrap());
1130-
}
1131-
11321026
#[test]
11331027
fn test_set_io_priority() {
11341028
let test_command = TestHelperSyscall::default();

crates/libcontainer/src/workload/default.rs

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
use std::ffi::CString;
1+
use std::{
2+
ffi::CString,
3+
path::{Path, PathBuf},
4+
};
25

36
use nix::unistd;
47
use oci_spec::runtime::Spec;
58

6-
use super::{Executor, ExecutorError, EMPTY};
9+
use super::{Executor, ExecutorError};
710

811
#[derive(Clone)]
912
pub struct DefaultExecutor {}
@@ -15,12 +18,10 @@ impl Executor for DefaultExecutor {
1518
.process()
1619
.as_ref()
1720
.and_then(|p| p.args().as_ref())
18-
.unwrap_or(&EMPTY);
19-
20-
if args.is_empty() {
21-
tracing::error!("no arguments provided to execute");
22-
Err(ExecutorError::InvalidArg)?;
23-
}
21+
.ok_or_else(|| {
22+
tracing::error!("no arguments provided to execute");
23+
ExecutorError::InvalidArg
24+
})?;
2425

2526
let executable = args[0].as_str();
2627
let cstring_path = CString::new(executable.as_bytes()).map_err(|err| {
@@ -40,8 +41,123 @@ impl Executor for DefaultExecutor {
4041
// payload through execvp, so it should never reach here.
4142
unreachable!();
4243
}
44+
45+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError> {
46+
let proc = spec
47+
.process()
48+
.as_ref()
49+
.ok_or_else(|| ExecutorError::InvalidArg)?;
50+
51+
if let Some(args) = proc.args() {
52+
let envs: Vec<String> = proc.env().as_ref().unwrap_or(&vec![]).clone();
53+
let path_vars: Vec<&String> = envs.iter().filter(|&e| e.starts_with("PATH=")).collect();
54+
if path_vars.is_empty() {
55+
tracing::error!("PATH environment variable is not set");
56+
Err(ExecutorError::InvalidArg)?;
57+
}
58+
let path_var = path_vars[0].trim_start_matches("PATH=");
59+
match get_executable_path(&args[0], path_var) {
60+
None => {
61+
tracing::error!(
62+
"executable {} for container process not found in PATH",
63+
args[0]
64+
);
65+
Err(ExecutorError::InvalidArg)?;
66+
}
67+
Some(path) => match is_executable(&path) {
68+
Ok(true) => {
69+
tracing::debug!("found executable {:?}", path);
70+
}
71+
Ok(false) => {
72+
tracing::error!(
73+
"executable {:?} does not have the correct permission set",
74+
path
75+
);
76+
Err(ExecutorError::InvalidArg)?;
77+
}
78+
Err(err) => {
79+
tracing::error!(
80+
"failed to check permissions for executable {:?}: {}",
81+
path,
82+
err
83+
);
84+
Err(ExecutorError::InvalidArg)?;
85+
}
86+
},
87+
}
88+
}
89+
90+
Ok(())
91+
}
4392
}
4493

4594
pub fn get_executor() -> Box<dyn Executor> {
4695
Box::new(DefaultExecutor {})
4796
}
97+
98+
fn get_executable_path(name: &str, path_var: &str) -> Option<PathBuf> {
99+
// if path has / in it, we have to assume absolute path, as per runc impl
100+
if name.contains('/') && PathBuf::from(name).exists() {
101+
return Some(PathBuf::from(name));
102+
}
103+
for path in path_var.split(':') {
104+
let potential_path = PathBuf::from(path).join(name);
105+
if potential_path.exists() {
106+
return Some(potential_path);
107+
}
108+
}
109+
None
110+
}
111+
112+
fn is_executable(path: &Path) -> std::result::Result<bool, std::io::Error> {
113+
use std::os::unix::fs::PermissionsExt;
114+
let metadata = path.metadata()?;
115+
let permissions = metadata.permissions();
116+
// we have to check if the path is file and the execute bit
117+
// is set. In case of directories, the execute bit is also set,
118+
// so have to check if this is a file or not
119+
Ok(metadata.is_file() && permissions.mode() & 0o001 != 0)
120+
}
121+
122+
#[cfg(test)]
123+
mod tests {
124+
use super::*;
125+
126+
#[test]
127+
fn test_get_executable_path() {
128+
let non_existing_abs_path = "/some/non/existent/absolute/path";
129+
let existing_abs_path = "/usr/bin/sh";
130+
let existing_binary = "sh";
131+
let non_existing_binary = "non-existent";
132+
let path_value = "/usr/bin:/bin";
133+
134+
assert_eq!(
135+
get_executable_path(existing_abs_path, path_value),
136+
Some(PathBuf::from(existing_abs_path))
137+
);
138+
assert_eq!(get_executable_path(non_existing_abs_path, path_value), None);
139+
140+
assert_eq!(
141+
get_executable_path(existing_binary, path_value),
142+
Some(PathBuf::from("/usr/bin/sh"))
143+
);
144+
145+
assert_eq!(get_executable_path(non_existing_binary, path_value), None);
146+
}
147+
148+
#[test]
149+
fn test_is_executable() {
150+
let tmp = tempfile::tempdir().expect("create temp directory for test");
151+
let executable_path = PathBuf::from("/bin/sh");
152+
let directory_path = tmp.path();
153+
let non_executable_path = directory_path.join("non_executable_file");
154+
let non_existent_path = PathBuf::from("/some/non/existent/path");
155+
156+
std::fs::File::create(non_executable_path.as_path()).unwrap();
157+
158+
assert!(is_executable(&non_existent_path).is_err());
159+
assert!(is_executable(&executable_path).unwrap());
160+
assert!(!is_executable(&non_executable_path).unwrap());
161+
assert!(!is_executable(directory_path).unwrap());
162+
}
163+
}

crates/libcontainer/src/workload/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub trait CloneBoxExecutor {
4646
pub trait Executor: CloneBoxExecutor {
4747
/// Executes the workload
4848
fn exec(&self, spec: &Spec) -> Result<(), ExecutorError>;
49+
50+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError>;
4951
}
5052

5153
impl<T> CloneBoxExecutor for T

crates/youki/src/workload/executor.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,29 @@ impl Executor for DefaultExecutor {
2929
// container workloads.
3030
libcontainer::workload::default::get_executor().exec(spec)
3131
}
32+
33+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError> {
34+
#[cfg(feature = "wasm-wasmer")]
35+
match super::wasmer::get_executor().validate(spec) {
36+
Ok(_) => return Ok(()),
37+
Err(ExecutorError::CantHandle(_)) => (),
38+
Err(err) => return Err(err),
39+
}
40+
#[cfg(feature = "wasm-wasmedge")]
41+
match super::wasmedge::get_executor().validate(spec) {
42+
Ok(_) => return Ok(()),
43+
Err(ExecutorError::CantHandle(_)) => (),
44+
Err(err) => return Err(err),
45+
}
46+
#[cfg(feature = "wasm-wasmtime")]
47+
match super::wasmtime::get_executor().validate(spec) {
48+
Ok(_) => return Ok(()),
49+
Err(ExecutorError::CantHandle(_)) => (),
50+
Err(err) => return Err(err),
51+
}
52+
53+
libcontainer::workload::default::get_executor().validate(spec)
54+
}
3255
}
3356

3457
pub fn default_executor() -> DefaultExecutor {

crates/youki/src/workload/wasmedge.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ impl Executor for WasmedgeExecutor {
6262

6363
Ok(())
6464
}
65+
66+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError> {
67+
if !can_handle(spec) {
68+
return Err(ExecutorError::CantHandle(EXECUTOR_NAME));
69+
}
70+
71+
Ok(())
72+
}
6573
}
6674

6775
pub fn get_executor() -> WasmedgeExecutor {

crates/youki/src/workload/wasmer.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ impl Executor for WasmerExecutor {
8080

8181
Ok(())
8282
}
83+
84+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError> {
85+
if !can_handle(spec) {
86+
return Err(ExecutorError::CantHandle(EXECUTOR_NAME));
87+
}
88+
89+
Ok(())
90+
}
8391
}
8492

8593
pub fn get_executor() -> WasmerExecutor {

crates/youki/src/workload/wasmtime.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ impl Executor for WasmtimeExecutor {
9090
.call(&mut store, &[], &mut [])
9191
.map_err(|err| ExecutorError::Execution(err.into()))
9292
}
93+
94+
fn validate(&self, spec: &Spec) -> Result<(), ExecutorError> {
95+
if !can_handle(spec) {
96+
return Err(ExecutorError::CantHandle(EXECUTOR_NAME));
97+
}
98+
99+
Ok(())
100+
}
93101
}
94102

95103
pub fn get_executor() -> WasmtimeExecutor {

0 commit comments

Comments
 (0)