Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/firecracker
Path: blob/main/tests/integration_tests/security/test_seccomp.py
1958 views
1
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
# SPDX-License-Identifier: Apache-2.0
3
"""Tests that the seccomp filters don't let denied syscalls through."""
4
5
import os
6
import tempfile
7
import platform
8
import time
9
import pytest
10
11
from host_tools.cargo_build import run_seccompiler_bin
12
import framework.utils as utils
13
14
15
def _get_basic_syscall_list():
16
"""Return the JSON list of syscalls that the demo jailer needs."""
17
if platform.machine() == "x86_64":
18
sys_list = [
19
"rt_sigprocmask",
20
"rt_sigaction",
21
"execve",
22
"mmap",
23
"mprotect",
24
"arch_prctl",
25
"set_tid_address",
26
"readlink",
27
"open",
28
"read",
29
"close",
30
"brk",
31
"sched_getaffinity",
32
"sigaltstack",
33
"munmap",
34
"exit_group",
35
"poll"
36
]
37
else:
38
# platform.machine() == "aarch64"
39
sys_list = [
40
"rt_sigprocmask",
41
"rt_sigaction",
42
"execve",
43
"mmap",
44
"mprotect",
45
"set_tid_address",
46
"read",
47
"close",
48
"brk",
49
"sched_getaffinity",
50
"sigaltstack",
51
"munmap",
52
"exit_group",
53
"ppoll"
54
]
55
56
json = ""
57
for syscall in sys_list[0:-1]:
58
json += """
59
{{
60
"syscall": \"{}\"
61
}},
62
""".format(syscall)
63
64
json += """
65
{{
66
"syscall": \"{}\"
67
}}
68
""".format(sys_list[-1])
69
70
return json
71
72
73
def _run_seccompiler_bin(json_data, basic=False):
74
json_temp = tempfile.NamedTemporaryFile(delete=False)
75
json_temp.write(json_data.encode('utf-8'))
76
json_temp.flush()
77
78
bpf_temp = tempfile.NamedTemporaryFile(delete=False)
79
80
run_seccompiler_bin(bpf_path=bpf_temp.name,
81
json_path=json_temp.name, basic=basic)
82
83
os.unlink(json_temp.name)
84
return bpf_temp.name
85
86
87
def test_seccomp_ls(bin_seccomp_paths):
88
"""Assert that the seccomp filter denies an unallowed syscall."""
89
# pylint: disable=redefined-outer-name
90
# pylint: disable=subprocess-run-check
91
# The fixture pattern causes a pylint false positive for that rule.
92
93
# Path to the `ls` binary, which attempts to execute the forbidden
94
# `SYS_access`.
95
ls_command_path = '/bin/ls'
96
demo_jailer = bin_seccomp_paths['demo_jailer']
97
assert os.path.exists(demo_jailer)
98
99
json_filter = """{{
100
"main": {{
101
"default_action": "trap",
102
"filter_action": "allow",
103
"filter": [
104
{}
105
]
106
}}
107
}}""".format(_get_basic_syscall_list())
108
109
# Run seccompiler-bin.
110
bpf_path = _run_seccompiler_bin(json_filter)
111
112
# Run the mini jailer.
113
outcome = utils.run_cmd([demo_jailer, ls_command_path, bpf_path],
114
no_shell=True,
115
ignore_return_code=True)
116
117
os.unlink(bpf_path)
118
119
# The seccomp filters should send SIGSYS (31) to the binary. `ls` doesn't
120
# handle it, so it will exit with error.
121
assert outcome.returncode != 0
122
123
124
def test_advanced_seccomp(bin_seccomp_paths):
125
"""
126
Test seccompiler-bin with `demo_jailer`.
127
128
Test that the demo jailer (with advanced seccomp) allows the harmless demo
129
binary, denies the malicious demo binary and that an empty allowlist
130
denies everything.
131
"""
132
# pylint: disable=redefined-outer-name
133
# pylint: disable=subprocess-run-check
134
# The fixture pattern causes a pylint false positive for that rule.
135
136
demo_jailer = bin_seccomp_paths['demo_jailer']
137
demo_harmless = bin_seccomp_paths['demo_harmless']
138
demo_malicious = bin_seccomp_paths['demo_malicious']
139
140
assert os.path.exists(demo_jailer)
141
assert os.path.exists(demo_harmless)
142
assert os.path.exists(demo_malicious)
143
144
json_filter = """{{
145
"main": {{
146
"default_action": "trap",
147
"filter_action": "allow",
148
"filter": [
149
{},
150
{{
151
"syscall": "write",
152
"args": [
153
{{
154
"index": 0,
155
"type": "dword",
156
"op": "eq",
157
"val": 1,
158
"comment": "stdout fd"
159
}},
160
{{
161
"index": 2,
162
"type": "qword",
163
"op": "eq",
164
"val": 14,
165
"comment": "nr of bytes"
166
}}
167
]
168
}}
169
]
170
}}
171
}}""".format(_get_basic_syscall_list())
172
173
# Run seccompiler-bin.
174
bpf_path = _run_seccompiler_bin(json_filter)
175
176
# Run the mini jailer for harmless binary.
177
outcome = utils.run_cmd([demo_jailer, demo_harmless, bpf_path],
178
no_shell=True,
179
ignore_return_code=True)
180
181
# The demo harmless binary should have terminated gracefully.
182
assert outcome.returncode == 0
183
184
# Run the mini jailer for malicious binary.
185
outcome = utils.run_cmd([demo_jailer, demo_malicious, bpf_path],
186
no_shell=True,
187
ignore_return_code=True)
188
189
# The demo malicious binary should have received `SIGSYS`.
190
assert outcome.returncode == -31
191
192
os.unlink(bpf_path)
193
194
# Run seccompiler-bin with `--basic` flag.
195
bpf_path = _run_seccompiler_bin(json_filter, basic=True)
196
197
# Run the mini jailer for malicious binary.
198
outcome = utils.run_cmd([demo_jailer, demo_malicious, bpf_path],
199
no_shell=True,
200
ignore_return_code=True)
201
202
# The malicious binary also terminates gracefully, since the --basic option
203
# disables all argument checks.
204
assert outcome.returncode == 0
205
206
os.unlink(bpf_path)
207
208
# Run the mini jailer with an empty allowlist. It should trap on any
209
# syscall.
210
json_filter = """{
211
"main": {
212
"default_action": "trap",
213
"filter_action": "allow",
214
"filter": []
215
}
216
}"""
217
218
# Run seccompiler-bin.
219
bpf_path = _run_seccompiler_bin(json_filter)
220
221
outcome = utils.run_cmd([demo_jailer, demo_harmless, bpf_path],
222
no_shell=True,
223
ignore_return_code=True)
224
225
# The demo binary should have received `SIGSYS`.
226
assert outcome.returncode == -31
227
228
os.unlink(bpf_path)
229
230
231
def test_no_seccomp(test_microvm_with_api):
232
"""Test Firecracker --no-seccomp."""
233
test_microvm = test_microvm_with_api
234
test_microvm.jailer.extra_args.update({"no-seccomp": None})
235
test_microvm.spawn()
236
237
test_microvm.basic_config()
238
239
test_microvm.start()
240
241
utils.assert_seccomp_level(test_microvm.jailer_clone_pid, "0")
242
243
244
# The possible Firecracker --seccomp-level values.
245
# "default" stands for no custom parameter.
246
SECCOMP_LEVELS = ["default", "0", "1", "2"]
247
248
# Map FC seccomp-level to kernel seccomp-level.
249
# Note that level 1 also maps to kernel level 2, which stands for
250
# any custom BPF filter.
251
# The default is 2.
252
KERNEL_LEVEL = {"default": "2", "0": "0", "1": "2", "2": "2"}
253
254
255
@pytest.mark.parametrize(
256
"level",
257
SECCOMP_LEVELS
258
)
259
def test_seccomp_level(test_microvm_with_api, level):
260
"""Test Firecracker --seccomp-level value."""
261
test_microvm = test_microvm_with_api
262
test_microvm.jailer.daemonize = False
263
264
if level != "default":
265
test_microvm.jailer.extra_args.update({"seccomp-level": level})
266
267
test_microvm.spawn(create_logger=False)
268
269
test_microvm.basic_config()
270
271
test_microvm.start()
272
273
utils.assert_seccomp_level(
274
test_microvm.jailer_clone_pid, KERNEL_LEVEL[level])
275
276
test_microvm.kill()
277
278
# For seccomp-level, check that we output the deprecation warnings.
279
if level != "default":
280
time.sleep(0.5)
281
with open(test_microvm.screen_log, 'r') as file:
282
log_data = file.read()
283
assert "You are using a deprecated parameter: --seccomp-level " \
284
f"{level}, that will be removed in a future version." \
285
in log_data
286
287