1 | // SPDX-License-Identifier: BSD-3-Clause |
2 | /* |
3 | * Simple Landlock sandbox manager able to execute a process restricted by |
4 | * user-defined file system and network access control policies. |
5 | * |
6 | * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net> |
7 | * Copyright © 2020 ANSSI |
8 | */ |
9 | |
10 | #define _GNU_SOURCE |
11 | #define __SANE_USERSPACE_TYPES__ |
12 | #include <arpa/inet.h> |
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 |
27 | static inline int |
28 | landlock_create_ruleset(const struct landlock_ruleset_attr *const attr, |
29 | const size_t size, const __u32 flags) |
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, |
37 | const enum landlock_rule_type rule_type, |
38 | const void *const rule_attr, |
39 | const __u32 flags) |
40 | { |
41 | return syscall(__NR_landlock_add_rule, ruleset_fd, rule_type, rule_attr, |
42 | flags); |
43 | } |
44 | #endif |
45 | |
46 | #ifndef landlock_restrict_self |
47 | static inline int landlock_restrict_self(const int ruleset_fd, |
48 | const __u32 flags) |
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" |
56 | #define ENV_TCP_BIND_NAME "LL_TCP_BIND" |
57 | #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT" |
58 | #define ENV_DELIMITER ":" |
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++) { |
67 | if (env_path[i] == ENV_DELIMITER[0]) |
68 | num_paths++; |
69 | } |
70 | } |
71 | *path_list = malloc(size: num_paths * sizeof(**path_list)); |
72 | for (i = 0; i < num_paths; i++) |
73 | (*path_list)[i] = strsep(stringp: &env_path, ENV_DELIMITER); |
74 | |
75 | return num_paths; |
76 | } |
77 | |
78 | /* clang-format off */ |
79 | |
80 | #define ACCESS_FILE ( \ |
81 | LANDLOCK_ACCESS_FS_EXECUTE | \ |
82 | LANDLOCK_ACCESS_FS_WRITE_FILE | \ |
83 | LANDLOCK_ACCESS_FS_READ_FILE | \ |
84 | LANDLOCK_ACCESS_FS_TRUNCATE) |
85 | |
86 | /* clang-format on */ |
87 | |
88 | static int populate_ruleset_fs(const char *const env_var, const int ruleset_fd, |
89 | const __u64 allowed_access) |
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(name: env_var); |
99 | if (!env_path_name) { |
100 | /* Prevents users to forget a setting. */ |
101 | fprintf(stderr, format: "Missing environment variable %s\n" , env_var); |
102 | return 1; |
103 | } |
104 | env_path_name = strdup(s: env_path_name); |
105 | unsetenv(name: env_var); |
106 | num_paths = parse_path(env_path: env_path_name, path_list: &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 | |
119 | path_beneath.parent_fd = open(file: path_list[i], O_PATH | O_CLOEXEC); |
120 | if (path_beneath.parent_fd < 0) { |
121 | fprintf(stderr, format: "Failed to open \"%s\": %s\n" , |
122 | path_list[i], strerror(errno)); |
123 | continue; |
124 | } |
125 | if (fstat(fd: path_beneath.parent_fd, buf: &statbuf)) { |
126 | fprintf(stderr, format: "Failed to stat \"%s\": %s\n" , |
127 | path_list[i], strerror(errno)); |
128 | close(fd: 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, rule_type: LANDLOCK_RULE_PATH_BENEATH, |
135 | rule_attr: &path_beneath, flags: 0)) { |
136 | fprintf(stderr, |
137 | format: "Failed to update the ruleset with \"%s\": %s\n" , |
138 | path_list[i], strerror(errno)); |
139 | close(fd: path_beneath.parent_fd); |
140 | goto out_free_name; |
141 | } |
142 | close(fd: path_beneath.parent_fd); |
143 | } |
144 | ret = 0; |
145 | |
146 | out_free_name: |
147 | free(ptr: path_list); |
148 | free(ptr: env_path_name); |
149 | return ret; |
150 | } |
151 | |
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(name: env_var); |
163 | if (!env_port_name) |
164 | return 0; |
165 | env_port_name = strdup(s: env_port_name); |
166 | unsetenv(name: env_var); |
167 | |
168 | while ((strport = strsep(stringp: &env_port_name, ENV_DELIMITER))) { |
169 | net_port.port = atoi(nptr: strport); |
170 | if (landlock_add_rule(ruleset_fd, rule_type: LANDLOCK_RULE_NET_PORT, |
171 | rule_attr: &net_port, flags: 0)) { |
172 | fprintf(stderr, |
173 | format: "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(ptr: env_port_name); |
182 | return ret; |
183 | } |
184 | |
185 | /* clang-format off */ |
186 | |
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 | \ |
202 | LANDLOCK_ACCESS_FS_MAKE_SYM | \ |
203 | LANDLOCK_ACCESS_FS_REFER | \ |
204 | LANDLOCK_ACCESS_FS_TRUNCATE) |
205 | |
206 | /* clang-format on */ |
207 | |
208 | #define LANDLOCK_ABI_LAST 4 |
209 | |
210 | int main(const int argc, char *const argv[], char *const *const envp) |
211 | { |
212 | const char *cmd_path; |
213 | char *const *cmd_argv; |
214 | int ruleset_fd, abi; |
215 | char *env_port_name; |
216 | __u64 access_fs_ro = ACCESS_FS_ROUGHLY_READ, |
217 | access_fs_rw = ACCESS_FS_ROUGHLY_READ | ACCESS_FS_ROUGHLY_WRITE; |
218 | |
219 | struct landlock_ruleset_attr ruleset_attr = { |
220 | .handled_access_fs = access_fs_rw, |
221 | .handled_access_net = LANDLOCK_ACCESS_NET_BIND_TCP | |
222 | LANDLOCK_ACCESS_NET_CONNECT_TCP, |
223 | }; |
224 | |
225 | if (argc < 2) { |
226 | fprintf(stderr, |
227 | format: "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]); |
231 | fprintf(stderr, |
232 | format: "Execute a command in a restricted environment.\n\n" ); |
233 | fprintf(stderr, |
234 | format: "Environment variables containing paths and ports " |
235 | "each separated by a colon:\n" ); |
236 | fprintf(stderr, |
237 | format: "* %s: list of paths allowed to be used in a read-only way.\n" , |
238 | ENV_FS_RO_NAME); |
239 | fprintf(stderr, |
240 | format: "* %s: list of paths allowed to be used in a read-write way.\n\n" , |
241 | ENV_FS_RW_NAME); |
242 | fprintf(stderr, |
243 | format: "Environment variables containing ports are optional " |
244 | "and could be skipped.\n" ); |
245 | fprintf(stderr, |
246 | format: "* %s: list of ports allowed to bind (server).\n" , |
247 | ENV_TCP_BIND_NAME); |
248 | fprintf(stderr, |
249 | format: "* %s: list of ports allowed to connect (client).\n" , |
250 | ENV_TCP_CONNECT_NAME); |
251 | fprintf(stderr, |
252 | format: "\nexample:\n" |
253 | "%s=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" " |
254 | "%s=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" " |
255 | "%s=\"9418\" " |
256 | "%s=\"80:443\" " |
257 | "%s bash -i\n\n" , |
258 | ENV_FS_RO_NAME, ENV_FS_RW_NAME, ENV_TCP_BIND_NAME, |
259 | ENV_TCP_CONNECT_NAME, argv[0]); |
260 | fprintf(stderr, |
261 | format: "This sandboxer can use Landlock features " |
262 | "up to ABI version %d.\n" , |
263 | LANDLOCK_ABI_LAST); |
264 | return 1; |
265 | } |
266 | |
267 | abi = landlock_create_ruleset(NULL, size: 0, LANDLOCK_CREATE_RULESET_VERSION); |
268 | if (abi < 0) { |
269 | const int err = errno; |
270 | |
271 | perror(s: "Failed to check Landlock compatibility" ); |
272 | switch (err) { |
273 | case ENOSYS: |
274 | fprintf(stderr, |
275 | format: "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" ); |
279 | break; |
280 | case EOPNOTSUPP: |
281 | fprintf(stderr, |
282 | format: "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" ); |
287 | break; |
288 | } |
289 | return 1; |
290 | } |
291 | |
292 | /* Best-effort security. */ |
293 | switch (abi) { |
294 | case 1: |
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 | */ |
311 | ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; |
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; |
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); |
322 | fprintf(stderr, |
323 | format: "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 | format: "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); |
336 | } |
337 | access_fs_ro &= ruleset_attr.handled_access_fs; |
338 | access_fs_rw &= ruleset_attr.handled_access_fs; |
339 | |
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 | |
353 | ruleset_fd = |
354 | landlock_create_ruleset(attr: &ruleset_attr, size: sizeof(ruleset_attr), flags: 0); |
355 | if (ruleset_fd < 0) { |
356 | perror(s: "Failed to create a ruleset" ); |
357 | return 1; |
358 | } |
359 | |
360 | if (populate_ruleset_fs(ENV_FS_RO_NAME, ruleset_fd, allowed_access: access_fs_ro)) { |
361 | goto err_close_ruleset; |
362 | } |
363 | if (populate_ruleset_fs(ENV_FS_RW_NAME, ruleset_fd, allowed_access: access_fs_rw)) { |
364 | goto err_close_ruleset; |
365 | } |
366 | |
367 | if (populate_ruleset_net(ENV_TCP_BIND_NAME, ruleset_fd, |
368 | allowed_access: LANDLOCK_ACCESS_NET_BIND_TCP)) { |
369 | goto err_close_ruleset; |
370 | } |
371 | if (populate_ruleset_net(ENV_TCP_CONNECT_NAME, ruleset_fd, |
372 | allowed_access: LANDLOCK_ACCESS_NET_CONNECT_TCP)) { |
373 | goto err_close_ruleset; |
374 | } |
375 | |
376 | if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) { |
377 | perror(s: "Failed to restrict privileges" ); |
378 | goto err_close_ruleset; |
379 | } |
380 | if (landlock_restrict_self(ruleset_fd, flags: 0)) { |
381 | perror(s: "Failed to enforce ruleset" ); |
382 | goto err_close_ruleset; |
383 | } |
384 | close(fd: ruleset_fd); |
385 | |
386 | cmd_path = argv[1]; |
387 | cmd_argv = argv + 1; |
388 | fprintf(stderr, format: "Executing the sandboxed command...\n" ); |
389 | execvpe(file: cmd_path, argv: cmd_argv, envp: envp); |
390 | fprintf(stderr, format: "Failed to execute \"%s\": %s\n" , cmd_path, |
391 | strerror(errno)); |
392 | fprintf(stderr, format: "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(fd: ruleset_fd); |
398 | return 1; |
399 | } |
400 | |