|
1 | 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. |
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 | """Tests that verify the jailer's behavior.""" |
| 4 | +import http.client as http_client |
4 | 5 | import os |
| 6 | +import resource |
5 | 7 | import stat |
6 | 8 | import subprocess |
| 9 | +import time |
7 | 10 |
|
| 11 | +import psutil |
| 12 | +import requests |
| 13 | +import urllib3 |
| 14 | + |
| 15 | +from framework.builder import SnapshotBuilder |
8 | 16 | from framework.defs import FC_BINARY_NAME |
9 | 17 | from framework.jailer import JailerContext |
10 | 18 | import host_tools.cargo_build as build_tools |
|
19 | 27 | SOCK_STATS = stat.S_IFSOCK | REG_PERMS |
20 | 28 | # These are the stats of the devices created by tha jailer. |
21 | 29 | CHAR_STATS = stat.S_IFCHR | stat.S_IRUSR | stat.S_IWUSR |
| 30 | +# Limit on file size in bytes. |
| 31 | +FSIZE = 2097151 |
| 32 | +# Limit on number of file descriptors. |
| 33 | +NOFILE = 1024 |
| 34 | +# Resource limits to be set by the jailer. |
| 35 | +RESOURCE_LIMITS = [ |
| 36 | + 'no-file={}'.format(NOFILE), |
| 37 | + 'fsize={}'.format(FSIZE), |
| 38 | +] |
22 | 39 |
|
23 | 40 |
|
24 | 41 | def check_stats(filepath, stats, uid, gid): |
@@ -130,6 +147,19 @@ def get_cpus(node): |
130 | 147 | return open(node_cpus_path, 'r').readline().strip() |
131 | 148 |
|
132 | 149 |
|
| 150 | +def check_limits(pid, no_file, fsize): |
| 151 | + """Verify resource limits against expected values.""" |
| 152 | + # Fetch firecracker process limits for number of open fds |
| 153 | + (soft, hard) = resource.prlimit(pid, resource.RLIMIT_NOFILE) |
| 154 | + assert soft == no_file |
| 155 | + assert hard == no_file |
| 156 | + |
| 157 | + # Fetch firecracker process limits for maximum file size |
| 158 | + (soft, hard) = resource.prlimit(pid, resource.RLIMIT_FSIZE) |
| 159 | + assert soft == fsize |
| 160 | + assert hard == fsize |
| 161 | + |
| 162 | + |
133 | 163 | def test_cgroups(test_microvm_with_initrd): |
134 | 164 | """Test the cgroups are correctly set by the jailer.""" |
135 | 165 | test_microvm = test_microvm_with_initrd |
@@ -198,6 +228,115 @@ def test_args_cgroups(test_microvm_with_initrd): |
198 | 228 | ) |
199 | 229 |
|
200 | 230 |
|
| 231 | +def test_args_default_resource_limits(test_microvm_with_initrd): |
| 232 | + """Test the resource limits are correctly set by the jailer.""" |
| 233 | + test_microvm = test_microvm_with_initrd |
| 234 | + |
| 235 | + test_microvm.spawn() |
| 236 | + |
| 237 | + # Get firecracker's PID |
| 238 | + pid = int(test_microvm.jailer_clone_pid) |
| 239 | + assert pid != 0 |
| 240 | + |
| 241 | + # Fetch firecracker process limits for number of open fds |
| 242 | + (soft, hard) = resource.prlimit(pid, resource.RLIMIT_NOFILE) |
| 243 | + # Check that the default limit was set. |
| 244 | + assert soft == 2048 |
| 245 | + assert hard == 2048 |
| 246 | + |
| 247 | + # Fetch firecracker process limits for number of open fds |
| 248 | + (soft, hard) = resource.prlimit(pid, resource.RLIMIT_FSIZE) |
| 249 | + # Check that no limit was set |
| 250 | + assert soft == -1 |
| 251 | + assert hard == -1 |
| 252 | + |
| 253 | + |
| 254 | +def test_args_resource_limits(test_microvm_with_initrd): |
| 255 | + """Test the resource limits are correctly set by the jailer.""" |
| 256 | + test_microvm = test_microvm_with_initrd |
| 257 | + test_microvm.jailer.resource_limits = RESOURCE_LIMITS |
| 258 | + |
| 259 | + test_microvm.spawn() |
| 260 | + |
| 261 | + # Get firecracker's PID |
| 262 | + pid = int(test_microvm.jailer_clone_pid) |
| 263 | + assert pid != 0 |
| 264 | + |
| 265 | + # Check limit values were correctly set. |
| 266 | + check_limits(pid, NOFILE, FSIZE) |
| 267 | + |
| 268 | + |
| 269 | +def test_negative_file_size_limit(test_microvm_with_ssh): |
| 270 | + """Test creating snapshot file fails when size exceeds `fsize` limit.""" |
| 271 | + test_microvm = test_microvm_with_ssh |
| 272 | + test_microvm.jailer.resource_limits = ['fsize=1024'] |
| 273 | + |
| 274 | + test_microvm.spawn() |
| 275 | + test_microvm.basic_config() |
| 276 | + test_microvm.start() |
| 277 | + |
| 278 | + snapshot_builder = SnapshotBuilder(test_microvm) |
| 279 | + # Create directory and files for saving snapshot state and memory. |
| 280 | + _snapshot_dir = snapshot_builder.create_snapshot_dir() |
| 281 | + |
| 282 | + # Pause microVM for snapshot. |
| 283 | + response = test_microvm.vm.patch(state='Paused') |
| 284 | + assert test_microvm.api_session.is_status_no_content(response.status_code) |
| 285 | + |
| 286 | + # Attempt to create a snapshot. |
| 287 | + try: |
| 288 | + test_microvm.snapshot.create( |
| 289 | + mem_file_path="/snapshot/vm.mem", |
| 290 | + snapshot_path="/snapshot/vm.vmstate", |
| 291 | + ) |
| 292 | + except ( |
| 293 | + http_client.RemoteDisconnected, |
| 294 | + urllib3.exceptions.ProtocolError, |
| 295 | + requests.exceptions.ConnectionError |
| 296 | + ) as _error: |
| 297 | + test_microvm.expect_kill_by_signal = True |
| 298 | + # Check the microVM received signal `SIGXFSZ` (25), |
| 299 | + # which corresponds to exceeding file size limit. |
| 300 | + msg = 'Shutting down VM after intercepting signal 25, code 0' |
| 301 | + test_microvm.check_log_message(msg) |
| 302 | + time.sleep(1) |
| 303 | + # Check that the process was terminated. |
| 304 | + assert not psutil.pid_exists(test_microvm.jailer_clone_pid) |
| 305 | + else: |
| 306 | + assert False, "Negative test failed" |
| 307 | + |
| 308 | + |
| 309 | +def test_negative_no_file_limit(test_microvm_with_ssh): |
| 310 | + """Test microVM is killed when exceeding `no-file` limit.""" |
| 311 | + test_microvm = test_microvm_with_ssh |
| 312 | + test_microvm.jailer.resource_limits = ['no-file=3'] |
| 313 | + |
| 314 | + # pylint: disable=W0703 |
| 315 | + try: |
| 316 | + test_microvm.spawn() |
| 317 | + except Exception as error: |
| 318 | + assert "No file descriptors available (os error 24)" in str(error) |
| 319 | + assert test_microvm.jailer_clone_pid is None |
| 320 | + else: |
| 321 | + assert False, "Negative test failed" |
| 322 | + |
| 323 | + |
| 324 | +def test_new_pid_ns_resource_limits(test_microvm_with_ssh): |
| 325 | + """Test that Firecracker process inherits jailer resource limits.""" |
| 326 | + test_microvm = test_microvm_with_ssh |
| 327 | + |
| 328 | + test_microvm.jailer.daemonize = False |
| 329 | + test_microvm.jailer.new_pid_ns = True |
| 330 | + test_microvm.jailer.resource_limits = RESOURCE_LIMITS |
| 331 | + |
| 332 | + test_microvm.spawn() |
| 333 | + |
| 334 | + # Get Firecracker's PID. |
| 335 | + fc_pid = test_microvm.pid_in_new_ns |
| 336 | + # Check limit values were correctly set. |
| 337 | + check_limits(fc_pid, NOFILE, FSIZE) |
| 338 | + |
| 339 | + |
201 | 340 | def test_new_pid_namespace(test_microvm_with_ssh): |
202 | 341 | """Test that Firecracker is spawned in a new PID namespace if requested.""" |
203 | 342 | test_microvm = test_microvm_with_ssh |
|
0 commit comments