Skip to content

Commit a5874fd

Browse files
l0kodkees
authored andcommitted
exec: Add a new AT_EXECVE_CHECK flag to execveat(2)
Add a new AT_EXECVE_CHECK flag to execveat(2) to check if a file would be allowed for execution. The main use case is for script interpreters and dynamic linkers to check execution permission according to the kernel's security policy. Another use case is to add context to access logs e.g., which script (instead of interpreter) accessed a file. As any executable code, scripts could also use this check [1]. This is different from faccessat(2) + X_OK which only checks a subset of access rights (i.e. inode permission and mount options for regular files), but not the full context (e.g. all LSM access checks). The main use case for access(2) is for SUID processes to (partially) check access on behalf of their caller. The main use case for execveat(2) + AT_EXECVE_CHECK is to check if a script execution would be allowed, according to all the different restrictions in place. Because the use of AT_EXECVE_CHECK follows the exact kernel semantic as for a real execution, user space gets the same error codes. An interesting point of using execveat(2) instead of openat2(2) is that it decouples the check from the enforcement. Indeed, the security check can be logged (e.g. with audit) without blocking an execution environment not yet ready to enforce a strict security policy. LSMs can control or log execution requests with security_bprm_creds_for_exec(). However, to enforce a consistent and complete access control (e.g. on binary's dependencies) LSMs should restrict file executability, or measure executed files, with security_file_open() by checking file->f_flags & __FMODE_EXEC. Because AT_EXECVE_CHECK is dedicated to user space interpreters, it doesn't make sense for the kernel to parse the checked files, look for interpreters known to the kernel (e.g. ELF, shebang), and return ENOEXEC if the format is unknown. Because of that, security_bprm_check() is never called when AT_EXECVE_CHECK is used. It should be noted that script interpreters cannot directly use execveat(2) (without this new AT_EXECVE_CHECK flag) because this could lead to unexpected behaviors e.g., `python script.sh` could lead to Bash being executed to interpret the script. Unlike the kernel, script interpreters may just interpret the shebang as a simple comment, which should not change for backward compatibility reasons. Because scripts or libraries files might not currently have the executable permission set, or because we might want specific users to be allowed to run arbitrary scripts, the following patch provides a dynamic configuration mechanism with the SECBIT_EXEC_RESTRICT_FILE and SECBIT_EXEC_DENY_INTERACTIVE securebits. This is a redesign of the CLIP OS 4's O_MAYEXEC: https://github.com/clipos-archive/src_platform_clip-patches/blob/f5cb330d6b684752e403b4e41b39f7004d88e561/1901_open_mayexec.patch This patch has been used for more than a decade with customized script interpreters. Some examples can be found here: https://github.com/clipos-archive/clipos4_portage-overlay/search?q=O_MAYEXEC Cc: Al Viro <[email protected]> Cc: Christian Brauner <[email protected]> Cc: Kees Cook <[email protected]> Acked-by: Paul Moore <[email protected]> Reviewed-by: Serge Hallyn <[email protected]> Reviewed-by: Jeff Xu <[email protected]> Tested-by: Jeff Xu <[email protected]> Link: https://docs.python.org/3/library/io.html#io.open_code [1] Signed-off-by: Mickaël Salaün <[email protected]> Link: https://lore.kernel.org/r/[email protected] Signed-off-by: Kees Cook <[email protected]>
1 parent fac04ef commit a5874fd

File tree

6 files changed

+76
-3
lines changed

6 files changed

+76
-3
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
.. SPDX-License-Identifier: GPL-2.0
2+
.. Copyright © 2024 Microsoft Corporation
3+
4+
===================
5+
Executability check
6+
===================
7+
8+
AT_EXECVE_CHECK
9+
===============
10+
11+
Passing the ``AT_EXECVE_CHECK`` flag to :manpage:`execveat(2)` only performs a
12+
check on a regular file and returns 0 if execution of this file would be
13+
allowed, ignoring the file format and then the related interpreter dependencies
14+
(e.g. ELF libraries, script's shebang).
15+
16+
Programs should always perform this check to apply kernel-level checks against
17+
files that are not directly executed by the kernel but passed to a user space
18+
interpreter instead. All files that contain executable code, from the point of
19+
view of the interpreter, should be checked. However the result of this check
20+
should only be enforced according to ``SECBIT_EXEC_RESTRICT_FILE`` or
21+
``SECBIT_EXEC_DENY_INTERACTIVE.``.
22+
23+
The main purpose of this flag is to improve the security and consistency of an
24+
execution environment to ensure that direct file execution (e.g.
25+
``./script.sh``) and indirect file execution (e.g. ``sh script.sh``) lead to
26+
the same result. For instance, this can be used to check if a file is
27+
trustworthy according to the caller's environment.
28+
29+
In a secure environment, libraries and any executable dependencies should also
30+
be checked. For instance, dynamic linking should make sure that all libraries
31+
are allowed for execution to avoid trivial bypass (e.g. using ``LD_PRELOAD``).
32+
For such secure execution environment to make sense, only trusted code should
33+
be executable, which also requires integrity guarantees.
34+
35+
To avoid race conditions leading to time-of-check to time-of-use issues,
36+
``AT_EXECVE_CHECK`` should be used with ``AT_EMPTY_PATH`` to check against a
37+
file descriptor instead of a path.

Documentation/userspace-api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Security-related interfaces
3535
mfd_noexec
3636
spec_ctrl
3737
tee
38+
check_exec
3839

3940
Devices and I/O
4041
===============

fs/exec.c

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -892,7 +892,8 @@ static struct file *do_open_execat(int fd, struct filename *name, int flags)
892892
.lookup_flags = LOOKUP_FOLLOW,
893893
};
894894

895-
if ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH)) != 0)
895+
if ((flags &
896+
~(AT_SYMLINK_NOFOLLOW | AT_EMPTY_PATH | AT_EXECVE_CHECK)) != 0)
896897
return ERR_PTR(-EINVAL);
897898
if (flags & AT_SYMLINK_NOFOLLOW)
898899
open_exec_flags.lookup_flags &= ~LOOKUP_FOLLOW;
@@ -1541,6 +1542,21 @@ static struct linux_binprm *alloc_bprm(int fd, struct filename *filename, int fl
15411542
}
15421543
bprm->interp = bprm->filename;
15431544

1545+
/*
1546+
* At this point, security_file_open() has already been called (with
1547+
* __FMODE_EXEC) and access control checks for AT_EXECVE_CHECK will
1548+
* stop just after the security_bprm_creds_for_exec() call in
1549+
* bprm_execve(). Indeed, the kernel should not try to parse the
1550+
* content of the file with exec_binprm() nor change the calling
1551+
* thread, which means that the following security functions will not
1552+
* be called:
1553+
* - security_bprm_check()
1554+
* - security_bprm_creds_from_file()
1555+
* - security_bprm_committing_creds()
1556+
* - security_bprm_committed_creds()
1557+
*/
1558+
bprm->is_check = !!(flags & AT_EXECVE_CHECK);
1559+
15441560
retval = bprm_mm_init(bprm);
15451561
if (!retval)
15461562
return bprm;
@@ -1836,7 +1852,7 @@ static int bprm_execve(struct linux_binprm *bprm)
18361852

18371853
/* Set the unchanging part of bprm->cred */
18381854
retval = security_bprm_creds_for_exec(bprm);
1839-
if (retval)
1855+
if (retval || bprm->is_check)
18401856
goto out;
18411857

18421858
retval = exec_binprm(bprm);

include/linux/binfmts.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ struct linux_binprm {
4242
* Set when errors can no longer be returned to the
4343
* original userspace.
4444
*/
45-
point_of_no_return:1;
45+
point_of_no_return:1,
46+
/*
47+
* Set by user space to check executability according to the
48+
* caller's environment.
49+
*/
50+
is_check:1;
4651
struct file *executable; /* Executable to pass to the interpreter */
4752
struct file *interpreter;
4853
struct file *file;

include/uapi/linux/fcntl.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,8 @@
155155
#define AT_HANDLE_MNT_ID_UNIQUE 0x001 /* Return the u64 unique mount ID. */
156156
#define AT_HANDLE_CONNECTABLE 0x002 /* Request a connectable file handle */
157157

158+
/* Flags for execveat2(2). */
159+
#define AT_EXECVE_CHECK 0x10000 /* Only perform a check if execution
160+
would be allowed. */
161+
158162
#endif /* _UAPI_LINUX_FCNTL_H */

security/security.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,12 @@ int security_vm_enough_memory_mm(struct mm_struct *mm, long pages)
12481248
* to 1 if AT_SECURE should be set to request libc enable secure mode. @bprm
12491249
* contains the linux_binprm structure.
12501250
*
1251+
* If execveat(2) is called with the AT_EXECVE_CHECK flag, bprm->is_check is
1252+
* set. The result must be the same as without this flag even if the execution
1253+
* will never really happen and @bprm will always be dropped.
1254+
*
1255+
* This hook must not change current->cred, only @bprm->cred.
1256+
*
12511257
* Return: Returns 0 if the hook is successful and permission is granted.
12521258
*/
12531259
int security_bprm_creds_for_exec(struct linux_binprm *bprm)
@@ -3098,6 +3104,10 @@ int security_file_receive(struct file *file)
30983104
* Save open-time permission checking state for later use upon file_permission,
30993105
* and recheck access if anything has changed since inode_permission.
31003106
*
3107+
* We can check if a file is opened for execution (e.g. execve(2) call), either
3108+
* directly or indirectly (e.g. ELF's ld.so) by checking file->f_flags &
3109+
* __FMODE_EXEC .
3110+
*
31013111
* Return: Returns 0 if permission is granted.
31023112
*/
31033113
int security_file_open(struct file *file)

0 commit comments

Comments
 (0)