Skip to content

Commit 2fea612

Browse files
test: firecracker-microvm#1099 jailer mount propagation Signed-off-by: Anthony Corletti <[email protected]>
1 parent 3ca2fab commit 2fea612

File tree

2 files changed

+118
-21
lines changed

2 files changed

+118
-21
lines changed

tests/framework/microvm.py

+42-21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import select
1919
import shutil
2020
import signal
21+
import subprocess
2122
import time
2223
import uuid
2324
from collections import namedtuple
@@ -299,9 +300,9 @@ def kill(self):
299300
backend.kill()
300301
self.disks_vhost_user.clear()
301302

302-
assert (
303-
"Shutting down VM after intercepting signal" not in self.log_data
304-
), self.log_data
303+
assert "Shutting down VM after intercepting signal" not in self.log_data, (
304+
self.log_data
305+
)
305306

306307
try:
307308
if self.firecracker_pid:
@@ -330,9 +331,9 @@ def kill(self):
330331
f"ps aux | grep {self.jailer.jailer_id}"
331332
)
332333
# make sure firecracker was killed
333-
assert (
334-
stderr == "" and "firecracker" not in stdout
335-
), f"Firecracker reported its pid {self.firecracker_pid}, which was killed, but there still exist processes using the supposedly dead Firecracker's jailer_id: {stdout}"
334+
assert stderr == "" and "firecracker" not in stdout, (
335+
f"Firecracker reported its pid {self.firecracker_pid}, which was killed, but there still exist processes using the supposedly dead Firecracker's jailer_id: {stdout}"
336+
)
336337

337338
# Mark the microVM as not spawned, so we avoid trying to kill twice.
338339
self._spawned = False
@@ -391,9 +392,9 @@ def _validate_api_response_times(self):
391392
if current_call.url != "/snapshot/create":
392393
exec_time = float(match.group("execution_time")) / 1000.0
393394

394-
assert (
395-
exec_time <= MAX_API_CALL_DURATION_MS
396-
), f"{current_call.method} {current_call.url} API call exceeded maximum duration: {exec_time} ms. Body: {current_call.body}"
395+
assert exec_time <= MAX_API_CALL_DURATION_MS, (
396+
f"{current_call.method} {current_call.url} API call exceeded maximum duration: {exec_time} ms. Body: {current_call.body}"
397+
)
397398

398399
current_call = None
399400

@@ -560,18 +561,18 @@ def pin_threads(self, first_cpu):
560561
Return next "free" cpu core.
561562
"""
562563
for vcpu, pcpu in enumerate(range(first_cpu, first_cpu + self.vcpus_count)):
563-
assert self.pin_vcpu(
564-
vcpu, pcpu
565-
), f"Failed to pin fc_vcpu {vcpu} thread to core {pcpu}."
564+
assert self.pin_vcpu(vcpu, pcpu), (
565+
f"Failed to pin fc_vcpu {vcpu} thread to core {pcpu}."
566+
)
566567
# The cores first_cpu,...,first_cpu + self.vcpus_count - 1 are assigned to the individual vCPU threads,
567568
# So the remaining two threads (VMM and API) get first_cpu + self.vcpus_count
568569
# and first_cpu + self.vcpus_count + 1
569-
assert self.pin_vmm(
570-
first_cpu + self.vcpus_count
571-
), "Failed to pin firecracker thread."
572-
assert self.pin_api(
573-
first_cpu + self.vcpus_count + 1
574-
), "Failed to pin fc_api thread."
570+
assert self.pin_vmm(first_cpu + self.vcpus_count), (
571+
"Failed to pin firecracker thread."
572+
)
573+
assert self.pin_api(first_cpu + self.vcpus_count + 1), (
574+
"Failed to pin fc_api thread."
575+
)
575576

576577
return first_cpu + self.vcpus_count + 2
577578

@@ -683,9 +684,9 @@ def _wait_create(self):
683684
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
684685
def check_log_message(self, message):
685686
"""Wait until `message` appears in logging output."""
686-
assert (
687-
message in self.log_data
688-
), f'Message ("{message}") not found in log data ("{self.log_data}").'
687+
assert message in self.log_data, (
688+
f'Message ("{message}") not found in log data ("{self.log_data}").'
689+
)
689690

690691
@retry(wait=wait_fixed(0.2), stop=stop_after_attempt(5), reraise=True)
691692
def get_exit_code(self):
@@ -1115,13 +1116,33 @@ def build_from_snapshot(self, snapshot: Snapshot):
11151116
vm.restore_from_snapshot(snapshot, resume=True)
11161117
return vm
11171118

1119+
def unmount(self, path: str):
1120+
try:
1121+
subprocess.run(["umount", path], check=True)
1122+
except subprocess.CalledProcessError:
1123+
print(f"Failed to unmount {path}")
1124+
1125+
def get_mounts_at_path(self, path: str) -> list:
1126+
try:
1127+
with open("/proc/mounts", "r") as f:
1128+
return [
1129+
line.split()[1]
1130+
for line in f
1131+
if line.split()[1].startswith(os.path.abspath(path))
1132+
]
1133+
except FileNotFoundError:
1134+
return False # /proc/mounts may not exist on some systems
1135+
11181136
def kill(self):
11191137
"""Clean up all built VMs"""
11201138
for vm in self.vms:
11211139
vm.kill()
11221140
vm.jailer.cleanup()
11231141
chroot_base_with_id = vm.jailer.chroot_base_with_id()
11241142
if len(vm.jailer.jailer_id) > 0 and chroot_base_with_id.exists():
1143+
mounts = self.get_mounts_at_path(chroot_base_with_id)
1144+
if mounts:
1145+
[self.unmount(mounted_path) for mounted_path in mounts]
11251146
shutil.rmtree(chroot_base_with_id)
11261147
vm.netns.cleanup()
11271148

tests/integration_tests/security/test_jail.py

+76
Original file line numberDiff line numberDiff line change
@@ -664,3 +664,79 @@ def test_cgroupsv2_written_only_once(uvm_plain, cgroups_info):
664664
assert len(write_lines) == 1
665665
assert len(mkdir_lines) != len(cgroups), "mkdir equal to number of cgroups"
666666
assert len(mkdir_lines) == 1
667+
668+
669+
def test_mount_proagation_to_root(uvm_plain, tmp_path, guest_kernel, rootfs_rw):
670+
"""
671+
Test that the jailer mounts are propagated to the root mount namespace.
672+
673+
This is a test for
674+
https://github.com/firecracker-microvm/firecracker/pull/#1093
675+
"""
676+
677+
test_microvm = uvm_plain
678+
679+
# make a directory to hold the original content
680+
original_content_dir = tmp_path / "original"
681+
original_content_dir.mkdir(parents=True)
682+
683+
# make a directory to hold the jailed content
684+
jailed_content_dir = tmp_path / "firecracker" / "testbindmount" / "root"
685+
jailed_content_dir.mkdir(parents=True)
686+
687+
test_microvm.jailer.jailer_id = "testbindmount"
688+
test_microvm.jailer.chroot_base = tmp_path
689+
test_microvm.jailer.daemonize = True
690+
test_microvm.jailer.gid = 0
691+
test_microvm.jailer.uid = 0
692+
test_microvm.extra_args = {"seccomp-level": 0}
693+
694+
# assert that the directory was created
695+
assert jailed_content_dir.exists()
696+
697+
# Create the guest kernel and rootfs in the jailed content directory
698+
# and mount them in the jailed content directory
699+
os.system(f"cp {guest_kernel} {original_content_dir}")
700+
os.system(f"cp {rootfs_rw} {original_content_dir}")
701+
guest_kernel_mount_path = jailed_content_dir / os.path.basename(guest_kernel)
702+
rootfs_mount_path = jailed_content_dir / os.path.basename(rootfs_rw)
703+
guest_kernel_mount_path.touch()
704+
rootfs_mount_path.touch()
705+
706+
# assert that the files were created
707+
assert guest_kernel_mount_path.exists()
708+
assert rootfs_mount_path.exists()
709+
710+
# mount the rootfs
711+
subprocess.run(
712+
[
713+
"mount",
714+
"--bind",
715+
original_content_dir / os.path.basename(guest_kernel),
716+
guest_kernel_mount_path,
717+
],
718+
check=True,
719+
)
720+
subprocess.run(
721+
[
722+
"mount",
723+
"--bind",
724+
original_content_dir / os.path.basename(rootfs_rw),
725+
rootfs_mount_path,
726+
],
727+
check=True,
728+
)
729+
730+
# assert that the mounts are present
731+
assert guest_kernel_mount_path.exists()
732+
assert rootfs_mount_path.exists()
733+
734+
print("test_microvm", test_microvm.__dict__)
735+
print("test_microvm.jailer", test_microvm.jailer.__dict__)
736+
737+
# run
738+
test_microvm.spawn()
739+
740+
# assert that the mounts are present
741+
assert guest_kernel_mount_path.exists()
742+
assert rootfs_mount_path.exists()

0 commit comments

Comments
 (0)