diff options
-rw-r--r-- | Documentation/userspace-api/landlock.rst | 67 | ||||
-rw-r--r-- | fs/namei.c | 2 | ||||
-rw-r--r-- | fs/open.c | 2 | ||||
-rw-r--r-- | include/linux/lsm_hook_defs.h | 1 | ||||
-rw-r--r-- | include/linux/lsm_hooks.h | 10 | ||||
-rw-r--r-- | include/linux/security.h | 6 | ||||
-rw-r--r-- | include/uapi/linux/landlock.h | 21 | ||||
-rw-r--r-- | samples/landlock/sandboxer.c | 29 | ||||
-rw-r--r-- | security/apparmor/lsm.c | 6 | ||||
-rw-r--r-- | security/landlock/fs.c | 206 | ||||
-rw-r--r-- | security/landlock/fs.h | 24 | ||||
-rw-r--r-- | security/landlock/limits.h | 2 | ||||
-rw-r--r-- | security/landlock/setup.c | 1 | ||||
-rw-r--r-- | security/landlock/syscalls.c | 2 | ||||
-rw-r--r-- | security/security.c | 16 | ||||
-rw-r--r-- | security/tomoyo/tomoyo.c | 13 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/base_test.c | 38 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/common.h | 85 | ||||
-rw-r--r-- | tools/testing/selftests/landlock/fs_test.c | 468 |
19 files changed, 878 insertions, 121 deletions
diff --git a/Documentation/userspace-api/landlock.rst b/Documentation/userspace-api/landlock.rst index cec780c2f497..d8cd8cd9ce25 100644 --- a/Documentation/userspace-api/landlock.rst +++ b/Documentation/userspace-api/landlock.rst @@ -8,7 +8,7 @@ Landlock: unprivileged access control ===================================== :Author: Mickaël Salaün -:Date: September 2022 +:Date: October 2022 The goal of Landlock is to enable to restrict ambient rights (e.g. global filesystem access) for a set of processes. Because Landlock is a stackable @@ -60,7 +60,8 @@ the need to be explicit about the denied-by-default access rights. LANDLOCK_ACCESS_FS_MAKE_FIFO | LANDLOCK_ACCESS_FS_MAKE_BLOCK | LANDLOCK_ACCESS_FS_MAKE_SYM | - LANDLOCK_ACCESS_FS_REFER, + LANDLOCK_ACCESS_FS_REFER | + LANDLOCK_ACCESS_FS_TRUNCATE, }; Because we may not know on which kernel version an application will be @@ -69,16 +70,28 @@ should try to protect users as much as possible whatever the kernel they are using. To avoid binary enforcement (i.e. either all security features or none), we can leverage a dedicated Landlock command to get the current version of the Landlock ABI and adapt the handled accesses. Let's check if we should -remove the ``LANDLOCK_ACCESS_FS_REFER`` access right which is only supported -starting with the second version of the ABI. +remove the ``LANDLOCK_ACCESS_FS_REFER`` or ``LANDLOCK_ACCESS_FS_TRUNCATE`` +access rights, which are only supported starting with the second and third +version of the ABI. .. code-block:: c int abi; abi = landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION); - if (abi < 2) { + if (abi < 0) { + /* Degrades gracefully if Landlock is not handled. */ + perror("The running kernel does not enable to use Landlock"); + return 0; + } + switch (abi) { + case 1: + /* Removes LANDLOCK_ACCESS_FS_REFER for ABI < 2 */ ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; + __attribute__((fallthrough)); + case 2: + /* Removes LANDLOCK_ACCESS_FS_TRUNCATE for ABI < 3 */ + ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; } This enables to create an inclusive ruleset that will contain our rules. @@ -127,8 +140,8 @@ descriptor. It may also be required to create rules following the same logic as explained for the ruleset creation, by filtering access rights according to the Landlock -ABI version. In this example, this is not required because -``LANDLOCK_ACCESS_FS_REFER`` is not allowed by any rule. +ABI version. In this example, this is not required because all of the requested +``allowed_access`` rights are already available in ABI 1. We now have a ruleset with one rule allowing read access to ``/usr`` while denying all other handled accesses for the filesystem. The next step is to @@ -252,6 +265,37 @@ To be allowed to use :manpage:`ptrace(2)` and related syscalls on a target process, a sandboxed process should have a subset of the target process rules, which means the tracee must be in a sub-domain of the tracer. +Truncating files +---------------- + +The operations covered by ``LANDLOCK_ACCESS_FS_WRITE_FILE`` and +``LANDLOCK_ACCESS_FS_TRUNCATE`` both change the contents of a file and sometimes +overlap in non-intuitive ways. It is recommended to always specify both of +these together. + +A particularly surprising example is :manpage:`creat(2)`. The name suggests +that this system call requires the rights to create and write files. However, +it also requires the truncate right if an existing file under the same name is +already present. + +It should also be noted that truncating files does not require the +``LANDLOCK_ACCESS_FS_WRITE_FILE`` right. Apart from the :manpage:`truncate(2)` +system call, this can also be done through :manpage:`open(2)` with the flags +``O_RDONLY | O_TRUNC``. + +When opening a file, the availability of the ``LANDLOCK_ACCESS_FS_TRUNCATE`` +right is associated with the newly created file descriptor and will be used for +subsequent truncation attempts using :manpage:`ftruncate(2)`. The behavior is +similar to opening a file for reading or writing, where permissions are checked +during :manpage:`open(2)`, but not during the subsequent :manpage:`read(2)` and +:manpage:`write(2)` calls. + +As a consequence, it is possible to have multiple open file descriptors for the +same file, where one grants the right to truncate the file and the other does +not. It is also possible to pass such file descriptors between processes, +keeping their Landlock properties, even when these processes do not have an +enforced Landlock ruleset. + Compatibility ============= @@ -398,6 +442,15 @@ Starting with the Landlock ABI version 2, it is now possible to securely control renaming and linking thanks to the new ``LANDLOCK_ACCESS_FS_REFER`` access right. +File truncation (ABI < 3) +------------------------- + +File truncation could not be denied before the third Landlock ABI, so it is +always allowed when using a kernel that only supports the first or second ABI. + +Starting with the Landlock ABI version 3, it is now possible to securely control +truncation thanks to the new ``LANDLOCK_ACCESS_FS_TRUNCATE`` access right. + .. _kernel_support: Kernel support diff --git a/fs/namei.c b/fs/namei.c index 720270dc9fe5..309ae6fc8c99 100644 --- a/fs/namei.c +++ b/fs/namei.c @@ -3211,7 +3211,7 @@ static int handle_truncate(struct user_namespace *mnt_userns, struct file *filp) if (error) return error; - error = security_path_truncate(path); + error = security_file_truncate(filp); if (!error) { error = do_truncate(mnt_userns, path->dentry, 0, ATTR_MTIME|ATTR_CTIME|ATTR_OPEN, diff --git a/fs/open.c b/fs/open.c index 9d0197db15e7..82c1a28b3308 100644 --- a/fs/open.c +++ b/fs/open.c @@ -188,7 +188,7 @@ long do_sys_ftruncate(unsigned int fd, loff_t length, int small) if (IS_APPEND(file_inode(f.file))) goto out_putf; sb_start_write(inode->i_sb); - error = security_path_truncate(&f.file->f_path); + error = security_file_truncate(f.file); if (!error) error = do_truncate(file_mnt_user_ns(f.file), dentry, length, ATTR_MTIME | ATTR_CTIME, f.file); diff --git a/include/linux/lsm_hook_defs.h b/include/linux/lsm_hook_defs.h index 7f4aaddce298..3c984cadb88e 100644 --- a/include/linux/lsm_hook_defs.h +++ b/include/linux/lsm_hook_defs.h @@ -183,6 +183,7 @@ LSM_HOOK(int, 0, file_send_sigiotask, struct task_struct *tsk, struct fown_struct *fown, int sig) LSM_HOOK(int, 0, file_receive, struct file *file) LSM_HOOK(int, 0, file_open, struct file *file) +LSM_HOOK(int, 0, file_truncate, struct file *file) LSM_HOOK(int, 0, task_alloc, struct task_struct *task, unsigned long clone_flags) LSM_HOOK(void, LSM_RET_VOID, task_free, struct task_struct *task) diff --git a/include/linux/lsm_hooks.h b/include/linux/lsm_hooks.h index 1d02d1170e21..dfba3923c76b 100644 --- a/include/linux/lsm_hooks.h +++ b/include/linux/lsm_hooks.h @@ -409,7 +409,9 @@ * @attr is the iattr structure containing the new file attributes. * Return 0 if permission is granted. * @path_truncate: - * Check permission before truncating a file. + * Check permission before truncating the file indicated by path. + * Note that truncation permissions may also be checked based on + * already opened files, using the @file_truncate hook. * @path contains the path structure for the file. * Return 0 if permission is granted. * @inode_getattr: @@ -610,6 +612,12 @@ * to receive an open file descriptor via socket IPC. * @file contains the file structure being received. * Return 0 if permission is granted. + * @file_truncate: + * Check permission before truncating a file, i.e. using ftruncate. + * Note that truncation permission may also be checked based on the path, + * using the @path_truncate hook. + * @file contains the file structure for the file. + * Return 0 if permission is granted. * @file_open: * Save open-time permission checking state for later use upon * file_permission, and recheck access if anything has changed diff --git a/include/linux/security.h b/include/linux/security.h index 2bfc2e1ce51f..316f42716105 100644 --- a/include/linux/security.h +++ b/include/linux/security.h @@ -403,6 +403,7 @@ int security_file_send_sigiotask(struct task_struct *tsk, struct fown_struct *fown, int sig); int security_file_receive(struct file *file); int security_file_open(struct file *file); +int security_file_truncate(struct file *file); int security_task_alloc(struct task_struct *task, unsigned long clone_flags); void security_task_free(struct task_struct *task); int security_cred_alloc_blank(struct cred *cred, gfp_t gfp); @@ -1043,6 +1044,11 @@ static inline int security_file_open(struct file *file) return 0; } +static inline int security_file_truncate(struct file *file) +{ + return 0; +} + static inline int security_task_alloc(struct task_struct *task, unsigned long clone_flags) { diff --git a/include/uapi/linux/landlock.h b/include/uapi/linux/landlock.h index 9c4bcc37a455..f3223f964691 100644 --- a/include/uapi/linux/landlock.h +++ b/include/uapi/linux/landlock.h @@ -95,8 +95,19 @@ struct landlock_path_beneath_attr { * A file can only receive these access rights: * * - %LANDLOCK_ACCESS_FS_EXECUTE: Execute a file. - * - %LANDLOCK_ACCESS_FS_WRITE_FILE: Open a file with write access. + * - %LANDLOCK_ACCESS_FS_WRITE_FILE: Open a file with write access. Note that + * you might additionally need the %LANDLOCK_ACCESS_FS_TRUNCATE right in order + * to overwrite files with :manpage:`open(2)` using ``O_TRUNC`` or + * :manpage:`creat(2)`. * - %LANDLOCK_ACCESS_FS_READ_FILE: Open a file with read access. + * - %LANDLOCK_ACCESS_FS_TRUNCATE: Truncate a file with :manpage:`truncate(2)`, + * :manpage:`ftruncate(2)`, :manpage:`creat(2)`, or :manpage:`open(2)` with + * ``O_TRUNC``. Whether an opened file can be truncated with + * :manpage:`ftruncate(2)` is determined during :manpage:`open(2)`, in the + * same way as read and write permissions are checked during + * :manpage:`open(2)` using %LANDLOCK_ACCESS_FS_READ_FILE and + * %LANDLOCK_ACCESS_FS_WRITE_FILE. This access right is available since the + * third version of the Landlock ABI. * * A directory can receive access rights related to files or directories. The * following access right is applied to the directory itself, and the @@ -139,10 +150,9 @@ struct landlock_path_beneath_attr { * * It is currently not possible to restrict some file-related actions * accessible through these syscall families: :manpage:`chdir(2)`, - * :manpage:`truncate(2)`, :manpage:`stat(2)`, :manpage:`flock(2)`, - * :manpage:`chmod(2)`, :manpage:`chown(2)`, :manpage:`setxattr(2)`, - * :manpage:`utime(2)`, :manpage:`ioctl(2)`, :manpage:`fcntl(2)`, - * :manpage:`access(2)`. + * :manpage:`stat(2)`, :manpage:`flock(2)`, :manpage:`chmod(2)`, + * :manpage:`chown(2)`, :manpage:`setxattr(2)`, :manpage:`utime(2)`, + * :manpage:`ioctl(2)`, :manpage:`fcntl(2)`, :manpage:`access(2)`. * Future Landlock evolutions will enable to restrict them. */ /* clang-format off */ @@ -160,6 +170,7 @@ struct landlock_path_beneath_attr { #define LANDLOCK_ACCESS_FS_MAKE_BLOCK (1ULL << 11) #define LANDLOCK_ACCESS_FS_MAKE_SYM (1ULL << 12) #define LANDLOCK_ACCESS_FS_REFER (1ULL << 13) +#define LANDLOCK_ACCESS_FS_TRUNCATE (1ULL << 14) /* clang-format on */ #endif /* _UAPI_LINUX_LANDLOCK_H */ diff --git a/samples/landlock/sandboxer.c b/samples/landlock/sandboxer.c index f29bb3c72230..e2056c8b902c 100644 --- a/samples/landlock/sandboxer.c +++ b/samples/landlock/sandboxer.c @@ -76,7 +76,8 @@ static int parse_path(char *env_path, const char ***const path_list) #define ACCESS_FILE ( \ LANDLOCK_ACCESS_FS_EXECUTE | \ LANDLOCK_ACCESS_FS_WRITE_FILE | \ - LANDLOCK_ACCESS_FS_READ_FILE) + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_TRUNCATE) /* clang-format on */ @@ -160,11 +161,12 @@ out_free_name: LANDLOCK_ACCESS_FS_MAKE_FIFO | \ LANDLOCK_ACCESS_FS_MAKE_BLOCK | \ LANDLOCK_ACCESS_FS_MAKE_SYM | \ - LANDLOCK_ACCESS_FS_REFER) + LANDLOCK_ACCESS_FS_REFER | \ + LANDLOCK_ACCESS_FS_TRUNCATE) /* clang-format on */ -#define LANDLOCK_ABI_LAST 2 +#define LANDLOCK_ABI_LAST 3 int main(const int argc, char *const argv[], char *const *const envp) { @@ -232,8 +234,27 @@ int main(const int argc, char *const argv[], char *const *const envp) /* Best-effort security. */ switch (abi) { case 1: - /* Removes LANDLOCK_ACCESS_FS_REFER for ABI < 2 */ + /* + * Removes LANDLOCK_ACCESS_FS_REFER for ABI < 2 + * + * Note: The "refer" operations (file renaming and linking + * across different directories) are always forbidden when using + * Landlock with ABI 1. + * + * If only ABI 1 is available, this sandboxer knowingly forbids + * refer operations. + * + * If a program *needs* to do refer operations after enabling + * Landlock, it can not use Landlock at ABI level 1. To be + * compatible with different kernel versions, such programs + * should then fall back to not restrict themselves at all if + * the running kernel only supports ABI 1. + */ ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_REFER; + __attribute__((fallthrough)); + case 2: + /* Removes LANDLOCK_ACCESS_FS_TRUNCATE for ABI < 3 */ + ruleset_attr.handled_access_fs &= ~LANDLOCK_ACCESS_FS_TRUNCATE; fprintf(stderr, "Hint: You should update the running kernel " diff --git a/security/apparmor/lsm.c b/security/apparmor/lsm.c index f34675f7c3df..b751d6253977 100644 --- a/security/apparmor/lsm.c +++ b/security/apparmor/lsm.c @@ -333,6 +333,11 @@ static int apparmor_path_truncate(const struct path *path) return common_perm_cond(OP_TRUNC, path, MAY_WRITE | AA_MAY_SETATTR); } +static int apparmor_file_truncate(struct file *file) +{ + return apparmor_path_truncate(&file->f_path); +} + static int apparmor_path_symlink(const struct path *dir, struct dentry *dentry, const char *old_name) { @@ -1241,6 +1246,7 @@ static struct security_hook_list apparmor_hooks[] __lsm_ro_after_init = { LSM_HOOK_INIT(mmap_file, apparmor_mmap_file), LSM_HOOK_INIT(file_mprotect, apparmor_file_mprotect), LSM_HOOK_INIT(file_lock, apparmor_file_lock), + LSM_HOOK_INIT(file_truncate, apparmor_file_truncate), LSM_HOOK_INIT(getprocattr, apparmor_getprocattr), LSM_HOOK_INIT(setprocattr, apparmor_setprocattr), diff --git a/security/landlock/fs.c b/security/landlock/fs.c index 64ed7665455f..adcea0fe7e68 100644 --- a/security/landlock/fs.c +++ b/security/landlock/fs.c @@ -146,7 +146,8 @@ retry: #define ACCESS_FILE ( \ LANDLOCK_ACCESS_FS_EXECUTE | \ LANDLOCK_ACCESS_FS_WRITE_FILE | \ - LANDLOCK_ACCESS_FS_READ_FILE) + LANDLOCK_ACCESS_FS_READ_FILE | \ + LANDLOCK_ACCESS_FS_TRUNCATE) /* clang-format on */ /* @@ -297,6 +298,19 @@ get_handled_accesses(const struct landlock_ruleset *const domain) return access_dom & LANDLOCK_MASK_ACCESS_FS; } +/** + * init_layer_masks - Initialize layer masks from an access request + * + * Populates @layer_masks such that for each access right in @access_request, + * the bits for all the layers are set where this access right is handled. + * + * @domain: The domain that defines the current restrictions. + * @access_request: The requested access rights to check. + * @layer_masks: The layer masks to populate. + * + * Returns: An access mask where each access right bit is set which is handled + * in any of the active layers in @domain. + */ static inline access_mask_t init_layer_masks(const struct landlock_ruleset *const domain, const access_mask_t access_request, @@ -430,7 +444,7 @@ is_eacces(const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS], } /** - * check_access_path_dual - Check accesses for requests with a common path + * is_access_to_paths_allowed - Check accesses for requests with a common path * * @domain: Domain to check against. * @path: File hierarchy to walk through. @@ -465,14 +479,10 @@ is_eacces(const layer_mask_t (*const layer_masks)[LANDLOCK_NUM_ACCESS_FS], * allow the request. * * Returns: - * - 0 if the access request is granted; - * - -EACCES if it is denied because of access right other than - * LANDLOCK_ACCESS_FS_REFER; - * - -EXDEV if the renaming or linking would be a privileged escalation - * (according to each layered policies), or if LANDLOCK_ACCESS_FS_REFER is - * not allowed by the source or the destination. + * - true if the access request is granted; + * - false otherwise. */ -static int check_access_path_dual( +static bool is_access_to_paths_allowed( const struct landlock_ruleset *const domain, const struct path *const path, const access_mask_t access_request_parent1, @@ -492,17 +502,17 @@ static int check_access_path_dual( (*layer_masks_child2)[LANDLOCK_NUM_ACCESS_FS] = NULL; if (!access_request_parent1 && !access_request_parent2) - return 0; + return true; if (WARN_ON_ONCE(!domain || !path)) - return 0; + return true; if (is_nouser_or_private(path->dentry)) - return 0; + return true; if (WARN_ON_ONCE(domain->num_layers < 1 || !layer_masks_parent1)) - return -EACCES; + return false; if (unlikely(layer_masks_parent2)) { if (WARN_ON_ONCE(!dentry_child1)) - return -EACCES; + return false; /* * For a double request, first check for potential privilege * escalation by looking at domain handled accesses (which are @@ -513,7 +523,7 @@ static int check_access_path_dual( is_dom_check = true; } else { if (WARN_ON_ONCE(dentry_child1 || dentry_child2)) - return -EACCES; + return false; /* For a simple request, only check for requested accesses. */ access_masked_parent1 = access_request_parent1; access_masked_parent2 = access_request_parent2; @@ -622,24 +632,7 @@ jump_up: } path_put(&walker_path); - if (allowed_parent1 && allowed_parent2) - return 0; - - /* - * This prioritizes EACCES over EXDEV for all actions, including - * renames with RENAME_EXCHANGE. - */ - if (likely(is_eacces(layer_masks_parent1, access_request_parent1) || - is_eacces(layer_masks_parent2, access_request_parent2))) - return -EACCES; - - /* - * Gracefully forbids reparenting if the destination directory - * hierarchy is not a superset of restrictions of the source directory - * hierarchy, or if LANDLOCK_ACCESS_FS_REFER is not allowed by the - * source or the destination. - */ - return -EXDEV; + return allowed_parent1 && allowed_parent2; } static inline int check_access_path(const struct landlock_ruleset *const domain, @@ -649,8 +642,10 @@ static inline int check_access_path(const struct landlock_ruleset *const domain, layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; access_request = init_layer_masks(domain, access_request, &layer_masks); - return check_access_path_dual(domain, path, access_request, - &layer_masks, NULL, 0, NULL, NULL); + if (is_access_to_paths_allowed(domain, path, access_request, + &layer_masks, NULL, 0, NULL, NULL)) + return 0; + return -EACCES; } static inline int current_check_access_path(const struct path *const path, @@ -711,8 +706,9 @@ static inline access_mask_t maybe_remove(const struct dentry *const dentry) * file. While walking from @dir to @mnt_root, we record all the domain's * allowed accesses in @layer_masks_dom. * - * This is similar to check_access_path_dual() but much simpler because it only - * handles walking on the same mount point and only checks one set of accesses. + * This is similar to is_access_to_paths_allowed() but much simpler because it + * only handles walking on the same mount point and only checks one set of + * accesses. * * Returns: * - true if all the domain access rights are allowed for @dir; @@ -857,10 +853,11 @@ static int current_check_refer_path(struct dentry *const old_dentry, access_request_parent1 = init_layer_masks( dom, access_request_parent1 | access_request_parent2, &layer_masks_parent1); - return check_access_path_dual(dom, new_dir, - access_request_parent1, - &layer_masks_parent1, NULL, 0, - NULL, NULL); + if (is_access_to_paths_allowed( + dom, new_dir, access_request_parent1, + &layer_masks_parent1, NULL, 0, NULL, NULL)) + return 0; + return -EACCES; } access_request_parent1 |= LANDLOCK_ACCESS_FS_REFER; @@ -886,11 +883,27 @@ static int current_check_refer_path(struct dentry *const old_dentry, * parent access rights. This will be useful to compare with the * destination parent access rights. */ - return check_access_path_dual(dom, &mnt_dir, access_request_parent1, - &layer_masks_parent1, old_dentry, - access_request_parent2, - &layer_masks_parent2, - exchange ? new_dentry : NULL); + if (is_access_to_paths_allowed( + dom, &mnt_dir, access_request_parent1, &layer_masks_parent1, + old_dentry, access_request_parent2, &layer_masks_parent2, + exchange ? new_dentry : NULL)) + return 0; + + /* + * This prioritizes EACCES over EXDEV for all actions, including + * renames with RENAME_EXCHANGE. + */ + if (likely(is_eacces(&layer_masks_parent1, access_request_parent1) || + is_eacces(&layer_masks_parent2, access_request_parent2))) + return -EACCES; + + /* + * Gracefully forbids reparenting if the destination directory + * hierarchy is not a superset of restrictions of the source directory + * hierarchy, or if LANDLOCK_ACCESS_FS_REFER is not allowed by the + * source or the destination. + */ + return -EXDEV; } /* Inode hooks */ @@ -1142,9 +1155,23 @@ static int hook_path_rmdir(const struct path *const dir, return current_check_access_path(dir, LANDLOCK_ACCESS_FS_REMOVE_DIR); } +static int hook_path_truncate(const struct path *const path) +{ + return current_check_access_path(path, LANDLOCK_ACCESS_FS_TRUNCATE); +} + /* File hooks */ -static inline access_mask_t get_file_access(const struct file *const file) +/** + * get_required_file_open_access - Get access needed to open a file + * + * @file: File being opened. + * + * Returns the access rights that are required for opening the given file, + * depending on the file type and open mode. + */ +static inline access_mask_t +get_required_file_open_access(const struct file *const file) { access_mask_t access = 0; @@ -1162,19 +1189,95 @@ static inline access_mask_t get_file_access(const struct file *const file) return access; } +static int hook_file_alloc_security(struct file *const file) +{ + /* + * Grants all access rights, even if most of them are not checked later + * on. It is more consistent. + * + * Notably, file descriptors for regular files can also be acquired + * without going through the file_open hook, for example when using + * memfd_create(2). + */ + landlock_file(file)->allowed_access = LANDLOCK_MASK_ACCESS_FS; + return 0; +} + static int hook_file_open(struct file *const file) { + layer_mask_t layer_masks[LANDLOCK_NUM_ACCESS_FS] = {}; + access_mask_t open_access_request, full_access_request, allowed_access; + const access_mask_t optional_access = LANDLOCK_ACCESS_FS_TRUNCATE; const struct landlock_ruleset *const dom = landlock_get_current_domain(); if (!dom) return 0; + /* - * Because a file may be opened with O_PATH, get_file_access() may - * return 0. This case will be handled with a future Landlock + * Because a file may be opened with O_PATH, get_required_file_open_access() + * may return 0. This case will be handled with a future Landlock * evolution. */ - return check_access_path(dom, &file->f_path, get_file_access(file)); + open_access_request = get_required_file_open_access(file); + + /* + * We look up more access than what we immediately need for open(), so + * that we can later authorize operations on opened files. + */ + full_access_request = open_access_request | optional_access; + + if (is_access_to_paths_allowed( + dom, &file->f_path, + init_layer_masks(dom, full_access_request, &layer_masks), + &layer_masks, NULL, 0, NULL, NULL)) { + allowed_access = full_access_request; + } else { + unsigned long access_bit; + const unsigned long access_req = full_access_request; + + /* + * Calculate the actual allowed access rights from layer_masks. + * Add each access right to allowed_access which has not been + * vetoed by any layer. + */ + allowed_access = 0; + for_each_set_bit(access_bit, &access_req, + ARRAY_SIZE(layer_masks)) { + if (!layer_masks[access_bit]) + allowed_access |= BIT_ULL(access_bit); + } + } + + /* + * For operations on already opened files (i.e. ftruncate()), it is the + * access rights at the time of open() which decide whether the + * operation is permitted. Therefore, we record the relevant subset of + * file access rights in the opened struct file. + */ + landlock_file(file)->allowed_access = allowed_access; + + if ((open_access_request & allowed_access) == open_access_request) + return 0; + + return -EACCES; +} + +static int hook_file_truncate(struct file *const file) +{ + /* + * Allows truncation if the truncate right was available at the time of + * opening the file, to get a consistent access check as for read, write + * and execute operations. + * + * Note: For checks done based on the file's Landlock allowed access, we + * enforce them independently of whether the current thread is in a + * Landlock domain, so that open files passed between independent + * processes retain their behaviour. + */ + if (landlock_file(file)->allowed_access & LANDLOCK_ACCESS_FS_TRUNCATE) + return 0; + return -EACCES; } static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = { @@ -1194,8 +1297,11 @@ static struct security_hook_list landlock_hooks[] __lsm_ro_after_init = { LSM_HOOK_INIT(path_symlink, hook_path_symlink), LSM_HOOK_INIT(path_unlink, hook_path_unlink), LSM_HOOK_INIT(path_rmdir, hook_path_rmdir), + LSM_HOOK_INIT(path_truncate, hook_path_truncate), + LSM_HOOK_INIT(file_alloc_security, hook_file_alloc_security), LSM_HOOK_INIT(file_open, hook_file_open), + LSM_HOOK_INIT(file_truncate, hook_file_truncate), }; __init void landlock_add_fs_hooks(void) diff --git a/security/landlock/fs.h b/security/landlock/fs.h index 8db7acf9109b..488e4813680a 100644 --- a/security/landlock/fs.h +++ b/security/landlock/fs.h @@ -37,6 +37,24 @@ struct landlock_inode_security { }; /** + * struct landlock_file_security - File security blob + * + * This information is populated when opening a file in hook_file_open, and + * tracks the relevant Landlock access rights that were available at the time + * of opening the file. Other LSM hooks use these rights in order to authorize + * operations on already opened files. + */ +struct landlock_file_security { + /** + * @allowed_access: Access rights that were available at the time of + * opening the file. This is not necessarily the full set of access + * rights available at that time, but it's the necessary subset as + * needed to authorize later operations on the open file. + */ + access_mask_t allowed_access; +}; + +/** * struct landlock_superblock_security - Superblock security blob * * Enable hook_sb_delete() to wait for concurrent calls to release_inode(). @@ -50,6 +68,12 @@ struct landlock_superblock_security { atomic_long_t inode_refs; }; +static inline struct landlock_file_security * +landlock_file(const struct file *const file) +{ + return file->f_security + landlock_blob_sizes.lbs_file; +} + static inline struct landlock_inode_security * landlock_inode(const struct inode *const inode) { diff --git a/security/landlock/limits.h b/security/landlock/limits.h index b54184ab9439..82288f0e9e5e 100644 --- a/security/landlock/limits.h +++ b/security/landlock/limits.h @@ -18,7 +18,7 @@ #define LANDLOCK_MAX_NUM_LAYERS 16 #define LANDLOCK_MAX_NUM_RULES U32_MAX -#define LANDLOCK_LAST_ACCESS_FS LANDLOCK_ACCESS_FS_REFER +#define LANDLOCK_LAST_ACCESS_FS LANDLOCK_ACCESS_FS_TRUNCATE #define LANDLOCK_MASK_ACCESS_FS ((LANDLOCK_LAST_ACCESS_FS << 1) - 1) #define LANDLOCK_NUM_ACCESS_FS __const_hweight64(LANDLOCK_MASK_ACCESS_FS) diff --git a/security/landlock/setup.c b/security/landlock/setup.c index f8e8e980454c..3f196d2ce4f9 100644 --- a/security/landlock/setup.c +++ b/security/landlock/setup.c @@ -19,6 +19,7 @@ bool landlock_initialized __lsm_ro_after_init = false; struct lsm_blob_sizes landlock_blob_sizes __lsm_ro_after_init = { .lbs_cred = sizeof(struct landlock_cred_security), + .lbs_file = sizeof(struct landlock_file_security), .lbs_inode = sizeof(struct landlock_inode_security), .lbs_superblock = sizeof(struct landlock_superblock_security), }; diff --git a/security/landlock/syscalls.c b/security/landlock/syscalls.c index 2ca0ccbd905a..245cc650a4dc 100644 --- a/security/landlock/syscalls.c +++ b/security/landlock/syscalls.c @@ -129,7 +129,7 @@ static const struct file_operations ruleset_fops = { .write = fop_dummy_write, }; -#define LANDLOCK_ABI_VERSION 2 +#define LANDLOCK_ABI_VERSION 3 /** * sys_landlock_create_ruleset - Create a new ruleset diff --git a/security/security.c b/security/security.c index bdc295ad5fba..b967e035b456 100644 --- a/security/security.c +++ b/security/security.c @@ -185,11 +185,12 @@ static void __init lsm_set_blob_size(int *need, int *lbs) { int offset; - if (*need > 0) { - offset = *lbs; - *lbs += *need; - *need = offset; - } + if (*need <= 0) + return; + + offset = ALIGN(*lbs, sizeof(void *)); + *lbs = offset + *need; + *need = offset; } static void __init lsm_set_blob_sizes(struct lsm_blob_sizes *needed) @@ -1694,6 +1695,11 @@ int security_file_open(struct file *file) return fsnotify_perm(file, MAY_OPEN); } +int security_file_truncate(struct file *file) +{ + return call_int_hook(file_truncate, 0, file); +} + int security_task_alloc(struct task_struct *task, unsigned long clone_flags) { int rc = lsm_task_alloc(task); diff --git a/security/tomoyo/tomoyo.c b/security/tomoyo/tomoyo.c index 71e82d855ebf..af04a7b7eb28 100644 --- a/security/tomoyo/tomoyo.c +++ b/security/tomoyo/tomoyo.c @@ -135,6 +135,18 @@ static int tomoyo_path_truncate(const struct path *path) } /** + * tomoyo_file_truncate - Target for security_file_truncate(). + * + * @file: Pointer to "struct file". + * + * Returns 0 on success, negative value otherwise. + */ +static int tomoyo_file_truncate(struct file *file) +{ + return tomoyo_path_truncate(&file->f_path); +} + +/** * tomoyo_path_unlink - Target for security_path_unlink(). * * @parent: Pointer to "struct path". @@ -545,6 +557,7 @@ static struct security_hook_list tomoyo_hooks[] __lsm_ro_after_init = { LSM_HOOK_INIT(bprm_check_security, tomoyo_bprm_check_security), LSM_HOOK_INIT(file_fcntl, tomoyo_file_fcntl), LSM_HOOK_INIT(file_open, tomoyo_file_open), + LSM_HOOK_INIT(file_truncate, tomoyo_file_truncate), LSM_HOOK_INIT(path_truncate, tomoyo_path_truncate), LSM_HOOK_INIT(path_unlink, tomoyo_path_unlink), LSM_HOOK_INIT(path_mkdir, tomoyo_path_mkdir), diff --git a/tools/testing/selftests/landlock/base_test.c b/tools/testing/selftests/landlock/base_test.c index da9290817866..792c3f0a59b4 100644 --- a/tools/testing/selftests/landlock/base_test.c +++ b/tools/testing/selftests/landlock/base_test.c @@ -75,7 +75,7 @@ TEST(abi_version) const struct landlock_ruleset_attr ruleset_attr = { .handled_access_fs = LANDLOCK_ACCESS_FS_READ_FILE, }; - ASSERT_EQ(2, landlock_create_ruleset(NULL, 0, + ASSERT_EQ(3, landlock_create_ruleset(NULL, 0, |