Commit | Line | Data |
---|---|---|
ba84b0bf MS |
1 | // SPDX-License-Identifier: BSD-3-Clause |
2 | /* | |
a17c60e5 MS |
3 | * Simple Landlock sandbox manager able to execute a process restricted by |
4 | * user-defined file system and network access control policies. | |
ba84b0bf MS |
5 | * |
6 | * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> | |
7 | * Copyright © 2020 ANSSI | |
8 | */ | |
9 | ||
10 | #define _GNU_SOURCE | |
5e990dce KM |
11 | #define __SANE_USERSPACE_TYPES__ |
12 | #include <arpa/inet.h> | |
ba84b0bf MS |
13 | #include <errno.h> |
14 | #include <fcntl.h> | |
15 | #include <linux/landlock.h> | |
16 | #include <linux/prctl.h> | |
17 | #include <stddef.h> | |
18 | #include <stdio.h> | |
19 | #include <stdlib.h> | |
20 | #include <string.h> | |
21 | #include <sys/prctl.h> | |
22 | #include <sys/stat.h> | |
23 | #include <sys/syscall.h> | |
24 | #include <unistd.h> | |
25 | ||
26 | #ifndef landlock_create_ruleset | |
81709f3d MS |
27 | static inline int |
28 | landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, | |
29 | const size_t size, const __u32 flags) | |
ba84b0bf MS |
30 | { |
31 | return syscall(__NR_landlock_create_ruleset, attr, size, flags); | |
32 | } | |
33 | #endif | |
34 | ||
35 | #ifndef landlock_add_rule | |
36 | static inline int landlock_add_rule(const int ruleset_fd, | |
81709f3d MS |
37 | const enum landlock_rule_type rule_type, |
38 | const void *const rule_attr, | |
39 | const __u32 flags) | |
ba84b0bf | 40 | { |
81709f3d MS |
41 | return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, |
42 | flags); | |
ba84b0bf MS |
43 | } |
44 | #endif | |
45 | ||
46 | #ifndef landlock_restrict_self | |
47 | static inline int landlock_restrict_self(const int ruleset_fd, | |
81709f3d | 48 | const __u32 flags) |
ba84b0bf MS |
49 | { |
50 | return syscall(__NR_landlock_restrict_self, ruleset_fd, flags); | |
51 | } | |
52 | #endif | |
53 | ||
54 | #define ENV_FS_RO_NAME "LL_FS_RO" | |
55 | #define ENV_FS_RW_NAME "LL_FS_RW" | |
5e990dce KM |
56 | #define ENV_TCP_BIND_NAME "LL_TCP_BIND" |
57 | #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT" | |
58 | #define ENV_DELIMITER ":" | |
ba84b0bf MS |
59 | |
60 | static int parse_path(char *env_path, const char ***const path_list) | |
61 | { | |
62 | int i, num_paths = 0; | |
63 | ||
64 | if (env_path) { | |
65 | num_paths++; | |
66 | for (i = 0; env_path[i]; i++) { | |
5e990dce | 67 | if (env_path[i] == ENV_DELIMITER[0]) |
ba84b0bf MS |
68 | num_paths++; |
69 | } | |
70 | } | |
71 | *path_list = malloc(num_paths * sizeof(**path_list)); | |
72 | for (i = 0; i < num_paths; i++) | |
5e990dce | 73 | (*path_list)[i] = strsep(&env_path, ENV_DELIMITER); |
ba84b0bf MS |
74 | |
75 | return num_paths; | |
76 | } | |
77 | ||
9805a722 MS |
78 | /* clang-format off */ |
79 | ||
ba84b0bf MS |
80 | #define ACCESS_FILE ( \ |
81 | LANDLOCK_ACCESS_FS_EXECUTE | \ | |
82 | LANDLOCK_ACCESS_FS_WRITE_FILE | \ | |
faeb9197 | 83 | LANDLOCK_ACCESS_FS_READ_FILE | \ |
cd13738d GN |
84 | LANDLOCK_ACCESS_FS_TRUNCATE | \ |
85 | LANDLOCK_ACCESS_FS_IOCTL_DEV) | |
ba84b0bf | 86 | |
9805a722 MS |
87 | /* clang-format on */ |
88 | ||
5e990dce KM |
89 | static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd, |
90 | const __u64 allowed_access) | |
ba84b0bf MS |
91 | { |
92 | int num_paths, i, ret = 1; | |
93 | char *env_path_name; | |
94 | const char **path_list = NULL; | |
95 | struct landlock_path_beneath_attr path_beneath = { | |
96 | .parent_fd = -1, | |
97 | }; | |
98 | ||
99 | env_path_name = getenv(env_var); | |
100 | if (!env_path_name) { | |
101 | /* Prevents users to forget a setting. */ | |
102 | fprintf(stderr, "Missing environment variable %s\n", env_var); | |
103 | return 1; | |
104 | } | |
105 | env_path_name = strdup(env_path_name); | |
106 | unsetenv(env_var); | |
107 | num_paths = parse_path(env_path_name, &path_list); | |
108 | if (num_paths == 1 && path_list[0][0] == '\0') { | |
109 | /* | |
110 | * Allows to not use all possible restrictions (e.g. use | |
111 | * LL_FS_RO without LL_FS_RW). | |
112 | */ | |
113 | ret = 0; | |
114 | goto out_free_name; | |
115 | } | |
116 | ||
117 | for (i = 0; i < num_paths; i++) { | |
118 | struct stat statbuf; | |
119 | ||
81709f3d | 120 | path_beneath.parent_fd = open(path_list[i], O_PATH | O_CLOEXEC); |
ba84b0bf MS |
121 | if (path_beneath.parent_fd < 0) { |
122 | fprintf(stderr, "Failed to open \"%s\": %s\n", | |
81709f3d | 123 | path_list[i], strerror(errno)); |
a17c60e5 | 124 | continue; |
ba84b0bf MS |
125 | } |
126 | if (fstat(path_beneath.parent_fd, &statbuf)) { | |
a17c60e5 MS |
127 | fprintf(stderr, "Failed to stat \"%s\": %s\n", |
128 | path_list[i], strerror(errno)); | |
ba84b0bf MS |
129 | close(path_beneath.parent_fd); |
130 | goto out_free_name; | |
131 | } | |
132 | path_beneath.allowed_access = allowed_access; | |
133 | if (!S_ISDIR(statbuf.st_mode)) | |
134 | path_beneath.allowed_access &= ACCESS_FILE; | |
135 | if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_PATH_BENEATH, | |
81709f3d MS |
136 | &path_beneath, 0)) { |
137 | fprintf(stderr, | |
138 | "Failed to update the ruleset with \"%s\": %s\n", | |
139 | path_list[i], strerror(errno)); | |
ba84b0bf MS |
140 | close(path_beneath.parent_fd); |
141 | goto out_free_name; | |
142 | } | |
143 | close(path_beneath.parent_fd); | |
144 | } | |
145 | ret = 0; | |
146 | ||
147 | out_free_name: | |
66b513b7 | 148 | free(path_list); |
ba84b0bf MS |
149 | free(env_path_name); |
150 | return ret; | |
151 | } | |
152 | ||
5e990dce KM |
153 | static int populate_ruleset_net(const char *const env_var, const int ruleset_fd, |
154 | const __u64 allowed_access) | |
155 | { | |
156 | int ret = 1; | |
42212936 | 157 | char *env_port_name, *env_port_name_next, *strport; |
5e990dce KM |
158 | struct landlock_net_port_attr net_port = { |
159 | .allowed_access = allowed_access, | |
160 | .port = 0, | |
161 | }; | |
162 | ||
163 | env_port_name = getenv(env_var); | |
164 | if (!env_port_name) | |
165 | return 0; | |
166 | env_port_name = strdup(env_port_name); | |
167 | unsetenv(env_var); | |
168 | ||
42212936 IM |
169 | env_port_name_next = env_port_name; |
170 | while ((strport = strsep(&env_port_name_next, ENV_DELIMITER))) { | |
5e990dce KM |
171 | net_port.port = atoi(strport); |
172 | if (landlock_add_rule(ruleset_fd, LANDLOCK_RULE_NET_PORT, | |
173 | &net_port, 0)) { | |
174 | fprintf(stderr, | |
175 | "Failed to update the ruleset with port \"%llu\": %s\n", | |
176 | net_port.port, strerror(errno)); | |
177 | goto out_free_name; | |
178 | } | |
179 | } | |
180 | ret = 0; | |
181 | ||
182 | out_free_name: | |
183 | free(env_port_name); | |
184 | return ret; | |
185 | } | |
186 | ||
9805a722 MS |
187 | /* clang-format off */ |
188 | ||
ba84b0bf MS |
189 | #define ACCESS_FS_ROUGHLY_READ ( \ |
190 | LANDLOCK_ACCESS_FS_EXECUTE | \ | |
191 | LANDLOCK_ACCESS_FS_READ_FILE | \ | |
192 | LANDLOCK_ACCESS_FS_READ_DIR) | |
193 | ||
194 | #define ACCESS_FS_ROUGHLY_WRITE ( \ | |
195 | LANDLOCK_ACCESS_FS_WRITE_FILE | \ | |
196 | LANDLOCK_ACCESS_FS_REMOVE_DIR | \ | |
197 | LANDLOCK_ACCESS_FS_REMOVE_FILE | \ | |
198 | LANDLOCK_ACCESS_FS_MAKE_CHAR | \ | |
199 | LANDLOCK_ACCESS_FS_MAKE_DIR | \ | |
200 | LANDLOCK_ACCESS_FS_MAKE_REG | \ | |
201 | LANDLOCK_ACCESS_FS_MAKE_SOCK | \ | |
202 | LANDLOCK_ACCESS_FS_MAKE_FIFO | \ | |
203 | LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ | |
76b902f8 | 204 | LANDLOCK_ACCESS_FS_MAKE_SYM | \ |
faeb9197 | 205 | LANDLOCK_ACCESS_FS_REFER | \ |
cd13738d GN |
206 | LANDLOCK_ACCESS_FS_TRUNCATE | \ |
207 | LANDLOCK_ACCESS_FS_IOCTL_DEV) | |
76b902f8 | 208 | |
9805a722 MS |
209 | /* clang-format on */ |
210 | ||
cd13738d | 211 | #define LANDLOCK_ABI_LAST 5 |
903cfe8a | 212 | |
ba84b0bf MS |
213 | int main(const int argc, char *const argv[], char *const *const envp) |
214 | { | |
215 | const char *cmd_path; | |
216 | char *const *cmd_argv; | |
76b902f8 | 217 | int ruleset_fd, abi; |
5e990dce | 218 | char *env_port_name; |
76b902f8 MS |
219 | __u64 access_fs_ro = ACCESS_FS_ROUGHLY_READ, |
220 | access_fs_rw = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE; | |
5e990dce | 221 | |
ba84b0bf | 222 | struct landlock_ruleset_attr ruleset_attr = { |
76b902f8 | 223 | .handled_access_fs = access_fs_rw, |
5e990dce KM |
224 | .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | |
225 | LANDLOCK_ACCESS_NET_CONNECT_TCP, | |
ba84b0bf MS |
226 | }; |
227 | ||
228 | if (argc < 2) { | |
81709f3d | 229 | fprintf(stderr, |
5e990dce KM |
230 | "usage: %s=\"...\" %s=\"...\" %s=\"...\" %s=\"...\"%s " |
231 | "<cmd> [args]...\n\n", | |
232 | ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME, | |
233 | ENV_TCP_CONNECT_NAME, argv[0]); | |
81709f3d | 234 | fprintf(stderr, |
a17c60e5 | 235 | "Execute a command in a restricted environment.\n\n"); |
5e990dce KM |
236 | fprintf(stderr, |
237 | "Environment variables containing paths and ports " | |
238 | "each separated by a colon:\n"); | |
81709f3d MS |
239 | fprintf(stderr, |
240 | "* %s: list of paths allowed to be used in a read-only way.\n", | |
241 | ENV_FS_RO_NAME); | |
242 | fprintf(stderr, | |
5e990dce | 243 | "* %s: list of paths allowed to be used in a read-write way.\n\n", |
81709f3d | 244 | ENV_FS_RW_NAME); |
5e990dce KM |
245 | fprintf(stderr, |
246 | "Environment variables containing ports are optional " | |
247 | "and could be skipped.\n"); | |
248 | fprintf(stderr, | |
249 | "* %s: list of ports allowed to bind (server).\n", | |
250 | ENV_TCP_BIND_NAME); | |
251 | fprintf(stderr, | |
252 | "* %s: list of ports allowed to connect (client).\n", | |
253 | ENV_TCP_CONNECT_NAME); | |
81709f3d MS |
254 | fprintf(stderr, |
255 | "\nexample:\n" | |
a17c60e5 | 256 | "%s=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" " |
81709f3d | 257 | "%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " |
5e990dce KM |
258 | "%s=\"9418\" " |
259 | "%s=\"80:443\" " | |
903cfe8a | 260 | "%s bash -i\n\n", |
5e990dce KM |
261 | ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME, |
262 | ENV_TCP_CONNECT_NAME, argv[0]); | |
903cfe8a MS |
263 | fprintf(stderr, |
264 | "This sandboxer can use Landlock features " | |
265 | "up to ABI version %d.\n", | |
266 | LANDLOCK_ABI_LAST); | |
ba84b0bf MS |
267 | return 1; |
268 | } | |
269 | ||
76b902f8 MS |
270 | abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); |
271 | if (abi < 0) { | |
ba84b0bf MS |
272 | const int err = errno; |
273 | ||
76b902f8 | 274 | perror("Failed to check Landlock compatibility"); |
ba84b0bf MS |
275 | switch (err) { |
276 | case ENOSYS: | |
81709f3d MS |
277 | fprintf(stderr, |
278 | "Hint: Landlock is not supported by the current kernel. " | |
279 | "To support it, build the kernel with " | |
280 | "CONFIG_SECURITY_LANDLOCK=y and prepend " | |
281 | "\"landlock,\" to the content of CONFIG_LSM.\n"); | |
ba84b0bf MS |
282 | break; |
283 | case EOPNOTSUPP: | |
81709f3d MS |
284 | fprintf(stderr, |
285 | "Hint: Landlock is currently disabled. " | |
286 | "It can be enabled in the kernel configuration by " | |
287 | "prepending \"landlock,\" to the content of CONFIG_LSM, " | |
288 | "or at boot time by setting the same content to the " | |
289 | "\"lsm\" kernel parameter.\n"); | |
ba84b0bf MS |
290 | break; |
291 | } | |
292 | return 1; | |
293 | } | |
903cfe8a | 294 | |
76b902f8 | 295 | /* Best-effort security. */ |
903cfe8a MS |
296 | switch (abi) { |
297 | case 1: | |
f6e53fb2 GN |
298 | /* |
299 | * Removes LANDLOCK_ACCESS_FS_REFER for ABI < 2 | |
300 | * | |
301 | * Note: The "refer" operations (file renaming and linking | |
302 | * across different directories) are always forbidden when using | |
303 | * Landlock with ABI 1. | |
304 | * | |
305 | * If only ABI 1 is available, this sandboxer knowingly forbids | |
306 | * refer operations. | |
307 | * | |
308 | * If a program *needs* to do refer operations after enabling | |
309 | * Landlock, it can not use Landlock at ABI level 1. To be | |
310 | * compatible with different kernel versions, such programs | |
311 | * should then fall back to not restrict themselves at all if | |
312 | * the running kernel only supports ABI 1. | |
313 | */ | |
903cfe8a | 314 | ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; |
faeb9197 GN |
315 | __attribute__((fallthrough)); |
316 | case 2: | |
317 | /* Removes LANDLOCK_ACCESS_FS_TRUNCATE for ABI < 3 */ | |
318 | ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; | |
5e990dce KM |
319 | __attribute__((fallthrough)); |
320 | case 3: | |
321 | /* Removes network support for ABI < 4 */ | |
322 | ruleset_attr.handled_access_net &= | |
323 | ~(LANDLOCK_ACCESS_NET_BIND_TCP | | |
324 | LANDLOCK_ACCESS_NET_CONNECT_TCP); | |
cd13738d GN |
325 | __attribute__((fallthrough)); |
326 | case 4: | |
327 | /* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */ | |
328 | ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_IOCTL_DEV; | |
329 | ||
903cfe8a MS |
330 | fprintf(stderr, |
331 | "Hint: You should update the running kernel " | |
332 | "to leverage Landlock features " | |
333 | "provided by ABI version %d (instead of %d).\n", | |
334 | LANDLOCK_ABI_LAST, abi); | |
335 | __attribute__((fallthrough)); | |
336 | case LANDLOCK_ABI_LAST: | |
337 | break; | |
338 | default: | |
339 | fprintf(stderr, | |
340 | "Hint: You should update this sandboxer " | |
341 | "to leverage Landlock features " | |
342 | "provided by ABI version %d (instead of %d).\n", | |
343 | abi, LANDLOCK_ABI_LAST); | |
76b902f8 | 344 | } |
903cfe8a MS |
345 | access_fs_ro &= ruleset_attr.handled_access_fs; |
346 | access_fs_rw &= ruleset_attr.handled_access_fs; | |
76b902f8 | 347 | |
5e990dce KM |
348 | /* Removes bind access attribute if not supported by a user. */ |
349 | env_port_name = getenv(ENV_TCP_BIND_NAME); | |
350 | if (!env_port_name) { | |
351 | ruleset_attr.handled_access_net &= | |
352 | ~LANDLOCK_ACCESS_NET_BIND_TCP; | |
353 | } | |
354 | /* Removes connect access attribute if not supported by a user. */ | |
355 | env_port_name = getenv(ENV_TCP_CONNECT_NAME); | |
356 | if (!env_port_name) { | |
357 | ruleset_attr.handled_access_net &= | |
358 | ~LANDLOCK_ACCESS_NET_CONNECT_TCP; | |
359 | } | |
360 | ||
76b902f8 MS |
361 | ruleset_fd = |
362 | landlock_create_ruleset(&ruleset_attr, sizeof(ruleset_attr), 0); | |
363 | if (ruleset_fd < 0) { | |
364 | perror("Failed to create a ruleset"); | |
365 | return 1; | |
366 | } | |
5e990dce KM |
367 | |
368 | if (populate_ruleset_fs(ENV_FS_RO_NAME, ruleset_fd, access_fs_ro)) { | |
369 | goto err_close_ruleset; | |
370 | } | |
371 | if (populate_ruleset_fs(ENV_FS_RW_NAME, ruleset_fd, access_fs_rw)) { | |
ba84b0bf MS |
372 | goto err_close_ruleset; |
373 | } | |
5e990dce KM |
374 | |
375 | if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd, | |
376 | LANDLOCK_ACCESS_NET_BIND_TCP)) { | |
377 | goto err_close_ruleset; | |
378 | } | |
379 | if (populate_ruleset_net(ENV_TCP_CONNECT_NAME, ruleset_fd, | |
380 | LANDLOCK_ACCESS_NET_CONNECT_TCP)) { | |
ba84b0bf MS |
381 | goto err_close_ruleset; |
382 | } | |
5e990dce | 383 | |
ba84b0bf MS |
384 | if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { |
385 | perror("Failed to restrict privileges"); | |
386 | goto err_close_ruleset; | |
387 | } | |
388 | if (landlock_restrict_self(ruleset_fd, 0)) { | |
389 | perror("Failed to enforce ruleset"); | |
390 | goto err_close_ruleset; | |
391 | } | |
392 | close(ruleset_fd); | |
393 | ||
394 | cmd_path = argv[1]; | |
395 | cmd_argv = argv + 1; | |
a17c60e5 | 396 | fprintf(stderr, "Executing the sandboxed command...\n"); |
ba84b0bf MS |
397 | execvpe(cmd_path, cmd_argv, envp); |
398 | fprintf(stderr, "Failed to execute \"%s\": %s\n", cmd_path, | |
81709f3d | 399 | strerror(errno)); |
ba84b0bf MS |
400 | fprintf(stderr, "Hint: access to the binary, the interpreter or " |
401 | "shared libraries may be denied.\n"); | |
402 | return 1; | |
403 | ||
404 | err_close_ruleset: | |
405 | close(ruleset_fd); | |
406 | return 1; | |
407 | } |