Skip to content

Commit 183ab28

Browse files
committed
[jailer] add check of --exec-file parameter
Signed-off-by: Takahiro Itazuri <itazur@amazon.com>
1 parent 6450196 commit 183ab28

File tree

5 files changed

+147
-26
lines changed

5 files changed

+147
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
network interfaces. This address can be obtained as part of the GET
1818
`/vm/config`.
1919

20+
### Changed
21+
22+
- Changed the jailer option `--exec-file` to fail if the filename does not
23+
contain the string `firecracker` to prevent from running non-firecracker
24+
binaries.
25+
2026
### Fixed
2127

2228
- Make the `T2` template more robust by explicitly disabling additional

docs/jailer.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ jailer --id <id> \
3232
- `id` is the unique VM identification string, which may contain alphanumeric
3333
characters and hyphens. The maximum `id` length is currently 64 characters.
3434
- `exec_file` is the path to the Firecracker binary that will be exec-ed by the
35-
jailer. The user can provide a path to any binary, but the interaction with
36-
the jailer is mostly Firecracker specific.
35+
jailer. The filename must include the string `firecracker`. This is enforced
36+
because the interaction with the jailer is Firecracker specific.
3737
- `uid` and `gid` are the uid and gid the jailer switches to as it execs the
3838
target binary.
3939
- `parent-cgroup` is used to allow the placement of microvm cgroups in custom

src/jailer/src/env.rs

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,7 @@ impl Env {
120120
let exec_file = arguments
121121
.single_value("exec-file")
122122
.ok_or_else(|| Error::ArgumentParsing(MissingValue("exec-file".to_string())))?;
123-
let exec_file_path = canonicalize(&exec_file)
124-
.map_err(|err| Error::Canonicalize(PathBuf::from(&exec_file), err))?;
125-
126-
if !exec_file_path.is_file() {
127-
return Err(Error::NotAFile(exec_file_path));
128-
}
129-
130-
let exec_file_name = exec_file_path
131-
.file_name()
132-
.ok_or_else(|| Error::FileName(exec_file_path.clone()))?;
123+
let (exec_file_path, exec_file_name) = Env::validate_exec_file(exec_file)?;
133124

134125
let chroot_base = arguments
135126
.single_value("chroot-base-dir")
@@ -169,7 +160,7 @@ impl Env {
169160
let mut cgroups: Vec<Box<dyn Cgroup>> = Vec::new();
170161
let parent_cgroup = match arguments.single_value("parent-cgroup") {
171162
Some(parent_cg) => Path::new(parent_cg),
172-
None => Path::new(exec_file_name),
163+
None => Path::new(&exec_file_name),
173164
};
174165
if parent_cgroup
175166
.components()
@@ -247,6 +238,29 @@ impl Env {
247238
self.uid
248239
}
249240

241+
fn validate_exec_file(exec_file: &str) -> Result<(PathBuf, String)> {
242+
let exec_file_path = canonicalize(exec_file)
243+
.map_err(|err| Error::Canonicalize(PathBuf::from(exec_file), err))?;
244+
245+
if !exec_file_path.is_file() {
246+
return Err(Error::NotAFile(exec_file_path));
247+
}
248+
249+
let exec_file_name = exec_file_path
250+
.file_name()
251+
.ok_or_else(|| Error::ExtractFileName(exec_file_path.clone()))?
252+
.to_str()
253+
// Safe to unwrap as the original `exec_file` is `String`.
254+
.unwrap()
255+
.to_string();
256+
257+
if !exec_file_name.contains("firecracker") {
258+
return Err(Error::ExecFileName(exec_file_name));
259+
}
260+
261+
Ok((exec_file_path, exec_file_name))
262+
}
263+
250264
fn parse_resource_limits(resource_limits: &mut ResourceLimits, args: &[String]) -> Result<()> {
251265
for arg in args {
252266
let (name, value) = arg
@@ -293,7 +307,7 @@ impl Env {
293307
fn save_exec_file_pid(&mut self, pid: i32, chroot_exec_file: PathBuf) -> Result<()> {
294308
let chroot_exec_file_str = chroot_exec_file
295309
.to_str()
296-
.ok_or_else(|| Error::FileName(chroot_exec_file.clone()))?;
310+
.ok_or_else(|| Error::ExtractFileName(chroot_exec_file.clone()))?;
297311
let pid_file_path =
298312
PathBuf::from(format!("{}{}", chroot_exec_file_str, PID_FILE_EXTENSION));
299313
let mut pid_file = OpenOptions::new()
@@ -363,7 +377,7 @@ impl Env {
363377
let exec_file_name = self
364378
.exec_file_path
365379
.file_name()
366-
.ok_or_else(|| Error::FileName(self.exec_file_path.clone()))?;
380+
.ok_or_else(|| Error::ExtractFileName(self.exec_file_path.clone()))?;
367381
// We do a quick push here to get the global path of the executable inside the chroot,
368382
// without having to create a new PathBuf. We'll then do a pop to revert to the actual
369383
// chroot_dir right after the copy.
@@ -620,6 +634,8 @@ mod tests {
620634
use crate::build_arg_parser;
621635
use crate::cgroup::test_util::MockCgroupFs;
622636

637+
const PSEUDO_EXEC_FILE_PATH: &str = "/tmp/pseudo_firecracker_exec_file";
638+
623639
#[derive(Clone)]
624640
struct ArgVals<'a> {
625641
pub id: &'a str,
@@ -637,9 +653,10 @@ mod tests {
637653

638654
impl ArgVals<'_> {
639655
pub fn new() -> ArgVals<'static> {
656+
File::create(PSEUDO_EXEC_FILE_PATH).unwrap();
640657
ArgVals {
641658
id: "bd65600d-8669-4903-8a14-af88203add38",
642-
exec_file: "/proc/cpuinfo",
659+
exec_file: PSEUDO_EXEC_FILE_PATH,
643660
uid: "1001",
644661
gid: "1002",
645662
chroot_base: "/",
@@ -876,6 +893,49 @@ mod tests {
876893
}
877894
}
878895

896+
#[test]
897+
fn test_validate_exec_file() {
898+
// Success case
899+
File::create(PSEUDO_EXEC_FILE_PATH).unwrap();
900+
assert!(Env::validate_exec_file(PSEUDO_EXEC_FILE_PATH).is_ok());
901+
902+
// Error case 1: No such file exists
903+
std::fs::remove_file(PSEUDO_EXEC_FILE_PATH).unwrap();
904+
assert_eq!(
905+
format!(
906+
"{}",
907+
Env::validate_exec_file(PSEUDO_EXEC_FILE_PATH).unwrap_err()
908+
),
909+
format!(
910+
"Failed to canonicalize path {}: No such file or directory (os error 2)",
911+
PSEUDO_EXEC_FILE_PATH
912+
)
913+
);
914+
915+
// Error case 2: Not a file
916+
std::fs::create_dir_all("/tmp/firecracker_test_dir").unwrap();
917+
assert_eq!(
918+
format!(
919+
"{}",
920+
Env::validate_exec_file("/tmp/firecracker_test_dir").unwrap_err()
921+
),
922+
"/tmp/firecracker_test_dir is not a file"
923+
);
924+
925+
// Error case 3: Filename without "firecracker"
926+
File::create("/tmp/firecracker_test_dir/foobarbaz").unwrap();
927+
assert_eq!(
928+
format!(
929+
"{}",
930+
Env::validate_exec_file("/tmp/firecracker_test_dir/foobarbaz").unwrap_err()
931+
),
932+
"Invalid filename. The filename of `--exec-file` option must contain \"firecracker\": \
933+
foobarbaz"
934+
);
935+
std::fs::remove_file("/tmp/firecracker_test_dir/foobarbaz").unwrap();
936+
std::fs::remove_dir_all("/tmp/firecracker_test_dir").unwrap();
937+
}
938+
879939
#[test]
880940
fn test_setup_jailed_folder() {
881941
let mut mock_cgroups = MockCgroupFs::new().unwrap();
@@ -986,15 +1046,15 @@ mod tests {
9861046
assert!(!mock_cgroups.add_v1_mounts().is_err());
9871047

9881048
// Create tmp resources for `exec_file` and `chroot_base`.
989-
let some_file = TempFile::new_with_prefix("/tmp/").unwrap();
990-
let some_file_path = some_file.as_path().to_str().unwrap();
991-
let some_file_name = some_file.as_path().file_name().unwrap();
1049+
File::create(PSEUDO_EXEC_FILE_PATH).unwrap();
1050+
let exec_file_path = PSEUDO_EXEC_FILE_PATH;
1051+
let exec_file_name = Path::new(exec_file_path).file_name().unwrap();
9921052
let some_dir = TempDir::new().unwrap();
9931053
let some_dir_path = some_dir.as_path().to_str().unwrap();
9941054

9951055
let some_arg_vals = ArgVals {
9961056
id: "bd65600d-8669-4903-8a14-af88203add38",
997-
exec_file: some_file_path,
1057+
exec_file: exec_file_path,
9981058
uid: "1001",
9991059
gid: "1002",
10001060
chroot_base: some_dir_path,
@@ -1005,7 +1065,7 @@ mod tests {
10051065
resource_limits: Vec::new(),
10061066
parent_cgroup: None,
10071067
};
1008-
fs::write(some_file_path, "some_content").unwrap();
1068+
fs::write(exec_file_path, "some_content").unwrap();
10091069
args.parse(&make_args(&some_arg_vals)).unwrap();
10101070
let mut env = Env::new(&args, 0, 0).unwrap();
10111071

@@ -1014,10 +1074,10 @@ mod tests {
10141074

10151075
assert_eq!(
10161076
env.copy_exec_to_chroot().unwrap(),
1017-
some_file_name.to_os_string()
1077+
exec_file_name.to_os_string()
10181078
);
10191079

1020-
let dest_path = env.chroot_dir.join(some_file_name);
1080+
let dest_path = env.chroot_dir.join(exec_file_name);
10211081
// Check that `fs::copy()` copied src content and permission bits to destination.
10221082
let metadata_src = fs::metadata(&env.exec_file_path).unwrap();
10231083
let metadata_dest = fs::metadata(&dest_path).unwrap();

src/jailer/src/main.rs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ pub enum Error {
3838
CStringParsing(NulError),
3939
Dup2(io::Error),
4040
Exec(io::Error),
41-
FileName(PathBuf),
41+
ExecFileName(String),
42+
ExtractFileName(PathBuf),
4243
FileOpen(PathBuf, io::Error),
4344
FromBytesWithNul(std::ffi::FromBytesWithNulError),
4445
GetOldFdFlags(io::Error),
@@ -140,7 +141,13 @@ impl fmt::Display for Error {
140141
CStringParsing(_) => write!(f, "Encountered interior \\0 while parsing a string"),
141142
Dup2(ref err) => write!(f, "Failed to duplicate fd: {}", err),
142143
Exec(ref err) => write!(f, "Failed to exec into Firecracker: {}", err),
143-
FileName(ref path) => write!(
144+
ExecFileName(ref filename) => write!(
145+
f,
146+
"Invalid filename. The filename of `--exec-file` option must contain \
147+
\"firecracker\": {}",
148+
filename
149+
),
150+
ExtractFileName(ref path) => write!(
144151
f,
145152
"{}",
146153
format!("Failed to extract filename from path {:?}", path).replace("\"", "")
@@ -609,7 +616,12 @@ mod tests {
609616
format!("Failed to exec into Firecracker: {}", err2_str)
610617
);
611618
assert_eq!(
612-
format!("{}", Error::FileName(file_path.clone())),
619+
format!("{}", Error::ExecFileName("foobarbaz".to_string())),
620+
"Invalid filename. The filename of `--exec-file` option must contain \"firecracker\": \
621+
foobarbaz",
622+
);
623+
assert_eq!(
624+
format!("{}", Error::ExtractFileName(file_path.clone())),
613625
"Failed to extract filename from path /foo/bar",
614626
);
615627
assert_eq!(

tests/integration_tests/security/test_jail.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,49 @@ def test_empty_jailer_id(test_microvm_with_api):
104104
assert expected_err in str(err)
105105

106106

107+
def test_exec_file_not_exist(test_microvm_with_api, tmp_path):
108+
"""
109+
Test the jailer option `--exec-file`
110+
111+
@type: security
112+
"""
113+
test_microvm = test_microvm_with_api
114+
115+
# Error case 1: No such file exists
116+
pseudo_exec_file_path = tmp_path / "pseudo_firecracker_exec_file"
117+
test_microvm.jailer.exec_file = pseudo_exec_file_path
118+
119+
with pytest.raises(
120+
Exception,
121+
match=rf"Jailer error: Failed to canonicalize path {pseudo_exec_file_path}:"
122+
rf" No such file or directory \(os error 2\)",
123+
):
124+
test_microvm.spawn()
125+
126+
# Error case 2: Not a file
127+
pseudo_exec_dir_path = tmp_path / "firecracker_test_dir"
128+
pseudo_exec_dir_path.mkdir()
129+
test_microvm.jailer.exec_file = pseudo_exec_dir_path
130+
131+
with pytest.raises(
132+
Exception,
133+
match=rf"Jailer error: {pseudo_exec_dir_path} is not a file",
134+
):
135+
test_microvm.spawn()
136+
137+
# Error case 3: Filename without "firecracker"
138+
pseudo_exec_file_path = tmp_path / "foobarbaz"
139+
pseudo_exec_file_path.touch()
140+
test_microvm.jailer.exec_file = pseudo_exec_file_path
141+
142+
with pytest.raises(
143+
Exception,
144+
match=r"Jailer error: Invalid filename. The filename of `--exec-file` option"
145+
r' must contain "firecracker": foobarbaz',
146+
):
147+
test_microvm.spawn()
148+
149+
107150
def test_default_chroot_hierarchy(test_microvm_with_initrd):
108151
"""
109152
Test the folder hierarchy created by default by the jailer.

0 commit comments

Comments
 (0)