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