Skip to content

Measure jailer startup performance #5282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ and this project adheres to
- [#5165](https://github.com/firecracker-microvm/firecracker/pull/5165): Changed
Firecracker snapshot feature from developer preview to generally available.
Incremental snapshots remain in developer preview.
- [#5282](https://github.com/firecracker-microvm/firecracker/pull/5282): Update
jailer executable requrements. Now jailer does require for an executable to
have exec permissions set, but does not require it's name to contain
`firecracker`.

### Deprecated

Expand Down
67 changes: 33 additions & 34 deletions src/jailer/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,14 @@ impl Env {
let exec_file_path = canonicalize(exec_file)
.map_err(|err| JailerError::Canonicalize(PathBuf::from(exec_file), err))?;

if !exec_file_path.is_file() {
// Safety: the `canonicalize` call above ensured the existance of the path.
let metadata = std::fs::metadata(&exec_file_path).unwrap();

// Check that it is executable
if metadata.permissions().mode() & 0o111 == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this requiring that's executable by all users, while in reality it just needs to be executable by the jailer? Is there any issues with just having the execve fail with permission error?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks that there is at least some executable bit set.
Personally, I added this as I though it was already a requirement, just not explicitly checked and failing with a bit nicer message is better. I can drop it if people find it not necessary.

return Err(JailerError::NotExecutable(exec_file_path));
}
if !metadata.is_file() {
return Err(JailerError::NotAFile(exec_file_path));
}

Expand All @@ -301,10 +308,6 @@ impl Env {
.unwrap()
.to_string();

if !exec_file_name.contains("firecracker") {
return Err(JailerError::ExecFileName(exec_file_name));
}

Ok((exec_file_path, exec_file_name))
}

Expand Down Expand Up @@ -786,7 +789,8 @@ mod tests {
pub fn new(pseudo_exec_file_path: &'a str) -> ArgVals<'a> {
let pseudo_exec_file_dir = Path::new(&pseudo_exec_file_path).parent().unwrap();
fs::create_dir_all(pseudo_exec_file_dir).unwrap();
File::create(pseudo_exec_file_path).unwrap();
create_exec_file(pseudo_exec_file_path);

ArgVals {
id: "bd65600d-8669-4903-8a14-af88203add38",
exec_file: pseudo_exec_file_path,
Expand All @@ -803,6 +807,13 @@ mod tests {
}
}

fn create_exec_file(pseudo_exec_file_path: &str) {
let exec = File::create(pseudo_exec_file_path).unwrap();
let mut perms = exec.metadata().unwrap().permissions();
perms.set_mode(0o111);
exec.set_permissions(perms).unwrap();
}

fn make_args(arg_vals: &ArgVals) -> Vec<String> {
let mut arg_vec = vec![
"--binary-name",
Expand Down Expand Up @@ -1022,43 +1033,31 @@ mod tests {
let pseudo_exec_file_path = get_pseudo_exec_file_path();
let pseudo_exec_file_dir = Path::new(&pseudo_exec_file_path).parent().unwrap();
create_dir_all(pseudo_exec_file_dir).unwrap();
File::create(&pseudo_exec_file_path).unwrap();
create_exec_file(&pseudo_exec_file_path);
Env::validate_exec_file(&pseudo_exec_file_path).unwrap();

// Error case 1: No such file exists
std::fs::remove_file(&pseudo_exec_file_path).unwrap();
assert_eq!(
format!(
"{}",
Env::validate_exec_file(&pseudo_exec_file_path).unwrap_err()
),
format!(
"Failed to canonicalize path {}: No such file or directory (os error 2)",
pseudo_exec_file_path
)
);
assert!(matches!(
Env::validate_exec_file("/tmp/firecracker_test_dir/foobarbaz"),
Err(JailerError::Canonicalize(_, _))
));

// Error case 2: Not a file
std::fs::create_dir_all("/tmp/firecracker_test_dir").unwrap();
assert_eq!(
format!(
"{}",
Env::validate_exec_file("/tmp/firecracker_test_dir").unwrap_err()
),
"/tmp/firecracker_test_dir is not a file"
);
assert!(matches!(
Env::validate_exec_file("/tmp/firecracker_test_dir"),
Err(JailerError::NotAFile(_))
));

// Error case 3: Filename without "firecracker"
// Error case 3: Not an executable
File::create("/tmp/firecracker_test_dir/foobarbaz").unwrap();
assert_eq!(
format!(
"{}",
Env::validate_exec_file("/tmp/firecracker_test_dir/foobarbaz").unwrap_err()
),
"Invalid filename. The filename of `--exec-file` option must contain \"firecracker\": \
foobarbaz"
);
assert!(matches!(
Env::validate_exec_file("/tmp/firecracker_test_dir/foobarbaz"),
Err(JailerError::NotExecutable(_))
));
std::fs::remove_file("/tmp/firecracker_test_dir/foobarbaz").unwrap();

std::fs::remove_dir_all("/tmp/firecracker_test_dir").unwrap();
}

Expand Down Expand Up @@ -1196,7 +1195,7 @@ mod tests {
let exec_file_path = get_pseudo_exec_file_path();
let exec_file_dir = Path::new(&exec_file_path).parent().unwrap();
fs::create_dir_all(exec_file_dir).unwrap();
File::create(&exec_file_path).unwrap();
create_exec_file(&exec_file_path);
let some_dir = TempDir::new().unwrap();
let some_dir_path = some_dir.as_path().to_str().unwrap();

Expand Down
9 changes: 3 additions & 6 deletions src/jailer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,6 @@
Dup2(io::Error),
#[error("Failed to exec into Firecracker: {0}")]
Exec(io::Error),
#[error(
"Invalid filename. The filename of `--exec-file` option must contain \"firecracker\": {0}"
)]
ExecFileName(String),
#[error("{}", format!("Failed to extract filename from path {:?}", .0).replace('\"', ""))]
ExtractFileName(PathBuf),
#[error("{}", format!("Failed to open file {:?}: {}", .0, .1).replace('\"', ""))]
Expand All @@ -103,6 +99,8 @@
MountBind(io::Error),
#[error("Failed to change the propagation type to slave: {0}")]
MountPropagationSlave(io::Error),
#[error("{}", format!("{:?} is not executable", .0).replace('\"', ""))]
NotExecutable(PathBuf),
#[error("{}", format!("{:?} is not a file", .0).replace('\"', ""))]
NotAFile(PathBuf),
#[error("{}", format!("{:?} is not a directory", .0).replace('\"', ""))]
Expand Down Expand Up @@ -351,8 +349,7 @@
fs::create_dir_all(env.chroot_dir())
.map_err(|err| JailerError::CreateDir(env.chroot_dir().to_owned(), err))?;
env.run()
})
.unwrap_or_else(|err| panic!("Jailer error: {}", err));
})?;

Check warning on line 352 in src/jailer/src/main.rs

View check run for this annotation

Codecov / codecov/patch

src/jailer/src/main.rs#L352

Added line #L352 was not covered by tests
Ok(())
}

Expand Down
11 changes: 11 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,17 @@ def msr_reader_bin(test_fc_session_root_path):
yield msr_reader_bin_path


@pytest.fixture(scope="session")
def jailer_time_bin(test_fc_session_root_path):
"""Build a binary that fakes fc"""
jailer_time_bin_path = os.path.join(test_fc_session_root_path, "jailer_time")
build_tools.gcc_compile(
"host_tools/jailer_time.c",
jailer_time_bin_path,
)
yield jailer_time_bin_path


@pytest.fixture
def bin_seccomp_paths():
"""Build jailers and jailed binaries to test seccomp.
Expand Down
19 changes: 19 additions & 0 deletions tests/host_tools/jailer_time.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// This is used by `performance/test_jailer.py`

#include <stdio.h>
#include <time.h>

int main(int argc, char** argv) {
// print current time in us
struct timespec now = {0};
clock_gettime(CLOCK_MONOTONIC, &now);
unsigned long long current_ns = (unsigned long long)now.tv_sec * 1000000000 + (unsigned long long)now.tv_nsec;
unsigned long long current_us = current_ns / 1000;
printf("%llu\n", current_us);

// print the --start-time-us value
printf("%s", argv[4]);
}
81 changes: 81 additions & 0 deletions tests/integration_tests/performance/test_jailer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0
"""Performance benchmark for the jailer."""

import os
import shutil
import subprocess

import pytest

from framework.jailer import DEFAULT_CHROOT_PATH, JailerContext


@pytest.mark.nonci
@pytest.mark.parametrize("jailers", [1, 100, 300, 500])
@pytest.mark.parametrize("mounts", [0, 100, 300, 500])
def test_jailer_startup(jailer_time_bin, microvm_factory, jailers, mounts, metrics):
"""
Test the overhead of jailer startup without and with bind mounts
"""

jailer_binary = microvm_factory.jailer_binary_path

# Create bind mount points. The exact location of them
# does not matter, they just need to exist.
mounts_paths = "/tmp/mounts"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't really matter as perf tests are run sequentially, but you could use a tempdir from mktemp (iirc we have a util for that already that also takes care of cleaning up).

os.makedirs(mounts_paths)
for m in range(mounts):
mount_path = f"{mounts_paths}/mount{m}"
os.makedirs(mount_path)
subprocess.run(
["mount", "--bind", f"{mount_path}", f"{mount_path}"], check=True
)

metrics.set_dimensions(
{
"performance_test": "test_boottime",
"jailers": jailers,
"mounts": mounts,
}
)

# Testing 1 jailer will give 1 data point which is not enough,
# so do 100 runs in this case.
if jailers == 1:
iterations = 100
else:
iterations = 1

for i in range(iterations):
processes = []
for j in range(jailers):
jailer = JailerContext(
jailer_id=f"fakefc{i}{j}",
exec_file=jailer_time_bin,
# Don't deamonize to get the stdout
daemonize=False,
)
jailer.setup()

cmd = [str(jailer_binary), *jailer.construct_param_list()]
processes.append(
subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
)
)

for p in processes:
r = p.communicate()[0]
e, s = r.split()
metrics.put_metric(
"startup",
int(e) - int(s),
unit="Microseconds",
)

# Cleanup mounts and jailer dirs
for d in os.listdir(mounts_paths):
subprocess.run(["umount", f"{mounts_paths}/{d}"], check=True)
shutil.rmtree(mounts_paths)
shutil.rmtree(DEFAULT_CHROOT_PATH)
11 changes: 5 additions & 6 deletions tests/integration_tests/security/test_jail.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def test_empty_jailer_id(uvm_plain):
# we can set an empty ID.
with pytest.raises(
ChildProcessError,
match=r"Jailer error: Invalid instance ID: Invalid len \(0\); the length must be between 1 and 64",
match=r"Invalid instance ID: Invalid len \(0\); the length must be between 1 and 64",
):
test_microvm.spawn()

Expand All @@ -88,7 +88,7 @@ def test_exec_file_not_exist(uvm_plain, tmp_path):

with pytest.raises(
Exception,
match=rf"Jailer error: Failed to canonicalize path {pseudo_exec_file_path}:"
match=rf"Failed to canonicalize path {pseudo_exec_file_path}:"
rf" No such file or directory \(os error 2\)",
):
test_microvm.spawn()
Expand All @@ -102,11 +102,11 @@ def test_exec_file_not_exist(uvm_plain, tmp_path):

with pytest.raises(
Exception,
match=rf"Jailer error: {pseudo_exec_dir_path} is not a file",
match=rf"{pseudo_exec_dir_path} is not a file",
):
test_microvm.spawn()

# Error case 3: Filename without "firecracker"
# Error case 3: Not an executable
pseudo_exec_file_path = tmp_path / "foobarbaz"
pseudo_exec_file_path.touch()
fc_dir = Path("/srv/jailer") / pseudo_exec_file_path.name / test_microvm.id
Expand All @@ -115,8 +115,7 @@ def test_exec_file_not_exist(uvm_plain, tmp_path):

with pytest.raises(
Exception,
match=r"Jailer error: Invalid filename. The filename of `--exec-file` option"
r' must contain "firecracker": foobarbaz',
match=rf"{pseudo_exec_file_path} is not executable",
):
test_microvm.spawn()

Expand Down