1 // SPDX-License-Identifier: BSD-3-Clause
3 * Simple Landlock sandbox manager able to execute a process restricted by
4 * user-defined file system and network access control policies.
6 * Copyright © 2017-2020 Mickaël Salaün <mic@digikod.net>
7 * Copyright © 2020 ANSSI
11 #define __SANE_USERSPACE_TYPES__
12 #include <arpa/inet.h>
15 #include <linux/landlock.h>
16 #include <linux/prctl.h>
17 #include <linux/socket.h>
22 #include <sys/prctl.h>
24 #include <sys/syscall.h>
28 #ifndef landlock_create_ruleset
30 landlock_create_ruleset(const struct landlock_ruleset_attr
*const attr
,
31 const size_t size
, const __u32 flags
)
33 return syscall(__NR_landlock_create_ruleset
, attr
, size
, flags
);
37 #ifndef landlock_add_rule
38 static inline int landlock_add_rule(const int ruleset_fd
,
39 const enum landlock_rule_type rule_type
,
40 const void *const rule_attr
,
43 return syscall(__NR_landlock_add_rule
, ruleset_fd
, rule_type
, rule_attr
,
48 #ifndef landlock_restrict_self
49 static inline int landlock_restrict_self(const int ruleset_fd
,
52 return syscall(__NR_landlock_restrict_self
, ruleset_fd
, flags
);
56 #define ENV_FS_RO_NAME "LL_FS_RO"
57 #define ENV_FS_RW_NAME "LL_FS_RW"
58 #define ENV_TCP_BIND_NAME "LL_TCP_BIND"
59 #define ENV_TCP_CONNECT_NAME "LL_TCP_CONNECT"
60 #define ENV_SCOPED_NAME "LL_SCOPED"
61 #define ENV_DELIMITER ":"
63 static int str2num(const char *numstr
, __u64
*num_dst
)
70 num
= strtoull(numstr
, &endptr
, 10);
73 /* Was the string empty, or not entirely parsed successfully? */
74 else if ((*numstr
== '\0') || (*endptr
!= '\0'))
82 static int parse_path(char *env_path
, const char ***const path_list
)
88 for (i
= 0; env_path
[i
]; i
++) {
89 if (env_path
[i
] == ENV_DELIMITER
[0])
93 *path_list
= malloc(num_paths
* sizeof(**path_list
));
94 for (i
= 0; i
< num_paths
; i
++)
95 (*path_list
)[i
] = strsep(&env_path
, ENV_DELIMITER
);
100 /* clang-format off */
102 #define ACCESS_FILE ( \
103 LANDLOCK_ACCESS_FS_EXECUTE | \
104 LANDLOCK_ACCESS_FS_WRITE_FILE | \
105 LANDLOCK_ACCESS_FS_READ_FILE | \
106 LANDLOCK_ACCESS_FS_TRUNCATE | \
107 LANDLOCK_ACCESS_FS_IOCTL_DEV)
109 /* clang-format on */
111 static int populate_ruleset_fs(const char *const env_var
, const int ruleset_fd
,
112 const __u64 allowed_access
)
114 int num_paths
, i
, ret
= 1;
116 const char **path_list
= NULL
;
117 struct landlock_path_beneath_attr path_beneath
= {
121 env_path_name
= getenv(env_var
);
122 if (!env_path_name
) {
123 /* Prevents users to forget a setting. */
124 fprintf(stderr
, "Missing environment variable %s\n", env_var
);
127 env_path_name
= strdup(env_path_name
);
129 num_paths
= parse_path(env_path_name
, &path_list
);
130 if (num_paths
== 1 && path_list
[0][0] == '\0') {
132 * Allows to not use all possible restrictions (e.g. use
133 * LL_FS_RO without LL_FS_RW).
139 for (i
= 0; i
< num_paths
; i
++) {
142 path_beneath
.parent_fd
= open(path_list
[i
], O_PATH
| O_CLOEXEC
);
143 if (path_beneath
.parent_fd
< 0) {
144 fprintf(stderr
, "Failed to open \"%s\": %s\n",
145 path_list
[i
], strerror(errno
));
148 if (fstat(path_beneath
.parent_fd
, &statbuf
)) {
149 fprintf(stderr
, "Failed to stat \"%s\": %s\n",
150 path_list
[i
], strerror(errno
));
151 close(path_beneath
.parent_fd
);
154 path_beneath
.allowed_access
= allowed_access
;
155 if (!S_ISDIR(statbuf
.st_mode
))
156 path_beneath
.allowed_access
&= ACCESS_FILE
;
157 if (landlock_add_rule(ruleset_fd
, LANDLOCK_RULE_PATH_BENEATH
,
160 "Failed to update the ruleset with \"%s\": %s\n",
161 path_list
[i
], strerror(errno
));
162 close(path_beneath
.parent_fd
);
165 close(path_beneath
.parent_fd
);
175 static int populate_ruleset_net(const char *const env_var
, const int ruleset_fd
,
176 const __u64 allowed_access
)
179 char *env_port_name
, *env_port_name_next
, *strport
;
180 struct landlock_net_port_attr net_port
= {
181 .allowed_access
= allowed_access
,
184 env_port_name
= getenv(env_var
);
187 env_port_name
= strdup(env_port_name
);
190 env_port_name_next
= env_port_name
;
191 while ((strport
= strsep(&env_port_name_next
, ENV_DELIMITER
))) {
194 if (strcmp(strport
, "") == 0)
197 if (str2num(strport
, &port
)) {
198 fprintf(stderr
, "Failed to parse port at \"%s\"\n",
202 net_port
.port
= port
;
203 if (landlock_add_rule(ruleset_fd
, LANDLOCK_RULE_NET_PORT
,
206 "Failed to update the ruleset with port \"%llu\": %s\n",
207 net_port
.port
, strerror(errno
));
218 /* Returns true on error, false otherwise. */
219 static bool check_ruleset_scope(const char *const env_var
,
220 struct landlock_ruleset_attr
*ruleset_attr
)
222 char *env_type_scope
, *env_type_scope_next
, *ipc_scoping_name
;
224 bool abstract_scoping
= false;
225 bool signal_scoping
= false;
227 /* Scoping is not supported by Landlock ABI */
228 if (!(ruleset_attr
->scoped
&
229 (LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
| LANDLOCK_SCOPE_SIGNAL
)))
232 env_type_scope
= getenv(env_var
);
233 /* Scoping is not supported by the user */
234 if (!env_type_scope
|| strcmp("", env_type_scope
) == 0)
237 env_type_scope
= strdup(env_type_scope
);
238 env_type_scope_next
= env_type_scope
;
239 while ((ipc_scoping_name
=
240 strsep(&env_type_scope_next
, ENV_DELIMITER
))) {
241 if (strcmp("a", ipc_scoping_name
) == 0 && !abstract_scoping
) {
242 abstract_scoping
= true;
243 } else if (strcmp("s", ipc_scoping_name
) == 0 &&
245 signal_scoping
= true;
247 fprintf(stderr
, "Unknown or duplicate scope \"%s\"\n",
255 free(env_type_scope
);
258 if (!abstract_scoping
)
259 ruleset_attr
->scoped
&= ~LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
;
261 ruleset_attr
->scoped
&= ~LANDLOCK_SCOPE_SIGNAL
;
267 /* clang-format off */
269 #define ACCESS_FS_ROUGHLY_READ ( \
270 LANDLOCK_ACCESS_FS_EXECUTE | \
271 LANDLOCK_ACCESS_FS_READ_FILE | \
272 LANDLOCK_ACCESS_FS_READ_DIR)
274 #define ACCESS_FS_ROUGHLY_WRITE ( \
275 LANDLOCK_ACCESS_FS_WRITE_FILE | \
276 LANDLOCK_ACCESS_FS_REMOVE_DIR | \
277 LANDLOCK_ACCESS_FS_REMOVE_FILE | \
278 LANDLOCK_ACCESS_FS_MAKE_CHAR | \
279 LANDLOCK_ACCESS_FS_MAKE_DIR | \
280 LANDLOCK_ACCESS_FS_MAKE_REG | \
281 LANDLOCK_ACCESS_FS_MAKE_SOCK | \
282 LANDLOCK_ACCESS_FS_MAKE_FIFO | \
283 LANDLOCK_ACCESS_FS_MAKE_BLOCK | \
284 LANDLOCK_ACCESS_FS_MAKE_SYM | \
285 LANDLOCK_ACCESS_FS_REFER | \
286 LANDLOCK_ACCESS_FS_TRUNCATE | \
287 LANDLOCK_ACCESS_FS_IOCTL_DEV)
289 /* clang-format on */
291 #define LANDLOCK_ABI_LAST 6
294 #define STR(s) XSTR(s)
296 /* clang-format off */
298 static const char help
[] =
299 "usage: " ENV_FS_RO_NAME
"=\"...\" " ENV_FS_RW_NAME
"=\"...\" "
300 "[other environment variables] %1$s <cmd> [args]...\n"
302 "Execute the given command in a restricted environment.\n"
303 "Multi-valued settings (lists of ports, paths, scopes) are colon-delimited.\n"
305 "Mandatory settings:\n"
306 "* " ENV_FS_RO_NAME
": paths allowed to be used in a read-only way\n"
307 "* " ENV_FS_RW_NAME
": paths allowed to be used in a read-write way\n"
309 "Optional settings (when not set, their associated access check "
310 "is always allowed, which is different from an empty string which "
311 "means an empty list):\n"
312 "* " ENV_TCP_BIND_NAME
": ports allowed to bind (server)\n"
313 "* " ENV_TCP_CONNECT_NAME
": ports allowed to connect (client)\n"
314 "* " ENV_SCOPED_NAME
": actions denied on the outside of the landlock domain\n"
315 " - \"a\" to restrict opening abstract unix sockets\n"
316 " - \"s\" to restrict sending signals\n"
319 ENV_FS_RO_NAME
"=\"${PATH}:/lib:/usr:/proc:/etc:/dev/urandom\" "
320 ENV_FS_RW_NAME
"=\"/dev/null:/dev/full:/dev/zero:/dev/pts:/tmp\" "
321 ENV_TCP_BIND_NAME
"=\"9418\" "
322 ENV_TCP_CONNECT_NAME
"=\"80:443\" "
323 ENV_SCOPED_NAME
"=\"a:s\" "
326 "This sandboxer can use Landlock features up to ABI version "
327 STR(LANDLOCK_ABI_LAST
) ".\n";
329 /* clang-format on */
331 int main(const int argc
, char *const argv
[], char *const *const envp
)
333 const char *cmd_path
;
334 char *const *cmd_argv
;
337 __u64 access_fs_ro
= ACCESS_FS_ROUGHLY_READ
,
338 access_fs_rw
= ACCESS_FS_ROUGHLY_READ
| ACCESS_FS_ROUGHLY_WRITE
;
340 struct landlock_ruleset_attr ruleset_attr
= {
341 .handled_access_fs
= access_fs_rw
,
342 .handled_access_net
= LANDLOCK_ACCESS_NET_BIND_TCP
|
343 LANDLOCK_ACCESS_NET_CONNECT_TCP
,
344 .scoped
= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
345 LANDLOCK_SCOPE_SIGNAL
,
349 fprintf(stderr
, help
, argv
[0]);
353 abi
= landlock_create_ruleset(NULL
, 0, LANDLOCK_CREATE_RULESET_VERSION
);
355 const int err
= errno
;
357 perror("Failed to check Landlock compatibility");
361 "Hint: Landlock is not supported by the current kernel. "
362 "To support it, build the kernel with "
363 "CONFIG_SECURITY_LANDLOCK=y and prepend "
364 "\"landlock,\" to the content of CONFIG_LSM.\n");
368 "Hint: Landlock is currently disabled. "
369 "It can be enabled in the kernel configuration by "
370 "prepending \"landlock,\" to the content of CONFIG_LSM, "
371 "or at boot time by setting the same content to the "
372 "\"lsm\" kernel parameter.\n");
378 /* Best-effort security. */
382 * Removes LANDLOCK_ACCESS_FS_REFER for ABI < 2
384 * Note: The "refer" operations (file renaming and linking
385 * across different directories) are always forbidden when using
386 * Landlock with ABI 1.
388 * If only ABI 1 is available, this sandboxer knowingly forbids
391 * If a program *needs* to do refer operations after enabling
392 * Landlock, it can not use Landlock at ABI level 1. To be
393 * compatible with different kernel versions, such programs
394 * should then fall back to not restrict themselves at all if
395 * the running kernel only supports ABI 1.
397 ruleset_attr
.handled_access_fs
&= ~LANDLOCK_ACCESS_FS_REFER
;
398 __attribute__((fallthrough
));
400 /* Removes LANDLOCK_ACCESS_FS_TRUNCATE for ABI < 3 */
401 ruleset_attr
.handled_access_fs
&= ~LANDLOCK_ACCESS_FS_TRUNCATE
;
402 __attribute__((fallthrough
));
404 /* Removes network support for ABI < 4 */
405 ruleset_attr
.handled_access_net
&=
406 ~(LANDLOCK_ACCESS_NET_BIND_TCP
|
407 LANDLOCK_ACCESS_NET_CONNECT_TCP
);
408 __attribute__((fallthrough
));
410 /* Removes LANDLOCK_ACCESS_FS_IOCTL_DEV for ABI < 5 */
411 ruleset_attr
.handled_access_fs
&= ~LANDLOCK_ACCESS_FS_IOCTL_DEV
;
413 __attribute__((fallthrough
));
415 /* Removes LANDLOCK_SCOPE_* for ABI < 6 */
416 ruleset_attr
.scoped
&= ~(LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
417 LANDLOCK_SCOPE_SIGNAL
);
419 "Hint: You should update the running kernel "
420 "to leverage Landlock features "
421 "provided by ABI version %d (instead of %d).\n",
422 LANDLOCK_ABI_LAST
, abi
);
423 __attribute__((fallthrough
));
424 case LANDLOCK_ABI_LAST
:
428 "Hint: You should update this sandboxer "
429 "to leverage Landlock features "
430 "provided by ABI version %d (instead of %d).\n",
431 abi
, LANDLOCK_ABI_LAST
);
433 access_fs_ro
&= ruleset_attr
.handled_access_fs
;
434 access_fs_rw
&= ruleset_attr
.handled_access_fs
;
436 /* Removes bind access attribute if not supported by a user. */
437 env_port_name
= getenv(ENV_TCP_BIND_NAME
);
438 if (!env_port_name
) {
439 ruleset_attr
.handled_access_net
&=
440 ~LANDLOCK_ACCESS_NET_BIND_TCP
;
442 /* Removes connect access attribute if not supported by a user. */
443 env_port_name
= getenv(ENV_TCP_CONNECT_NAME
);
444 if (!env_port_name
) {
445 ruleset_attr
.handled_access_net
&=
446 ~LANDLOCK_ACCESS_NET_CONNECT_TCP
;
449 if (check_ruleset_scope(ENV_SCOPED_NAME
, &ruleset_attr
))
453 landlock_create_ruleset(&ruleset_attr
, sizeof(ruleset_attr
), 0);
454 if (ruleset_fd
< 0) {
455 perror("Failed to create a ruleset");
459 if (populate_ruleset_fs(ENV_FS_RO_NAME
, ruleset_fd
, access_fs_ro
)) {
460 goto err_close_ruleset
;
462 if (populate_ruleset_fs(ENV_FS_RW_NAME
, ruleset_fd
, access_fs_rw
)) {
463 goto err_close_ruleset
;
466 if (populate_ruleset_net(ENV_TCP_BIND_NAME
, ruleset_fd
,
467 LANDLOCK_ACCESS_NET_BIND_TCP
)) {
468 goto err_close_ruleset
;
470 if (populate_ruleset_net(ENV_TCP_CONNECT_NAME
, ruleset_fd
,
471 LANDLOCK_ACCESS_NET_CONNECT_TCP
)) {
472 goto err_close_ruleset
;
475 if (prctl(PR_SET_NO_NEW_PRIVS
, 1, 0, 0, 0)) {
476 perror("Failed to restrict privileges");
477 goto err_close_ruleset
;
479 if (landlock_restrict_self(ruleset_fd
, 0)) {
480 perror("Failed to enforce ruleset");
481 goto err_close_ruleset
;
487 fprintf(stderr
, "Executing the sandboxed command...\n");
488 execvpe(cmd_path
, cmd_argv
, envp
);
489 fprintf(stderr
, "Failed to execute \"%s\": %s\n", cmd_path
,
491 fprintf(stderr
, "Hint: access to the binary, the interpreter or "
492 "shared libraries may be denied.\n");